# **[Rahul Kadyan](https://znck.me)** I write about software, technology, and occasionally, life. I also [build things](projects/index.html.md). *** ## 2026 **[Tracking Work Alongside Spec-Driven Development](articles/2026-06-18-tracking-the-work/index.html.md)** *(June 18, 2026)*\ How I added a lightweight `tracking/` worklog to spec-driven development so agents can recover branch state, handoff context, and deferred work without rebuilding everything from chat history. **[Project Hail Mary](articles/2026-03-18-project-hail-mary/index.html.md)** *(March 18, 2026)*\ Thoughts on Andy Weir's Project Hail Mary: amnesia as a storytelling engine, elegant solutions to escalating problems, and the quiet brilliance of disciplined risk management. **[The Ability to Be Wrong Cheaply](articles/2026-03-05-the-ability-to-be-wrong-cheaply/index.html.md)** *(March 5, 2026)*\ When engineering time stops being the most expensive part of the process, the calculus flips. A story about building, pivoting, and learning that being wrong is the cheapest part of the loop. **[Stepping Away from the Terminal](articles/2026-03-04-stepping-away-from-the-terminal/index.html.md)** *(March 4, 2026)*\ I haven't touched my laptop in a few weeks. Building an iOS app entirely from my phone using Claude Code remote control, spec-driven development, and a feedback loop tight enough to pivot the product in an evening. **[What Building AI Agents Taught Me](articles/2026-02-27-what-building-ai-agents-taught-me/index.html.md)** *(February 27, 2026)*\ Building Citation Finder and Fact Checker at Grammarly broke my assumption that the same input produces the same output. Lessons on nondeterminism, evaluation over testing, ML objective coupling, and grounding citations in structured data. **[Designing a Desktop Overlay Platform](articles/2026-02-27-designing-a-desktop-overlay-platform/index.html.md)** *(February 27, 2026)*\ Grammarly works everywhere you write — as a transparent overlay on top of whatever app you're using, across five platforms from a single codebase. The architecture behind it: execution scopes, a plugin system, and a thin protocol boundary between platform-specific and shared code. **[Taming Code Generation: An Exploration in Scaling Myself](articles/2026-02-02-taming-code-generation/index.html.md)** *(February 2, 2026)*\ When code generation becomes cheap and fast, the bottleneck shifts to clear thinking. An exploration of spec-driven development workflows to scale yourself with AI-assisted coding. **[Shipping "Books at Home" in Two Days](articles/2026-01-12-shipping-books-at-home-in-two-days/index.html.md)** *(January 12, 2026)*\ How I built and shipped a fully-featured app for managing physical book collections in just two days—no prototypes, no MVPs, just agency and focused design. **[Small Things Like These](articles/2026-01-04-small-things-like-these/index.html.md)** *(January 4, 2026)*\ A reflection on Claire Keegan's powerful novella about moral courage, silence, and the cost of conscience in 1980s Ireland. ## 2025 **[A No-Fuss Option Type for TypeScript](articles/2025-11-23-simple-option-type/index.html.md)** *(November 23, 2025)*\ A minimal Option type for TypeScript that expresses absence cleanly without defensive checks. Built on discriminated unions and control-flow narrowing to keep things simple and predictable. **[Designing a result type that feels native in TypeScript](articles/2025-11-22-designing-a-result-type/index.html.md)** *(November 22, 2025)*\ A journey through designing a Result type that integrates naturally with TypeScript's control flow, using Promises as inspiration to create an error-handling pattern that feels like part of the language. **[A Tale of Two Monorepos](articles/2025-07-16-a-tale-of-two-monorepos/index.html.md)** *(July 16, 2025)*\ In this post, I share the story of two monorepos I designed at work. One at work and one at home. The differences in their design, structure, and the lessons learned from each. **[Zero Config Approach](articles/2025-07-15-zero-config-approach/index.html.md)** *(July 15, 2025)*\ Every monorepo starts out clean, but as it scales, configuration files can become a burden. This post discusses how we embraced conventions and tag-driven automation to eliminate repetitive boilerplate and configuration drift. **[Reclaim Toolchain Ownership](articles/2025-07-14-reclaim-toolchain-ownership/index.html.md)** *(July 14, 2025)*\ Continuing the monorepo series, this post discusses the importance of owning your toolchain. It’s about customizing your development environment to fit your needs, rather than being constrained by default settings. **[Single Version Policy](articles/2025-07-11-single-version-policy/index.html.md)** *(July 11, 2025)*\ Next in the series on monorepo learnings: how we tackled dependency chaos with a Single Version Policy. This approach ensures all packages share the same version of dependencies. **[Before the Coffee Gets Cold](articles/2025-07-07-before-the-coffee-gets-cold/index.html.md)** *(July 7, 2025)*\ Would you go back in time if you cannot change the past? Before the Coffee Gets Cold is a gentle, meditative exploration of longing, regret, and unresolved feelings through the lens of time travel. **[Package-First Philosophy](articles/2025-07-02-package-first-philosophy/index.html.md)** *(July 2, 2025)*\ Last year, I had the opportunity to design a monorepo at work. The experience taught me valuable lessons about code organization and team dynamics. This is the first in a series sharing those learnings—starting with how we made packages the center of our development universe. ## 2020 **[Negative feedback comes from a good place](articles/2020-01-14-appreciate-feedback/index.html.md)** *(January 14, 2020)*\ Reflections on the importance of honest feedback and how to give and receive it constructively in professional environments. ## 2019 **[Shipping Web Components with Vue](articles/2019-11-09-shipping-web-components-with-vue/index.html.md)** *(November 9, 2019)*\ A comprehensive guide on how to build and export Vue components as web components using Vue CLI and Rollup, making them reusable across different frameworks. **[Simple Hard Things](articles/2019-06-10-simple-hard-things/index.html.md)** *(June 10, 2019)*\ Exploring the challenges of getting seemingly simple design details right, particularly around typography and spacing in web design. **[Grammarly in Code](articles/2019-06-03-grammarly-in-code/index.html.md)** *(June 3, 2019)*\ The journey of building a VS Code extension to integrate Grammarly into the code editor, solving the pain of switching between editors while writing. **[Finally Some Colors](articles/2019-06-03-finally-some-colors/index.html.md)** *(June 3, 2019)*\ Adding color and visual improvements to enhance the reading experience of a blog. **[Comments in Code](articles/2019-05-26-comments-in-code/index.html.md)** *(May 26, 2019)*\ A detailed exploration of different types of code comments - clarification, documentation, license, reference, tagged, and control comments - with examples from Vue.js source. **[Running an awesome conference](articles/2019-05-20-awesomeconf/index.html.md)** *(May 20, 2019)*\ The story of organizing Vue.js conferences in India, from the initial inspiration to the challenges and rewards of bringing the Vue community together. ## 2018 **[Type Vuex without TypeScript](articles/2018-11-20-type-vuex/index.html.md)** *(November 20, 2018)*\ A continuation guide showing how to add type checking and intellisense to Vuex stores using JSDoc annotations and helper patterns, without switching to TypeScript. **[Type Vue without TypeScript](articles/2018-11-11-type-vue/index.html.md)** *(November 11, 2018)*\ A practical guide demonstrating how to get TypeScript-like type checking and intellisense in Vue components using JSDoc annotations and VS Code, without actually using TypeScript. ## 2017 **[Accessible Components: Test Driven Approach](articles/2017-11-29-accessible-components/index.html.md)** *(November 29, 2017)*\ How to use automated accessibility testing in Vue components to ensure they meet ARIA standards and provide inclusive user experiences. **[Provide/Inject in Vue 2.2](articles/2017-03-10-provide-inject/index.html.md)** *(March 10, 2017)*\ An exploration of Vue's provide/inject feature introduced in version 2.2, showing how it enables dependency injection patterns for advanced component communication. **[Fiddle with Vue.js](articles/2017-03-05-fiddle-with-vue/index.html.md)** *(March 5, 2017)*\ A guide for Vue.js contributors on how to create proper minimal reproductions for bug reports, including best practices for JSFiddle and repository setups. **[First Day in Vue.js](articles/2017-03-03-first-day/index.html.md)** *(March 3, 2017)*\ The personal story of discovering Vue.js while struggling with Angular, and how Vue's simplicity and elegance made complex interactive features achievable in just a few hours. # Tracking Work Alongside Spec-Driven Development A few months ago, I wrote about using specs to [tame code generation](../2026-02-02-taming-code-generation/index.html.md). The argument was that code generation can speed up implementation, but it does not remove the need to write down intent. If the only source of truth is a chat transcript, review requires reconstructing what I meant. Specs helped solve that. They gave me a contract before code existed. Acceptance criteria made implementation and review more objective. A related issue came up as I used more agents in the same project: even when the spec was clear, agents still had to rebuild execution context. They needed to read the repo, infer what had already happened, understand what was still open, and decide where the current branch fit. The spec described the intended behavior. It did not describe the current state of the work. Jonathon Vandamme shared useful context with me around tracking work chronology: keeping a record of how work moves through a project. I took that idea and developed it further for agent coordination. So I added a separate layer: **track execution context explicitly**. ## What Specs Do Not Cover Spec-driven development answers a product question: > What behavior should exist when this is done? That is the right question before implementation. It keeps the work anchored to user value, workflows, constraints, and acceptance criteria. But agents also need operational context: * Which branch is carrying the work? * Which task is active right now? * Was there a backlog item? * Which acceptance criteria or conformance IDs are touched? * Did another session already claim part of the work? * What was intentionally deferred? * Which README or project docs need to stay aligned? These questions sit between the spec and git history. Git records what changed. Specs record what should be true. Neither gives an agent a concise view of what is currently happening on a branch. Without that view, each new session spends time rebuilding context from commits, diffs, docs, and conversation history. That adds overhead, especially when more than one agent session touches the same area of the repo. ## The `tracking/` Model The `tracking/` directory is a small worklog system: ```text tracking/ +-- backlog/ +-- progress/ +-- tech-debt/ ``` Each part has one job. `tracking/backlog/` captures work before it becomes a branch. It is useful when an issue is known, but not yet implemented. `tracking/progress/` captures branch-level coordination. There is one progress file per branch or PR. `tracking/tech-debt/` captures deferred work. This prevents cleanup from being forgotten while also keeping the current branch small. The progress file is the main coordination document. Its job is to tell an agent where the branch stands without requiring a full reconstruction from git history and chat context. ```markdown # feat-rpc-retry - exponential backoff for an RPC client Branch: `feat/rpc-retry` PR: #482 Spec: [2026-06-10-rpc-retries](../spec/features/2026-06-10-rpc-retries.md) Backlog: [rpc-retry](../backlog/rpc-retry.md) ## Tasks | Task | Status | Owner | Notes | | -------------------- | ------ | ----- | ------------ | | Wire up retry policy | done | otter | RPC-RETRY-01 | | Add jitter | wip | maple | RPC-RETRY-02 | | Backfill tests | todo | - | RPC-RETRY-03 | ## Notes - otter: editing retry policy code and related tests. - maple: downstream caller depends on `RetryPolicy.makeDefault()` staying stable. - maple: deferred jitter tuning -> [tech-debt](../tech-debt/rpc-jitter.md) ``` This file is intentionally brief. It is an index, not a narrative. The branch points to the spec, PR, backlog item, and any deferred work. The task table shows the current state. Notes preserve only the handoff details that git does not capture. ## Why One File Per Branch The branch is the right unit for this because it matches how agent work is actually reviewed. A branch can contain several tasks. It can implement a spec, close a backlog item, introduce follow-up debt, and update docs. A future agent needs those links in one place. The file naming rule makes that stable: ```text tracking/progress/-.md ``` The date comes from the first branch-specific commit when one exists. If the branch has no unique commits yet, the file uses today's date and keeps that name for the life of the branch. That rule prevents agents from creating duplicate progress files as the branch evolves. ## Example: A JavaScript Runtime I used this process while building out a JavaScript runtime in a private repo. The work had several related streams: a command-line executable, module loading, fetch support, web-platform test coverage, cryptography APIs, structured clone, and byte transport between Swift and JavaScriptCore. That work had specs for the user-visible and architectural changes, including native byte transport and fetch streaming. The specs described what the runtime needed to support. The progress file described how the branch was moving through the implementation. The progress file for that branch linked the specs, then tracked the implementation as a task table: ```markdown Branch: `codex/native-byte-bridge` PR: #257 Spec: [native-byte-bridge](../spec/features/native-byte-bridge.md), [fetch streaming](../spec/features/fetch-streaming.md) | Task | Status | Owner | Notes | | ---------------------------------- | ------ | ----- | --------------------------------- | | Add fetch test harness | done | codex | local server + resource shim | | Build executable and module loader | done | codex | async import, import maps, REPL | | Add native byte bridge | done | codex | ArrayBuffer + typed-array support | | Stream production fetch responses | done | codex | native stream -> ReadableStream | | Maintain specs | done | codex | activated completed feature specs | ``` The useful part was not the table by itself. It was the combination of links and status. An agent could open the progress file and see that fetch streaming was already implemented, redirect behavior had test coverage, browser-only CORS behavior was intentionally out of scope, and active tech debt was clean for the PR. That is the kind of context that is easy to lose if it only exists in chat history. The same branch also showed why one progress file per branch matters. Earlier progress shards were folded into a single file, so the PR had one coordination surface. Git kept the detailed history. The progress file kept the operational summary. ## What Changed in `AGENTS.md` I moved this into `AGENTS.md` as a required workflow step. The key rule is: > Every work item updates `tracking/`. That applies to spec'd work, unplanned fixes, documentation changes, and chores. A small change can have a small progress entry, but it still gets one. The workflow now makes tracking explicit: 1. Read the target package README and relevant docs. 2. Create or switch to a branch. 3. Pick a backlog item if one exists. 4. Identify conformance IDs if the work has them. 5. Implement the smallest coherent slice. 6. Add or update tests. 7. Run the relevant checks. 8. Update `tracking/`. 9. Open or update a PR. The tracking step is near the end because it records what happened, but it also affects work in progress. Agents claim rows, mark tasks `wip`, move them to `done`, and add notes when a decision matters for handoff. For multi-session work, this makes `tracking/progress/` the shared coordination surface. ## How It Combines With Specs Specs and tracking are separate because they represent different kinds of truth. This separation keeps specs product-focused. A spec should not become a running work diary. It should describe behavior, workflows, constraints, and acceptance criteria. The progress file handles the operational state. It can say who is working on a row, what checks ran, which backlog item was reconciled, and what follow-up moved to tech debt. The link between them is the useful part: ```markdown Spec: [2026-06-10-rpc-retries](../spec/features/2026-06-10-rpc-retries.md) Backlog: [rpc-retry](../backlog/rpc-retry.md) ``` An agent can start at the progress file, open the spec for product intent, inspect the PR for review context, and follow tech-debt links for deferred work. That keeps the reconstruction step smaller. ## What Belongs in Progress Notes Progress files are not commit logs. Git already stores commit history better than Markdown ever will. A progress note should capture information that is useful for coordination and not obvious from the diff: * a file another session is actively editing * a compatibility constraint that affected the implementation * a link to deferred work * a reason a task is still `wip` * a check that could not run and why It should avoid restating every commit, every file change, or every implementation detail. The goal is fast orientation. A future agent should understand branch state in under a minute. ## Agent Context Agents are good at reading code, but making every session rediscover context is wasteful. The repeated pattern looks like this: 1. The spec exists. 2. The branch has partial implementation. 3. The chat history contains useful context. 4. A new session starts. 5. The agent spends time reconstructing branch state from scattered evidence. `tracking/` gives that state a durable place inside the repo. This is especially useful when threads compact, sessions pause, or multiple agents touch nearby files. The progress file becomes the first document to read after the README and spec. It does not replace review. It reduces how much context review has to rebuild. ## Tradeoffs The obvious cost is maintenance. Every work item has one more file to update. I think that cost is acceptable because the entries are short and scoped. A documentation-only change might add a single task row and one note. A larger feature might link to a spec, backlog item, PR, and tech-debt note. The other risk is duplication. If progress files start repeating commit history or spec content, they become stale. The fix is to keep them as indexes: task state, ownership, links, and handoff notes. The system works if each file owns a distinct concern. ## Result Spec-driven development made intent explicit. `tracking/` makes execution context explicit. Together, they give agents two things they need before changing code: * the product contract * the current state of the work That has become the important distinction in my project. Specs tell an agent what good looks like. Tracking tells the agent where the branch is and what remains. The article I wrote in February focused on clarity before implementation. This layer adds clarity during implementation. That is the practical difference: less reconstruction, fewer stale assumptions, and a cleaner handoff from one agent session to the next. # Project Hail Mary I just read Project Hail Mary. Picked it up expecting a fun sci-fi read. Put it down thinking about how we approach problems, communication, and risk. Andy Weir wrote a book where the protagonist wakes up alone on a spaceship with no memory of how he got there. The amnesia becomes the entire storytelling engine. Every time Grace remembers something, we learn it alongside him. The past and present unfold in parallel, each informing the other. We piece together the mission the same way he does: gradually, with growing stakes. The reader and the protagonist share the same information at the same time. We're genuinely solving this together. The problems in this book escalate relentlessly. Saving a star system. Communicating with an alien species. Surviving physics that wants to kill you in twelve different ways. The solutions stay grounded. Weir reaches for high school science, basic engineering, and first-principles reasoning. Grace uses what he has: a centrifuge, a spectrometer, basic math. The creativity comes from applying known tools in new ways. And the science itself stays honest. Astrophage is the one truly alien concept: a living organism that stores absurd amounts of energy. Everything else follows from known science. If astrophage is alive and its population is controlled, something must be eating it. Predator-prey dynamics. Basic ecology. One speculative leap, then logic takes over. The communication with Rocky (the alien) starts with knocking on walls. Slowly, painstakingly, a shared vocabulary emerges. What struck me was watching it grow to the point where they could discuss relativistic time dilation and understand sarcasm. The language slowly accumulated, the way real understanding does: word by word, misunderstanding by misunderstanding, until suddenly two species separated by everything can crack jokes. Stratt might be the most compelling character in the book, and she's not even on the ship. Her approach to the mission is a masterclass in risk management: choose proven technology, reduce failure modes, make hard calls early. She optimizes for survival probability. Every decision filters through one question: does this reduce the chance of mission failure? Her brilliance is discipline. ![Project Hail Mary](https://upload.wikimedia.org/wikipedia/en/a/ad/Project_Hail_Mary%2C_First_Edition_Cover_%282021%29.jpg) Good stories give you a plot to follow and a lens to keep. This one left me with a few: start with what you know, build communication patiently, choose proven tools over clever ones, and let complexity emerge from simple foundations. # The Ability to Be Wrong Cheaply I built a speed reading app. Used it for two weeks. Realized it was solving the wrong problem. Pivoted it into an RSS reader in a few hours. That sequence used to take months. Research phase, build phase, beta phase, feedback phase, pivot phase. Each one with its own timeline and cost. The fear of getting it wrong justified spending weeks making sure we got it right upfront. But what if getting it wrong costs almost nothing? ## The Real Unlock We talk a lot about building faster. AI-assisted development, code generation, spec-driven workflows. The conversation centers on speed: how quickly can we go from idea to working software? That's the wrong frame. **The real unlock isn't building faster. It's changing direction faster.** Speed matters, but not for the reason we think. A faster build cycle doesn't just mean shipping sooner. It means the feedback loop between idea, ship, and learn compresses so tightly that being wrong stops being expensive. And when being wrong is cheap, everything about how we make product decisions changes. ## What Cheap Mistakes Look Like The app is called Foveate. My wife showed me an Instagram reel of someone speed reading, and I'd never heard of RSVP before. Words flashing one at a time at a fixed point on screen. It intrigued me. I wrote 18 feature specs covering PDFs, ebooks, importing from open libraries, calibration, training mode. The vision was expansive. I started with [research and specs](../2026-02-02-taming-code-generation/index.html.md), then built the core engine in two days. I used it as my daily reader for two weeks. The speed reading engine worked exactly as specced. ORP anchoring, adaptive pacing, comprehension checks. Technically, it was solid. Then I started paying attention to what I was actually reading. Engineering blog posts with code blocks. Articles with architecture diagrams. Newsletters with pull quotes and embedded tweets. Speed reading works for quick, linear text. But most of what I read isn't linear. I can't flash a code snippet one word at a time. The engine I'd built served maybe 20% of my reading. The other 80% needed a normal reading experience. **The app wasn't wrong. The frame was.** What I actually needed was a way to keep up with the engineering world. An RSS reader where speed reading was a small feature, not the whole product. Subscribe to sources, read articles in a web view, but when the backlog grows, hit "Catch Up" and speed-read through LLM-generated summaries in three minutes. The specs made the pivot surgical. I described the new direction to Claude, and it looked at the spec index, figured out which specs to cut and which to add, and planned the path from the old vision to the new one. One PR stripped the library, training, calibration, and onboarding. Another added a feed timeline, daily digests, and an article web view. The pivot deleted 5,600 lines and added 2,758. The app got smaller and more focused at the same time. Total elapsed time from "this isn't right" to "this is what I actually need": a few hours. ## Why This Changes How We Think The traditional approach to product development assumes that mistakes are expensive. Requirements gathering, user research, prototyping, validation — all of these exist because building the wrong thing used to cost months of engineering time. When engineering time stops being the most expensive part of the process, the calculus flips. We don't need to be right upfront. We need to be honest quickly. I wrote a while back that [the gap between wanting to build something and shipping it is rarely technical anymore](../2026-01-12-shipping-books-at-home-in-two-days/index.html.md). That was about the first half of the loop: idea to ship. But the second half matters just as much: ship to learn. When both halves are cheap, the whole loop tightens. This doesn't mean specs and research are wasted effort. Code has become a cheap artifact, something Claude can regenerate in minutes. The specs are what matter. They're the shared understanding between me and the coding agent. Foveate's feature specs made it possible to rethink the entire product without losing context. The investment shifts. Less time making sure the plan is perfect. More time making sure the plan is easy to change. ## The Uncomfortable Part There's a psychological cost to this that's worth naming. Building something, using it, and admitting it's wrong — in the same week — requires a kind of honesty that doesn't come naturally. We get attached to what we build. The speed reading engine worked well. It was good engineering. Admitting it solved the wrong problem felt like admitting failure. But the cost of holding onto something that doesn't fit is higher than the cost of letting go. And when letting go means a few hours of work instead of months of sunk cost, the decision gets easier. ## What This Means Engineering used to be the expensive part, so we built entire processes around not wasting it. Requirements documents, approval gates, lengthy design phases. All of it optimized for a world where building the wrong thing cost months. That world is fading. When engineering loops are cheap, being wrong is easy. And when being wrong is easy, we can stop optimizing for correctness upfront and start optimizing for learning. # Stepping Away from the Terminal I haven't touched my laptop in a few weeks. That sentence sounds wrong, even to me. I'm a software developer and I've recently been building for Apple platforms, which means building on a Mac. Not optional. The entire development workflow is tethered to macOS. Except lately, I haven't needed to sit in front of one. There's a script running on my Mac right now: ```bash while true; do claude remote-control; done ``` That's it. The machine sits in the basement, screen off. I start Claude Code sessions from my phone, and the Mac picks them up. Builds install on my phone over home Wi-Fi. I review diffs on my phone, describe what I want, and watch the commits land. This didn't happen overnight. It happened in stages. Each one feeling like a small step and looking like a large one in retrospect. ## Four Steps Back In September, I switched from GitHub Copilot to Claude in VS Code. That was the obvious move. Better completions, better understanding of intent, better at holding context across files. In November, I closed VS Code entirely. Claude Code in the terminal felt more natural than any editor integration. Simpler interface, fewer distractions. I still had the code on my machine, but I stopped looking at it in an editor. The code started feeling like a side effect of the conversation, not the main event. In January, I moved to Claude Code on the web. The conversation wasn't even on my local machine anymore. But I still needed Xcode open to build, run, and validate. I completed multiple tasks throughout the day, but at the end of it I still had to verify the build on the laptop. In February, that last thread snapped. `claude remote-control` let me drive the Mac from my phone. The app built and installed over Wi-Fi, and I verified it right there. No IDE open. No terminal open. No laptop screen on. Each step felt like giving up control. Each step turned out to be giving up friction. **A phone and an idea became enough to start building.** ## What I Was Building The app is called Foveate. My wife showed me an Instagram reel of someone speed reading, and I'd never heard of RSVP before. It intrigued me, so I started exploring. Research, specs, then code. Built in two days, then used it for two weeks as my daily reader. Then I noticed the app didn't fit my actual reading habits. I didn't need a speed reading app. I needed a way to keep up with the engineering world, where speed reading was just one tool in the box. The product needed to change. And I didn't need a laptop to change it. ## Building from the Phone I did the pivot entirely from my phone. I was on the couch, thinking about the app, and I just started. Opened Claude Code on my phone, explained the new vision, and Claude captured it in a spec and started implementing. The workflow felt ordinary. I described what I wanted, Claude Code made the changes, built the app, and launched it on my phone. I tested the experience, gave feedback, and it fixed things and relaunched. That was the dev loop. ## Specs Made This Possible I wrote about [spec-driven development](../2026-02-02-taming-code-generation/index.html.md) a few weeks ago. Foveate is where that workflow proved itself. Developing from a phone means no browsing code. No visually scanning a file to understand its structure. No holding a mental model of the codebase by scrolling through directories. But I can read a spec. And when the spec is clear enough, the code becomes a detail I don't need to hold in my head. The initial vision of Foveate was expansive. 18 feature specs covering PDFs, ebooks, importing from open libraries, and more. When I realized building everything around RSVP wasn't what I wanted, the specs made the pivot tractable. I described the new direction to Claude: an RSS reader with articles I could skim through and LLM-generated digests to help me catch up. Claude looked at the spec index, figured out which specs to cut and which to add, and planned the path from the old vision to the new one. Without specs, a pivot like that would have meant re-reading code to understand what existed. With specs, it was a conversation about intent. ## What This Feels Like It feels like dictating a letter to someone who types faster than me. I'm still the one deciding what to say, still the one who knows the recipient and the purpose. The mechanical part has been absorbed. My job shrank to the part that was always mine. The thinking. I don't want to oversell this. Reading diffs on a phone is not pleasant. Complex architectural decisions still benefit from a larger screen. Debugging a subtle layout issue is easier when I can inspect the view hierarchy directly. There are real limitations. But the default has shifted. The phone is enough most of the time, which means I can build whenever inspiration strikes. The laptop has become the exception, not the primary tool. ## The Real Story None of the individual parts are remarkable. AI-assisted coding exists. Remote development exists. Product pivots happen all the time. What's notable is how they combine to make software development more accessible. Each step away from the traditional workflow removed a dependency I'd assumed was essential. No specific machine, no specific editor, no desk required. A phone and an idea turned out to be enough. The barrier to creating software is lower than it's ever been. I'm curious what I'll build next from the couch. Books at Home app icon # Books at Home Your complete personal book library manager. Catalog your collection, track your reading journey, and share with friends and family. ## Download Available on the App Store for iPhone, iPad, and Mac. Download on the App Store ## Features ### Library Management Add books via ISBN barcode scanning, search, or manual entry. Organize with filters and sorting by title, author, date, or rating. ### Reading Tracker Track your reading status, monitor page-by-page progress, record start and finish dates, and rate books with personal notes. ### Lending & Borrowing Never lose track of lent books. Track borrowers, set due dates, and get reminders for overdue loans. ### Wishlist Save books you want to read or buy with priority levels. Quick-add wishlist items to your library when you're ready. ### Library Sharing Share your collection with friends and family via iCloud. Browse libraries shared with you and request to borrow books. Your personal notes and purchase info stay private. ### Statistics & Insights See your total books and pages read, visualize reading status distribution, track books finished over time, and discover your most-read authors. ## Support ### Frequently Asked Questions #### How do I add books to my library? You can add books in three ways: 1. **Scan ISBN barcode** - Use the camera to scan the barcode on your book 2. **Search** - Search by title, author, or ISBN 3. **Manual entry** - Add book details manually #### How does iCloud sync work? Books at Home automatically syncs your library across all your Apple devices signed into the same iCloud account. Make sure iCloud is enabled in your device settings. #### How do I share my library? Go to More > My Sharing to create a share link. You can invite specific people to view your book collection via iCloud. #### What data is shared when I share my library? Only book metadata is shared: titles, authors, covers, ratings, and reading status. Your personal notes, lending information, purchase prices, and storage locations remain private. #### How do I export my data? Go to Settings > Export Library to download your entire library as a CSV file. #### How do I delete all my data? Go to Settings > Danger Zone > Delete All Books. This action cannot be undone. ### Report an Issue Found a bug or have a feature request? Please open an issue on GitHub: * [GitHub Issues](https://github.com/znck/support/issues) ### Contact For other inquiries, you can reach out via: * Email: [Privacy Policy](privacy-policy/index.html.md) Foveate app icon # Foveate Speed Read Your World. The world moves fast. Foveate helps you keep up. Subscribe to blogs, news sites, and publications you care about. Get AI-powered summaries and speed-read articles at 300–600 WPM with rapid serial visual presentation. ## Download Available on the App Store for iPhone and iPad. Download on the App Store ## Features ### RSS Feeds Subscribe to any RSS or Atom feed. Articles appear in a clean timeline grouped by day. Pull to refresh, filter by feed, search across all your articles, and save for offline reading. ### AI Summaries Apple Intelligence creates daily and weekly digests so you can catch up fast. Everything is processed on your device. ### Speed Reader Read one word at a time at a fixed point on the screen. Each word is anchored at its optimal recognition point and timing adapts automatically — slowing for longer words and pausing at sentence boundaries. Choose from three pace profiles: Cruise (steady speed), Flow (adapts dynamically), or Training (ramps up gradually). ### Themes Four reading themes built in. Midnight (warm charcoal with cinnabar accent), Parchment (cream with deep brick red), Overcast (cool gray with soft scarlet), and Pure Dark (true black for OLED). Or follow your system appearance automatically. ### Privacy No ads, no tracking, no analytics, no subscriptions. You pay once and get full access. Your data syncs across devices with iCloud. ## Support ### Frequently Asked Questions #### How does speed reading work? Instead of scanning lines of text, you read one word at a time at a fixed point on the screen. Each word is positioned for quick recognition. Most people read around 250 words per minute. With Foveate, you can comfortably reach 300–600 WPM while still understanding what you read. #### What are the pace profiles? * **Cruise** — Holds a steady speed throughout * **Flow** — Adapts dynamically as you read, slowing for complex words * **Training** — Starts slow and gradually ramps up #### How do AI summaries work? Foveate uses Apple Intelligence to generate daily and weekly digests of your feeds. Everything is processed on your device — no data is sent to external servers. #### Does it sync across devices? Yes. Your feeds, articles, and settings sync across iPhone and iPad with iCloud. ### Report an Issue Found a bug or have a feature request? Please open an issue on GitHub: * [GitHub Issues](https://github.com/znck/support/issues) ### Contact For other inquiries, you can reach out via: * Email: [Privacy Policy](privacy-policy/index.html.md) # Projects A collection of apps and open source projects I've built. ### Foveate Speed read your RSS feeds with AI summaries and rapid serial visual presentation. Subscribe to blogs and news sites, get daily digests from Apple Intelligence, and read articles at 300–600 WPM. [Learn more](../apps/foveate/index.html.md) ### Books at Home Your personal book library manager for iPhone, iPad, and Mac. Catalog your collection, track your reading journey, and share with friends and family. [Learn more](../apps/books-at-home/index.html.md) ### Moss A static site generator that processes Markdown with Vue components. Features include syntax highlighting, math rendering, and custom directives. [Learn more](https://moss.znck.dev) ### Grammarly for VS Code A VS Code extension that integrates Grammarly directly into the editor, providing real-time grammar and spelling checks while you write. [GitHub](https://github.com/znck/grammarly) ### Vue Developer Experience A collection of tools to enhance the Vue.js development experience, including language server support and IDE integrations. [GitHub](https://github.com/vuedx) # What Building AI Agents Taught Me I've spent most of my career in deterministic systems. Rendering engines, plugin architectures, monorepo tooling. Problems where a function returns the same output for the same input, a test passes or fails, and correctness is binary. That mental model served me well for years. Then I was asked to build an AI agent. Actually, two of them. At Grammarly, we built the [Citation Finder](https://www.grammarly.com/ai-agents/citation-finder) and Fact Checker: agents that scan a student's essay, identify claims that need evidence, find credible sources on the web, and generate properly formatted citations. One finds sources to support your arguments. The other flags when your claims contradict the evidence. Building them broke something in my engineering intuition. The assumption I'd carried through every system I'd ever built — **the same input produces the same output** — didn't hold anymore. ## The pipeline that taught me humility My first day on the project, I sat down to build what I thought was straightforward: take an essay, find claims, search for sources, show results. I assumed the hard part would be the UI. I was wrong about everything. The hard part is that LLMs are unreliable in *what they say*. Not "sometimes they're slow" unreliable. The same prompt, the same essay, run twice: different claims detected, different sources found, different citation formatting. A comma missing between two author names in APA style. An ampersand where there should be an "and." A journal title italicized when it shouldn't be. The output *looks* right but isn't. The agent works in four stages: 1. **Claim detection**: The LLM reads the essay and identifies verifiable claims, statements that could be supported or contradicted by evidence. 2. **Source search**: For each claim, a web search finds relevant sources. The LLM ranks them by relevance. 3. **Evidence classification**: Each source is classified as supporting, contradicting, or debating the claim. 4. **Citation formatting**: Source metadata is formatted into APA, MLA, or Chicago style. Each stage can fail independently. And silently. The claim detector misses a claim or flags a subjective opinion as factual. The web search returns irrelevant results. The relevance ranking is opaque: the LLM generates a score, but you can't inspect its reasoning. The citation formatter hallucinates author names and publication dates. My instinct was to build abstractions. Define interfaces. Make things composable and testable. But you can't unit test an LLM's judgment. You can test that the pipeline runs without crashing. You can't test that it's *right*. ## Testing becomes evaluation What you *can* do is use LLM-as-a-judge: a second model evaluates the first model's output across hundreds of essays, scoring claim detection quality, source relevance, and citation accuracy. The judge has its own biases. But it catches regressions that would otherwise ship silently, and it scales in a way manual review doesn't. Pass/fail doesn't exist here. You build scoring frameworks and track quality as a distribution over time. A regression isn't a red test. It's a shift in the curve. I had to learn to think in distributions rather than assertions, and to build systems that degrade gracefully when any stage produces garbage. The pipeline was running. The evaluation scores were okay. Not bad, not great. It took us a while to realize the problem wasn't in any single stage. It was in what we were asking the pipeline to do. ## Two goals that couldn't live together We launched the first version as a single agent that handled both citation finding and fact checking. It seemed natural. Both tasks start with claim detection, both involve web search, both show sources to the user. But the goals conflicted at the ML level. Citation finding needs high recall: surface everything that *might* need a citation. You'd rather show an unnecessary suggestion than miss a claim that should be cited. Fact checking needs high precision: only flag things that are actually contradicted by evidence. A false positive, telling a student their correct claim is wrong, is worse than missing a disputed claim. Same pipeline, opposite tuning needs. We tried to balance both and ended up with a system that was mediocre at each. The fix was embarrassingly obvious in hindsight: if two features optimize for different objectives, they shouldn't share a model. We split them into two separate agents. Claim detection got tuned independently for each. The quality of both improved immediately. This felt like a product lesson. It was really an engineering lesson about coupling. I knew not to couple code with different change frequencies. I didn't realize the same principle applies to ML objectives. ## Citations need grounding, not generation Of everything I worked on, citation formatting taught me the most about where LLMs should and shouldn't be trusted. APA, MLA, and Chicago each have hundreds of rules. The format depends on the source type, the number of authors, the presence of a DOI, the edition, whether it was accessed online. The right approach isn't to ask the LLM to generate citation metadata. It's to ground citation search in authoritative indices that already maintain structured data. Academic databases, paper repositories, and publisher APIs have accurate author names, publication dates, DOIs, and journal metadata. When your search index provides structured data, formatting becomes a deterministic transformation rather than a generative task. The LLM's role shifts from *generating* citation data to *selecting* the right source and *mapping* the user's claim to the relevant evidence. Generation where the model is strong: reasoning, relevance. Structured data where precision matters: metadata, formatting. ## What this changed for me Building these agents taught me things that didn't fit into my existing mental model: **Determinism is a luxury.** Most of my career, correctness was binary. LLMs don't work that way. You build for variance, not just for edge cases. **Evaluation replaces testing.** You can't assert that an LLM is *right*. You can track whether it's getting *worse*. That requires a fundamentally different relationship with quality. **Coupling applies to ML objectives, not just code.** If two features optimize for different things, they shouldn't share a pipeline. We learned this by shipping a coupled version and watching both features underperform. **"Good enough" is a design decision, not a compromise.** In deterministic systems, "good enough" means you haven't finished. In probabilistic systems, it means you've chosen where to spend your error budget. Citations need to be near-perfect. Source ranking can tolerate more noise. I went in thinking I was building a feature. I came out thinking differently about what it means to build software when the core computation is nondeterministic. That shift applies to a lot more than citation finding. # Designing a Desktop Overlay Platform Grammarly works everywhere you write. Word, Google Docs, Slack, your browser, native desktop apps. It draws underlines, shows suggestion cards, and runs an AI assistant, all as a transparent overlay on top of whatever application you're using. Making that work across macOS, Windows, Chrome Extension, Safari Extension, and the web from a single codebase is a harder problem than it looks. I designed the platform behind it. This is about the architectural decisions and the ideas that make it work. ## Why this needed to exist The product ships on five platforms: macOS, Windows, Chrome Extension, Safari Extension, and the web. For years, each platform had its own UI implementation. Features were built multiple times by different teams. Coverage drifted. A bug fixed on one platform would resurface on another months later. The company has far more web engineers than native engineers. But the desktop clients could only be worked on by the native teams. Every new feature (an AI assistant, a suggestion panel redesign, a growth experiment) had to be implemented separately on each platform, by people who were already stretched thin. Previous attempts at unification picked the wrong level of abstraction. Both worked in controlled conditions but neither survived the pace of real development. Server-Driven UI sent serialized UI state from the server to clients. It worked for frozen features, but versioning across multiple active renderer versions was a nightmare, the single-purpose component count ballooned, and the unconventional authoring model created a steep learning curve. Too brittle for rapid iteration. A platform-agnostic bridge layer let native and web code call each other via RPC. Clean in theory, but the contract kept growing. Every new capability required contributions to each platform *and* the shared codebase. Building on existing primitives was easy; needing a new one meant touching six codebases. Too costly for rapid iteration. *Both solved for correctness. Neither solved for velocity.* The insight was that not everything *needs* to be shared. Accessibility APIs on Windows work nothing like accessibility APIs on macOS, which work nothing like the DOM in a browser. Text access is inherently platform-specific. But what you *do* with that text is the same everywhere. Checking it, showing suggestions, rendering an assistant. That's where all the feature work happens. So the architecture draws a line. Platform-specific code handles text access and window management. Shared web code (TypeScript, React) handles everything else. The boundary is a protocol, not an abstraction layer. Each side is honest about what it can and can't do. ## The constraints Building a desktop overlay sounds simple. Float a transparent window above the editor. Draw underlines. Show a card when the user hovers. Then you realize the window needs to be click-through, so mouse events reach the app underneath. But you still need to know when the cursor hovers over your UI. Click-through and pointer-aware at the same time — on the same window. Then you need multiple windows (suggestions, assistant, settings) sharing state. Then you need all of it working on macOS, Windows, browser extensions, and web from the same codebase. No single technology solves all of these. ## Three layers The system splits into three execution contexts: ``` HOST Tauri (or web host) Native windows, mouse tracking, shortcuts FOREGROUND TypeScript, one per window React rendering BACKGROUND TypeScript, SharedWorker Plugin host, business logic, state ``` **The host** is Tauri on desktop (Electron would work too). It creates native windows, tracks mouse events outside webview bounds, and registers global shortcuts. On web, an iframe manager does the equivalent. **The background** is a singleton because business logic (backend connections, authentication, state) must be shared across all windows. A SharedWorker survives individual window closures and prevents duplicate connections. **The foreground** is per-window because each UI panel needs its own rendering context. Every window connects to the same background, forming a hub-and-spoke model. On desktop, a "main window" bootstraps everything. It starts at 0x0 pixels, invisible. It spawns the SharedWorker and establishes the data provider connection. Then it joins the window pool and gets reused as the first visible window. No wasted resources. This separation is what lets a web engineer ship a feature without touching native code. They work in the foreground and background layers — TypeScript, React, familiar tools. The host layer is someone else's problem. ## Execution scopes If I had to pick the single most important idea in this architecture, it's execution scopes. An execution scope defines a period during which a task is performed, with access to a set of resources. When the scope ends, everything inside it is cleaned up automatically. Scopes form a tree: ``` Global Scope (entire application lifetime) │ ├── Connection Scope (per data provider connection) │ │ │ ├── Document Scope (per text field being edited) │ │ │ │ │ └── Interaction Scope (hover → dismiss) │ │ │ └── Document Scope (another document) │ └── Connection Scope (another connection) ``` The global scope holds authentication and the window manager. A connection scope is created when a text data provider connects. A document scope is created when the user focuses a text field. An interaction scope lives for a single hover-and-card interaction. Resources are created when you need them. Cleaned up when you don't. ### Why this matters Without scopes, you'd either create everything at startup (wasteful), manually track each resource's lifecycle (error-prone), or use a flat dependency injection container (no hierarchy, no automatic cleanup). Scopes tie resource lifecycle to semantic application events. When a user closes a document, the document scope disposes. That cascades: the backend connection closes, the text model is cleaned up, suggestion state is cleared, UI subscriptions are cancelled. No manual cleanup code. No forgotten teardown. ``` Document Scope disposes ├── Child interaction scopes disposed first ├── Backend connection closed ├── Text model cleaned up ├── UI subscriptions cancelled └── Parent scope notified (not disposed) ``` `AsyncDisposableStack` handles it in LIFO order. Every resource implements `Symbol.dispose` — subscriptions, connections, UI elements, scope controllers. JavaScript's [explicit resource management](https://github.com/tc39/proposal-explicit-resource-management) proposal (`using` declarations and `DisposableStack`) was a perfect fit. Every `onStart` callback returns a `Disposable`. Every registration returns a `Disposable`. The scope collects them all. This pairs with [`Result`](../2025-11-22-designing-a-result-type/index.html.md) to unify error handling and resource management. No try/catch. No finally blocks. If a registration fails, everything that succeeded before it is rolled back through the same disposal mechanism. ### The scope is the operating system Every feature activates within a scope. Every resource is owned by a scope. Cross-window communication happens through scope-scoped entities. I started thinking of scopes as the operating system for the plugin layer: * **Process lifecycle** → Scope start/stop * **Memory management** → Disposal cascade * **Dependency injection** → DI container per scope, inheriting from parent * **IPC** → Remote entities over MessagePort * **Isolation** → Scope boundaries The hierarchy isn't fixed. Any plugin can define new scope types: ```typescript // A plugin declares a new scope type interface ScopeRegistry { readonly interaction: unknown } ``` This extensibility lets ephemeral UI scopes (a hover interaction lasting milliseconds) coexist with long-lived structural scopes (the global application state) in the same tree. ## Plugins Every feature is a plugin. Roughly 30 in production. Each plugin is an independent package with code that can run in the background and in the foreground, communicating via RPC. A plugin's activation is remarkably constrained. It can: 1. Resolve dependencies on other plugins 2. Register services on scope definitions 3. Register lifecycle callbacks That's it. No imperative "do stuff now." Everything is declarative: *when this scope starts, here's what I provide.* ```typescript export const activate = (context) => { const documentScope = context.getScopeDefinition('document') // Register a service on the scope definition documentScope.register({ token: SuggestionServiceToken, useFactory: (resolver) => resolver.resolve(BackendClient).map((client) => new SuggestionService(client)), }) // When a document scope starts, initialize the service documentScope.onStart((execution) => { const service = execution.resolve(SuggestionServiceToken) return service.connect() // returns Disposable }) } ``` The pattern: register a service on a scope definition. When a scope instance starts (a user focuses a text field), the service is created and initialized. When the scope ends (the user moves to another document), the returned `Disposable` cleans it up. The plugin never manages lifecycle directly. It declares what it provides and when. This design gives you two things that matter at scale. **Loose coupling.** Plugins never import each other's code. Each plugin has a public surface: just tokens and interfaces, no implementation. A plugin declares what it *provides* and what it *consumes*. The DI system wires them together at runtime. A suggestion plugin consumes a user-info token without knowing which plugin provides it. Plugins can be swapped, extended, or disabled without cascading changes. Each plugin ships a test double, so any plugin can be tested in isolation. **Automatic hygiene.** A plugin can't leak resources even if the author forgets to clean up. The scope owns the lifecycle. When the scope ends, everything the plugin registered is disposed. With 30 plugins from different teams, this is the difference between "works in isolation, breaks in production" and "works by construction." Each plugin spans two realms, background and foreground, connected through scope-level remote entities. The background holds the data; the foreground renders the UI. They're separate execution contexts with no shared memory, but the scope system keeps them in sync. This is the entanglement story. ## The data provider abstraction A data provider gives the platform read-write access to text on screen. It implements a protocol using whatever platform APIs are available: * **macOS desktop**: Accessibility tree parsing * **Windows desktop**: UI Automation APIs * **Chrome / Safari**: DOM APIs * **Web**: Host page DOM The protocol is the same everywhere: ``` documentDidFocus(request) documentTextDidChange(request) // OT operations documentGeometryDidChange(request) getDocumentText(request) applyDocumentTextChange(request) // OT operations ... ``` Text changes use operational transform operations (insert, delete, retain). The protocol is defined in an IDL and code-generated into multiple languages. A handful of methods. That's the entire contract between platform-specific code and shared application logic. A plugin that processes suggestions doesn't know or care whether the text came from a native app via accessibility APIs or from a textarea via the DOM. This is what makes "build once, run everywhere" real — not by hiding the platform, but by making the boundary so small that platform differences don't leak through. The data provider protocol also drives the scope lifecycle. When a data provider connects, a connection scope is created. When it sends `documentDidFocus`, a document scope is created. When the connection drops, the connection scope disposes and everything underneath it is cleaned up automatically. Every document scope, every interaction scope, every service. The protocol is the heartbeat. Scopes are the response. ## Entanglement The background runs in a SharedWorker. Each window runs in its own context. They need to share state, but they can't share memory. Two scopes in separate execution contexts, no shared memory, yet kept in sync. In practice, it's eventually consistent — changes propagate over MessagePort, not instantaneously. But from a plugin author's perspective, the abstraction holds. It works in two parts. **Structure.** When a window opens, the background serializes its scope tree. Just the shape, not the data. The foreground reconstructs the same hierarchy: same scope types, same IDs, same parent-child relationships. This is cheap. New scopes created in the background after the window opens are automatically mirrored. **Access.** Services registered in the background are exposed to the foreground as remote objects over MessagePort. The foreground calls methods on what looks like a local object; the calls are proxied to the background and the results come back. Both sides must be operating at the same scope level. You can't resolve a document-scoped service from the global scope. The interesting part is that windows choose which scopes to entangle: ```typescript createWindow({ name: 'suggestion-card', scopes: ['interaction'], // only this scope }) ``` A suggestion card window entangles one scope. Its ancestors are entangled too (the structure is always consistent), but remote objects are lazy. Nothing is resolved until a plugin asks for it, so the cost is just the tree shape. The assistant window entangles three scopes and actively uses services from all of them. A devtools window entangles zero. Complete isolation from production data. * **Suggestion card** entangles `['interaction']`: card state only * **Text overlay** entangles `['document']`: document data only * **Assistant** entangles `['global', 'connection', 'document']`: everything * **DevTools** entangles `[]`: nothing Security, performance, and resource management in one mechanism. A lightweight popup initializes fast with minimal memory. A full-featured panel gets the access it needs. Nothing gets more than it asked for. ## Click-through transparency The hardest problem. The overlay must be: 1. Visually transparent: see the app underneath 2. Input transparent: clicks pass through 3. Pointer-aware: know where the mouse is Requirements 2 and 3 fight each other. If clicks pass through your window, you don't receive mouse events. But you need mouse events to know when the user hovers over an underline. The core solution is **event proxying**: intercept mouse events at the OS level and forward them to the overlay window, even though the window itself isn't receiving them natively. On top of that, we built **tracking areas**: rectangular regions that plugins register to declare "I care about pointer events here." Tracking areas serve two purposes: they filter which events get proxied (you don't need every mouse move across the entire screen), and they enable fast switching between click-through and opaque mode. When the cursor enters a tracking area, the window becomes interactive for that region. This is how a user can move from hovering over a text decoration (click-through window) to clicking a button on the suggestion card (opaque window) without any visible transition. The event interception is platform-specific: **macOS.** A Core Graphics event tap intercepts mouse events system-wide on a private thread. **Windows.** A low-level mouse hook captures events on a dedicated thread. **Web.** The host page listens for pointer events and forwards them to the iframe via `postMessage`. We tried and rejected several approaches on Windows before landing on this one. DLL injection into the browser process worked but antivirus software blocked it for a significant percentage of users. RPC-based event forwarding (like the web approach) introduced noticeable latency on lower-end machines. ## What changed The AI assistant was the first real test. Before this platform, it would have been built separately on each native client by the native teams, with the web team building their own version in parallel. Instead, one team built it as a plugin. It's rolling out across all five platforms from a single codebase. That became the pattern. Feature teams write plugins, register services on scopes, and the platform handles the rest. They don't touch native code. They don't coordinate across platform teams. They don't even need to know whether the text came from an accessibility tree or a DOM element. Teams that previously couldn't contribute to desktop now own features across all five platforms. The ratio of web engineers to native engineers went from a bottleneck to an advantage. Roughly a hundred packages. 30 plugins. Five platforms. One codebase. ## The ideas that generalize I think the most useful parts of this work aren't specific to writing tools. They're structural ideas that apply to any application facing similar constraints. **Execution scopes** as a hierarchical resource lifecycle primitive. Not flat DI containers, but a tree where disposal cascades and resources are tied to semantic events. **Partial entanglement** for per-window access control and performance. Windows declare what they need, and nothing more gets wired up. **Event proxying with tracking areas** for pointer awareness on click-through transparent windows. The constraint seems impossible until you move event interception to the OS level and let plugins declare regions of interest. **A protocol boundary** between platform-specific data access and shared application logic. Not an abstraction that hides the platform. A boundary that's honest about it. Any application that needs transparent overlays, multi-window coordination, or cross-platform rendering from a single web codebase faces the same problems. The specific answers may differ, but the shape of the questions is the same. # Taming Code Generation: An Exploration in Scaling Myself Gone are the days when your imagination was out of reach. Builders used to spend months typing away lines of code, inching towards their vision. We are living in a true golden age of software development. Code is cheap now. Your vision and agency are in the driver's seat. In this moment, you have graduated from the orchestra. Now you are the composer. I wrote recently about [vision and agency in shipping books at home in two days](../2026-01-12-shipping-books-at-home-in-two-days/index.html.md). But this shift has arrived, not without its own problems. You have to ensure that your vision is getting executed as per your expectation.

You. Are. The. Bottleneck. Now.

I feel the same way. I have more unreviewed implementations than unimplemented ideas. And this is my attempt to scale myself. ## The Problem With Moving Fast Here's what typically happens when you use LLMs without explicit specifications: You ask Claude Code to build a feature. You describe what you want in the message. Claude builds it. Then you review, find ambiguities, ask for changes. Claude updates. Back and forth. Back and forth. The real problem? Your intent only lives in that conversation. There's no single source of truth. No checklist. No way to verify "did the code actually implement what we agreed on?" Reviews become expensive. You have to mentally reconstruct the spec from code, then check if the code matches. That's backwards. But more importantly, this pattern doesn't scale. As you add more features, more decisions, more complexity, the conversation gets longer. The drift gets wider. Your thinking gets slower relative to the machine's speed. You're not actually reviewing code. You're reverse-engineering intention. ## The Shift: Specs Before Code I started experimenting with a different approach. What if the spec came first? Not as documentation added after. As the actual blueprint, written before implementation, that drives everything—implementation, testing, and review. **Make specs precede code.** Not in theory. In practice. Every feature has a written spec before Claude Code touches it. Every spec has clear acceptance criteria. Every review checks the code against those criteria. The shift is small. The impact is different. Instead of: "Does this code implement the feature correctly?" (vague, requires context) You ask: "Does this code satisfy acceptance criterion #1? #2? #3?" (objective, checklist-based) Reviews go from two hours (reconstructing intent) to fifteen minutes (checking a list). ## The System I built a system around four skills, each serving a specific purpose in the workflow. I've open-sourced it at [znck/spec-driven-development](https://github.com/znck/spec-driven-development). ``` spec/ ├── CLAUDE.md # Main instructions for spec-driven development └── skills/ ├── spec-feature-writer/ │ └── SKILL.md # Draft feature specifications ├── spec-decision-writer/ │ └── SKILL.md # Document architectural decisions ├── spec-e2e-test-generator/ │ └── SKILL.md # Generate E2E tests from acceptance criteria └── spec-maintenance/ └── SKILL.md # Update indices, activate features ``` The skills follow the [agentskills.io](https://agentskills.io) format, which means they work as slash commands in Claude Code (e.g., `/spec-feature-writer`). The `CLAUDE.md` file is the entry point. It tells Claude Code when to use spec-driven development (new user-facing functionality, behavior changes, architectural decisions, multi-component changes) and when to skip it (bug fixes, small tweaks, docs). Then the instructions in `CLAUDE.md` set up the full project structure: ``` spec/ ├── CLAUDE.md # Instructions for Claude Code ├── README.md # Product overview (you create this) ├── skills/ │ └── ... ├── features/ │ ├── README.md # Feature index (WIP, Active, Inactive) │ └── YYYY-MM-DD-.md └── decision-records/ ├── README.md # Decision index (Active, Superseded) └── YYYY-MM-DD-.md ``` ## Four Skills, One Workflow **Skill 1: `/spec-feature-writer`** Before Claude Code builds anything, you use this skill. It asks clarifying questions about user value, workflows, and edge cases. Then it drafts a spec with four essential parts: * **User Value**: Why does the user care? Not "implement voice interviewing", but "users practice interviews with realistic feedback, without needing a human partner" * **How It Works**: User-centric description. What does the user experience? * **Key Interactions**: Edge cases and behaviors. What happens when the user pauses? When connection drops? * **Acceptance Criteria**: 5-8 testable requirements. These become your review checklist. The acceptance criteria are the contract. The code either satisfies them or it doesn't. **Skill 2: `/spec-decision-writer`** Why did you make certain architectural choices? This skill documents them. It captures the question you were answering, the decision, the rationale, user impact, and what alternatives you rejected. Decisions go to `spec/decision-records/YYYY-MM-DD-.md`. This prevents repeating old arguments and helps anyone understand the "why" behind choices. **Skill 3: `/spec-e2e-test-generator`** Tests come from specs. Each acceptance criterion gets a corresponding test. The mapping is direct: Test names match spec language. When a test fails, you immediately know which criterion wasn't satisfied. **Skill 4: `/spec-maintenance`** This skill keeps everything in sync. When a feature ships: update status from WIP to Active, check all acceptance criteria boxes, update the feature index, add to Recent Changes. The maintenance skill enforces a key rule: only WIP specs can be edited. Active specs are frozen. To change behavior, you create a new spec. This prevents spec drift after implementation. ## How the Workflow Actually Works The workflow has five phases: **1. Draft** Use `/spec-feature-writer` or `/spec-decision-writer`. Ask clarifying questions about user value, workflows, edge cases. Draft spec with acceptance criteria. Iterate based on feedback. Save to `spec/features/` or `spec/decision-records/`. **2. Lock** Summarize acceptance criteria, confirm with user. This is the contract. From here, implementation must satisfy exactly what's written—nothing more, nothing less. **3. Implement** Claude Code reads the spec thoroughly, implements to satisfy acceptance criteria, and flags ambiguities immediately instead of guessing: ``` SPEC CLARIFICATION NEEDED [Quote ambiguous part] Interpretations: 1) ... 2) ... Which is correct? ``` **4. Test** Use `/spec-e2e-test-generator`. One test per criterion. Test names match spec language. Run tests to verify implementation. **5. Activate** When tests pass, confirm with user, then use `/spec-maintenance` to check acceptance criteria boxes, update status from WIP to Active, and update indices. That's it. Specs drive code. Tests validate specs. Code passes tests. ## The Three States Specs have three states with strict editing rules: This is crucial. Once a spec goes Active, it's frozen. The code matches the spec. The tests validate the spec. Everything is aligned. To change behavior, you don't edit the Active spec. You create a new WIP spec, go through the workflow again, and the old one gets deprecated if needed. This prevents the drift that destroys most documentation systems. ## The Review Process Changes Everything This is where the real shift happens. Instead of spending two hours reconstructing what should have happened, you spend fifteen minutes checking: does the code satisfy criterion #1? Yes. Criterion #2? Yes. Criterion #3? Yes. Ship it. Here's a real example. A feature spec for "Voice-Based Interview Sessions" has acceptance criteria like: * User can start speaking and system captures voice * Speech is transcribed to text * Claude generates a contextual response * Response is played as audio * Timer counts down * User can pause and resume without losing state * User can manually end interview For each one, you ask: Does the code do this? Measurable. Objective. Done. And because Claude Code implemented to this spec, the answer is almost always yes. The code passes tests. The tests validate the criteria. Everything aligns. Code reviews become about acceptance criteria, not style or efficiency. The spec is the arbiter. ## What I'm Still Learning I'm not claiming this is solved. I'm exploring it. Some things I've discovered: **Specs scale better than code.** When you have five features, the spec system saves time. When you have fifty features, it saves your sanity. The clarity compounds. **Acceptance criteria are the contract.** They're not suggestions or aspirations. They're the definition of "done". Once all criteria pass, you're finished. This prevents scope creep. **The frozen Active state is powerful.** When specs can't change after implementation, you're forced to think clearly before locking. And you never have documentation drift. **Flagging ambiguities beats guessing.** When Claude Code says `SPEC CLARIFICATION NEEDED` instead of making assumptions, you catch misalignment early. This saves hours of rework. **The real bottleneck is intent clarity.** Not execution speed. Not tool capability. How clearly you can think about what you want. That's what limits everything else. ## The Deeper Pattern I think what's really happening is this: the faster the machine gets at generating code, the more clarity matters. Slow tools hide unclear thinking. When it takes a human two weeks to write code, small ambiguities don't matter. You figure it out as you go. Fast tools expose unclear thinking. When Claude Code implements in minutes, ambiguity becomes immediately visible. You spend your time reconstructing intent instead of building features. So the game changes. You're not competing with the machine on speed. You're competing with clarity of thought. And the way you scale clarity is through specs. Through forcing yourself to think carefully before asking the machine to build. ## Getting Started If you want to try this, I've packaged everything into a repository you can drop into your project. **Install with one command:** ```bash curl -L https://github.com/znck/spec-driven-development/archive/main.tar.gz | tar -xz --strip-components=1 -C . spec-driven-development-main/spec ``` Or clone and copy manually: ```bash git clone https://github.com/znck/spec-driven-development.git cp -r spec-driven-development/spec ./spec ``` Once installed, Claude Code will automatically follow the spec-driven workflow when you request significant changes. But here's the full process if you want to understand it: 1. **Pick a feature.** Something you're about to build. 2. **Use `/spec-feature-writer`.** Let it ask clarifying questions. Draft the spec with user value, how it works, key interactions, and acceptance criteria. 3. **Make sure each criterion is testable.** "User can adjust timer from 5–90 minutes" is testable. "System is responsive" is not. 4. **Lock the spec.** Confirm acceptance criteria with yourself. This is the contract. 5. **Implement.** Claude Code reads the spec, implements to criteria, flags ambiguities. 6. **Test.** Generate tests from criteria using `/spec-e2e-test-generator`. Run them. 7. **Activate.** When tests pass, use `/spec-maintenance` to update status and indices. The barrier to entry is low. The impact compounds over time. ## In Closing I started this exploration because I was drowning in code reviews. The machine moved faster than I could think. What I've learned is that the real scaling isn't about moving faster. It's about thinking more clearly before the machine starts. Specs do that. They force clarity. They make you write down "why" before "what". They turn vague intentions into testable contracts. And they let you review in minutes instead of hours. Is this a complete solution? No. I'm still exploring. I'm still learning what works and what doesn't. But I'm confident about this: in a world where code generation is cheap and fast, the scarce resource is clear thinking. And specs are how you cultivate that. That's what I'm attempting. That's what I'm learning. I'd be curious to hear what you discover if you try it. # Shipping "Books at Home" in Two Days I shipped "Books at Home" in two days. Not a prototype.\ Not a rough MVP.\ A real app—on the App Store—with barcode scanning, iCloud sync, lending, wishlists, and a small statistics view. When people hear that, they usually ask *how*. They expect a story about speed, intensity, or some clever trick. The truth is simpler than that. I didn't move fast. I just didn't wait. ## The Spark My wife and I live with physical books. The kind that quietly spread—onto shelves, bedside tables, and the floor near the bed. Over time, the collection grew into hundreds of books. Do we already own this?\ Who borrowed that copy of *Thinking, Fast and Slow*?\ Why did we buy the same book twice? We tried a few apps. They all felt slightly off. Too social. Too gamified. Poor UX. Ads everywhere. Too focused on reading goals when the real problem was much more basic: *what do we actually have?* Some just didn't feel right—and that's reason enough. I wanted something that felt like Apple Books—but for physical books. Clean. Private. Functional. So I built it. ## Agency Over Intelligence [Andrej Karpathy](https://x.com/karpathy/status/1894099637218545984) recently wrote about agency, and it landed immediately: > Agency is significantly more powerful and significantly more scarce. Agency isn't raw intelligence. It isn't deep expertise or perfect preparation. It's the quiet assumption that you can act—and adjust as you go. When I started building Books at Home, I didn't first "learn" VisionKit or CloudKit. I didn't create a study plan or block off time to research. I decided to solve our book problem. Everything else was downstream of that decision. ## Design Before Code Before writing code, I spent time thinking—not abstractly, but concretely. What does this app actually need to do? Not what *could* it do.\ Not what would look impressive. Just this: 1. Know what books we own 2. Find a book quickly 3. Remember who borrowed it 4. Track books we want to buy That was enough. I pictured the app while making coffee. A simple tab bar. A grid of covers. Search at the top. Tap for details. Long-press for quick actions. No wireframes. No Figma. Just a clear sense of how it should *feel*. Apple Books—but physical.\ Quiet. Fast. Obvious. ## Architecture Is Mostly Commitment Architecture tends to sound grand. In practice, it's mostly about committing and moving on. I used SwiftData. Not because it's perfect, but because it's a good-enough choice for a new Apple app today. I organized the code by feature. Because when I think "lending," I want to know where to look, even if Claude is writing the code, I would know where to look. Each of these decisions could have been debated. None of them needed to be. Most architectural regret doesn't come from making the wrong choice—it comes from delaying the choice entirely. You can always refactor later. You can't refactor something that doesn't exist. ## Product Sense Is Mostly Subtraction The initial plan had more ideas than what shipped. Smart collections. Widgets. Reading goals. Borrower contact integration. Most of it didn't make the cut. At one point, I started adding a contact picker so you could select borrowers from your address book. Halfway through, I stopped. Do you really need contact integration to remember that *Sam* borrowed a book? A text field shipped. The picker didn't. Same with smart collections. Useful, eventually. Not necessary now. If you have 50 books, you can scroll. If you have 500, we'll revisit it. The wishlist is simple. The stats are modest. The lending reminders are basic. None of that prevents the app from doing its job. ## Coding Wasn't the Hard Part Claude wrote about 95% of the code. I'm not exaggerating. I described what I wanted, made decisions when options appeared, and course-corrected when things drifted. The actual typing of Swift? Mostly not me. This isn't a story about AI replacing developers. It's a story about what becomes possible when implementation is no longer the bottleneck. VisionKit already knows how to scan barcodes. SwiftData already knows how to sync with iCloud. Claude already knows how to wire them together. My job was knowing *what* to build and *why*. The architecture decisions. The product cuts. The "good enough for now" calls. When something didn't work perfectly, I didn't stop to debug it myself. I described the problem, got a fix, moved on. The goal wasn't to write every line.\ It was to ship something real. ## Momentum Is a Side Effect of Decisions Two days isn't about speed. It's about momentum. Momentum comes from making small decisions quickly and letting them compound. Once the data model existed, the UI followed naturally.\ Once the UI existed, gaps became visible.\ Once the gaps were visible, priorities became obvious. The app didn't emerge from a detailed plan. It emerged from a sequence of committed steps. ## What Agency Actually Looks Like Agency doesn't mean certainty. I wasn't sure everything would work smoothly. I wasn't confident there wouldn't be bugs or trade-offs. Agency is acting anyway. The app isn't complete. It isn't polished in every corner. It isn't exactly what I imagined at the start. But it exists. Last weekend, my wife and I scanned all 203 books. It took about fifteen minutes. Now we know what we own. When someone borrows a book, we track it. When we're at a bookstore, we check. That's enough. ## Build Like You Don't Need Permission If you want to ship faster, the answer usually isn't better tools or deeper knowledge. It's deciding that you're allowed to act before everything is clear. What would you build if you trusted yourself to figure things out as you go?\ What decision are you postponing because you're waiting for certainty? The gap between *wanting* to build something and *shipping* it is rarely technical, or at least, not any more. It's agency. Books at Home isn't my cleverest code.\ But it's real.\ And it exists because I didn't wait. *** *[Books at Home](https://apps.apple.com/us/app/books-at-home/id6757405333) is available on the App Store for iPhone, iPad, and Mac.* # Small Things Like These I just finished Claire Keegan's *Small Things Like These*, and I'm still sitting with the weight of it. ![The town as I pictured it, generated with ChatGPT](../../_assets/2026-01-small-things-town-DN_1Erad.png) The story unfolds in a small Irish town in the 1980s—dark, coal-covered, cold. I could picture it so clearly: muddy roads, a ravine with a water stream splitting the town in half, connected by a stone bridge. On one side stands the convent, tall and pointy, imposing with its grandeur and mystery. On the other side, across that bridge, is Bill Furlong's modest home. The town market sits at a Y-shaped intersection, a big square where the forks meet, on the same side as Furlong's house at the bottom of the market street. The coal yard—Furlong's livelihood—lies further north of the convent, a liminal space connecting both worlds. That geography matters. The ravine doesn't just divide the town physically—it represents the moral divide Furlong must cross. Furlong is a coal merchant, a good man living a peaceful life with his wife and five daughters. But he carries inner turmoil from his childhood: he lost his mother young, never knew his father. He sees the world through two lenses—gratitude for the life he has, and an incomprehensible awareness of suffering that others choose to ignore. Keegan masterfully conveys the thought storms that erupt in him when small details trigger memories and questions he can't silence. His moral compass was shaped by Mrs. Wilson, the Protestant woman who took in his unwed mother when she was pregnant with him. That act of mercy gave him life, gave him a chance. Mrs. Wilson and her husband Ned raised him to be the poised, humble, helpful man he is. He exists as living proof that compassion matters, that one person's kindness can alter everything. Which makes what he discovers at the convent unbearable. The Magdalene laundries—where "fallen" girls are imprisoned, abused, their babies taken—operate with the town's silent complicity. Everyone knows. No one intervenes. The same Catholic institution that claims moral authority shows none, while Furlong remembers that it was Protestants who showed his mother mercy. Keegan uses coldness brilliantly throughout. The December setting, the frigid convent, the emotional coldness of willful ignorance—it all heightens Furlong's isolation. Those long walks home on winter nights, when he's haunted by the need to intervene, strip away all distractions. The external cold becomes unbearable because of the internal burning of his conscience. The Christmas timing is devastating. A season of mercy, yet the town practices none. When Furlong finally makes his choice—and it's a choice that will cost him everything—Keegan ends the story there. No neat resolution. No telling us whether his family stands with him, whether he loses his business, whether the town turns against him. The ambiguity isn't a weakness; it's the point. The choice itself is everything. Any conclusive ending would have diminished its power. By leaving it open, Keegan asks us directly: Could you make this choice? Could you bear this weight? ![Small Things Like These](https://upload.wikimedia.org/wikipedia/en/2/2a/Small_Things_Like_These.jpeg) *Small Things Like These* is a slim book with enormous emotional power. It's about the cost of staying silent, the courage required to act when everyone around you has chosen comfort over conscience. It's about how trauma echoes, how small acts of mercy ripple forward, and how sometimes doing the right thing means walking across that bridge alone into the cold. # A No‑Fuss Option Type for TypeScript Whenever I parse input, read configuration, or look up values in a map, I hit the same problem: sometimes a value is present, sometimes it isn't. JavaScript gives us `undefined`, `null`, and a long trail of defensive `if` checks. None of these carry any semantic meaning. In many languages, this problem has a well‑known solution: the `Option` type. It shows up in Rust, Swift, OCaml, Haskell, Scala—every ecosystem that wants a clean way to express absence without falling back to ad‑hoc conventions. I wanted a version of that pattern that fits naturally into TypeScript. Something small, intuitive, and aligned with the language's control‑flow analysis. That led me to this implementation of `Option`. ## A simple Option for TypeScript `Option` represents either: * a present value — `Some` * or explicit absence — `None` In TypeScript, it looks like this: ```ts interface Protocol { orElse(defaultValue: T): T } interface Some extends Protocol { readonly ok: true readonly value: T } interface None extends Protocol { readonly ok: false } export type Option = Some | None ``` The `ok` flag gives us precise narrowing: ```ts if (option.ok) { // option is Some console.log(option.value) } else { // option is None } ``` At runtime, both variants use a small internal class, but callers only see the plain union. This keeps the type simple without adding extra complexity. The only extra convenience shared between `Some` and `None` is a small `orElse` method for choosing a default value. It keeps the type practical without adding complexity. ## Design decisions ### 1. Discriminated unions as the primary interface The most important part of `Option` isn't a helper method, it's the discriminant: ```ts if (option.ok) { // happy path } else { // nothing here } ``` This plays directly into TypeScript's control‑flow analysis. There's no need for custom `isSome` or `isNone` helpers; the language already understands how to narrow on a boolean flag. This mirrors the design of my `Result` type: the smallest possible surface that lets TypeScript do most of the heavy lifting. ### 2. A singleton `None` `None` has no payload, so there's no reason to allocate a new object every time. I use a singleton: ```ts let NONE_INSTANCE: None | null = null function none(): None { return (NONE_INSTANCE ??= new OptionImpl(false) as None) } ``` This keeps things simple and avoids creating new objects when we don't need to. ### 3. A tiny protocol: `orElse` Every Option implements the `Protocol` interface: ```ts orElse(defaultValue: T): T ``` This gives you a clean way to fall back to a default value: ```ts const port = Option.from(env.PORT).orElse(3000) ``` It covers the common case where you either branch on `option.ok` or you want a safe fallback. No extra abstractions needed. ### 4. Small and intuitive API The goal is to keep `Option` small and easy to use. No extra abstractions or hidden behavior, just a clear way to express when a value exists and when it doesn't. This simplicity keeps the type practical and lightweight in everyday code. ## Factory functions The public API is intentionally small: * `Option.some(value)` * `Option.none()` * `Option.from(value)` The last one is particularly helpful when dealing with nullable values: ```ts const name = Option.from(env.USER_NAME) ``` `Option.from` treats `null` and `undefined` as absence. ## How it feels in practice ### Parsing ```ts function parsePort(raw: string): Option { const port = Number(raw) return Number.isInteger(port) ? Option.some(port) : Option.none() } const port = parsePort(raw).orElse(defaultPort) startServer(port) ``` ### Map lookups ```ts function get(map: Map, key: K): Option { return map.has(key) ? Option.some(map.get(key)!) : Option.none() } ``` ## Relationship to Result `Option` and `Result` naturally work well together. They are siblings. * `Option` — absence is *expected*. * `Result` — failure is *meaningful* and carries information. A useful pattern is: ```ts Result.from(option) ``` This turns `None` into a typed error, which is helpful in validation flows. The main point is simple: `Option` stays small. It avoids extra helpers and relies on TypeScript's narrowing to do the work. ## Implementation For a sample implementation of this Option type, see this gist: [e671d54a](https://gist.github.com/znck/36b61f9d4ae927e70cfe04d6e671d54a) # Designing a result type that feels native in TypeScript Over the years, I've worked in several TypeScript codebases that leaned heavily on fp-ts, Effect, and a few homegrown FP-inspired Result types. They all had good intentions, but they never felt like TypeScript. Each brought its own mental model, its own vocabulary, and its own way of composing functions. When you're deep in those systems, you start thinking in terms of `Either`, `Task`, `Effect`, `pipe`, and typeclass instances, not in terms of the language you're actually writing. Every time I switched projects, the error‑handling dialect changed. Some teams wrapped everything in `TaskEither`, others used custom monads, others mixed Promises and union types in creative ways. Reading the code felt like switching between programming languages inside TypeScript. I kept wondering: *why does error handling feel so foreign when the language already has clear ideas about control flow?* That question eventually led me toward something simpler, a Result type that fits directly into the language instead of sitting on top of it. This article is about how I arrived there, and the one idea that finally made the design snap into place. ## Why I wanted a result type Most TypeScript code I write falls into two categories: 1. **Synchronous logic** Parsing, validating, mapping, sanitizing, the parts of the system where failures are expected and should be surfaced explicitly. 2. **Asynchronous boundaries** Network calls, file IO, database access, areas where failures usually show up as thrown errors. TypeScript already gives us a built-in way to express *asynchronous* failure: ```ts const value = await something() // may throw ``` But synchronous failure is where things usually fall apart: * throwing errors without clear intent, * returning `undefined` without meaning, * shaping errors differently in every function. A `Result` helps make failures explicit. But many existing libraries bring extra cognitive load by introducing new models, new vocabulary, and new patterns that feel heavier than the problem itself. I wanted something that fits the language instead of working around it. ## The key insight: promises are already results This is the idea that suddenly made everything clear: **A `Promise` is already a Result.** It represents: ``` T | throws Error ``` Which is conceptually identical to: ``` Result ``` A function returning a Promise is a throwing function, and the caller must catch or propagate errors. This means we already have one built-in “Result monad,” whether we call it that or not. Once you see this, a natural split emerges: **Use `Result` for synchronous failure.** **Use `Promise` for asynchronous failure.** Instead of mixing the two worlds, it helped to look at them side by side: ```ts async function foo(): Promise { // may throw, caller handles with try/catch or await } function foo(): Result { // may fail, caller handles through the value } ``` Both forms communicate that the function can fail, but in different domains. Promises work well for asynchronous operations. Result works well for synchronous operations. Keeping these two paths clear turned out to be a useful guiding principle. ## A Result that works with the language At the type level, the shape is familiar: ```ts interface Success { readonly ok: true readonly value: T } interface Failure { readonly ok: false readonly error: E } type Result = Success | Failure ``` It integrates with both styles of code because it behaves like a Promise when awaited and like a simple value when used inside generators. This dual shape leads to a few helpful properties: ### 1. It's `PromiseLike` You can `await` a Result: ```ts const user = await Result.success({ id: 1 }) // ^? { id: number } ``` * A success resolves. * A failure rejects. This makes Result ergonomic in async code similar to Promise. ### 2. It's iterable via `yield*` A `Result` implements `Symbol.iterator`, which means you can use it inside generator pipelines: ```ts const result = Result.gen(function* () { const user = yield* findUser() const account = yield* findAccount(user.id) return Result.success({ user, account }) }) ``` The generator stops on the first failure and returns that Result. It's a synchronous equivalent of `async/await`, but without the boilerplate of `if (!ok) return failure`. ### 3. The API stays intentionally small * `Result.success` * `Result.failure` * `Result.all` * `Result.gen` No combinators, no transformers, no monad vocabulary. JavaScript's control flow and TypeScript's narrowing do most of the work. ## How It Feels to Use ### Synchronous code Here is a simple example of a function that returns a `Result` instead of throwing: ```ts function parseUser(json: string): Result { try { const data = JSON.parse(json) return Result.success(validateUser(data)) } catch { return Result.failure(new Error('Invalid JSON')) } } ``` This function can fail, but the failure is carried through the return value. The caller decides how to handle it. In synchronous code, you handle the error by checking the result: ```ts const result = parseUser(raw) if (!result.ok) { return respond(400, result.error.message) } useUser(result.value) ``` ### Asynchronous code In asynchronous code, failure is expressed through Promise rejection: ```ts async function loadUser(id: string): Promise { const res = await fetch(`/api/users/${id}`) if (!res.ok) throw new Error('User not found') return res.text() } ``` You can still combine async failure and sync `Result`-based failure cleanly inside the same function: ```ts async function handleRequest(id: string) { try { const raw = await loadUser(id) // Promise, may throw const parsed = await parseUser(raw) // Result, may throw return successResponse(parsed) // happy path } catch (error) { return errorResponse(error) } } ``` This shows the split: synchronous code returns `Result` and the caller inspects the value, asynchronous code returns `Promise` and the caller uses `try` and `catch`. Both styles fit naturally inside the same function without getting in each other's way. ## Sequential logic with `Result.gen` `Result.gen` lets you write readable, sequential workflows: ```ts const profile = Result.gen(function* () { const user = yield* parseUser(raw) const settings = yield* parseSettings(raw) return Result.success({ user, settings }) }) ``` It reads top-to-bottom, stops on the first failure, and keeps everything synchronous. It also mirrors how async code behaves with `await`, which makes synchronous and asynchronous error handling feel similar and easier to follow.  ## Aggregating errors with `Result.all` Sometimes you want to run multiple independent synchronous operations and collect everything: ```ts const result = Result.all([parseUser(rawUser), parseSettings(rawSettings), parsePreferences(rawPrefs)]) ``` If all succeed, you get a typed tuple. If anything fails, you get an `AggregateError` with all the failures. This makes validation pipelines cleaner and more honest. ## Why this approach feels natural in TypeScript What surprised me most is how little machinery I needed. * TypeScript's union narrowing does the heavy lifting. * JavaScript's `await` and exceptions already model async failure. * Generators provide a clean way to express synchronous pipelines. The Result type stayed small and clear while still being practical for real projects. It works because it builds directly on the language's own primitives. ## What I learned * Promises and Results express the same idea in different parts of the language. Use each where it fits. * Keeping failure paths separate makes code easier to follow. * Generators offer a clear way to write synchronous pipelines. * Small abstractions work best when they build on what the language already gives you. This Result type is not a new model for error handling. It simply makes the existing patterns clearer and easier to use. ## What's next This Result type is one part of a small set of utilities I've been building for my own projects, along with `Option`, a typed `Exception` system, and a lightweight JSON codec. Each of those pieces deserves its own write-up. ## Implementation For a sample implementation of this Result type, see this gist: [8a3259b7](https://gist.github.com/znck/24ea135632523d8ef06c805f8a3259b7) # A Tale of Two Monorepos I've been living in two parallel monorepo worlds. At work, I'm designing a setup for hundreds of contributors across dozens of teams. In my personal time, I'm building products with a few friends—often owning entire codebases myself. In both cases, I've been iterating constantly, borrowing lessons from one to inform the other. After three iterations of each, I've ended up with two completely different implementations. They share core principles—Single Version Policy, Package-First Philosophy, and Zero-Config defaults—but their designs reflect the shape of the teams behind them. ## Design Follows Team Shape Monorepo design isn't about picking the best tools or the most popular structure. It's about aligning the repo with your team's capabilities, goals, and constraints. When a team is made up of specialists, the system should make ownership and boundaries explicit. When a team thrives on shared context, the system should get out of the way. Each design responds to the human architecture that drives the code. That's why the same person, with the same principles, can arrive at very different implementations when the team shape changes. ## Monorepo 1: One Product, Many Teams At work, three forces shaped our monorepo design: capabilities, goals, and constraints. Our capabilities came from the depth of the engineering organization—hundreds of contributors with specialized skills but limited shared context. The constraint was scale: dozens of teams all contributing to a single, complex product. The goal was to ensure safe, composable collaboration. We needed a system that allowed contributors to own parts of the codebase confidently while making their work intentionally accessible to others. ## Monorepo 2: Many Products, One Team In this setup, the same three forces led to very different choices. The team had a high degree of shared context. Everyone could touch any part of the codebase without coordination. The constraint was ambition: we wanted to ship multiple products quickly, without fragmenting the team or the code. The goal was velocity—reusing as much as possible, avoiding friction, and staying close to the code. The result was a streamlined monorepo where flexibility and speed mattered more than boundaries. It wasn't designed for safety at scale, but for momentum in motion. ## Comparing the Designs The contrast between these two monorepos is best understood not through tooling choices alone, but through the logic behind each decision. Here's how they diverged: ### Monorepo 1 #### Package Structure In monorepo 1, APIs were explicit. Every export required a TSDoc comment with a `@public` tag, making it clear what was intended for public use. We used Rollup with `@microsoft/api-extractor` to strip away non-public exports, ensuring that only the intended APIs were exposed. The package structure looked like this: ``` packages/ ├── package-a/ │ ├── src/ │ │ ├── index.ts │ │ ├── index.spec.ts │ │ ├── testing.ts │ │ └── testing.spec.ts │ ├── dist/ │ │ ├── index.cjs │ │ ├── index.cjs.map │ │ ├── index.mjs │ │ ├── index.mjs.map │ │ ├── index.d.ts │ │ ├── index.api.json │ │ ├── checksum.txt │ │ ├── testing.mjs │ │ ├── testing.mjs.map │ │ └── testing.d.ts │ ├── report/ │ │ ├── index.api.md │ │ └── testing.api.md │ ├── package.json │ ├── readme.md │ └── tsconfig.json ``` The structure follows clear conventions: * `src/` contains the source code, including test files * `dist/` contains the built files * `report/` contains API information snapshots generated by `@microsoft/api-extractor` * `package.json` defines the package metadata, including Nx targets and code ownership #### Package: `package.json` The `exports` field defines entry points for the package. There may be multiple entry points, each with its own type definitions and module formats. The Nx tags (`package.json#nx.tags`) define what targets should be attached to the package: * `library` tag is an alias for `vitest`, `typecheck`, `rollup`, and `eslint` tags * `app` tag is an alias for `playwright`, `vitest`, `typecheck`, `vite` and `eslint` tags * `library` tag adds `release` target if `private` field in `package.json` is not set to `true` * `vitest` tag adds `build_vitest` target to run tests with Vitest * `typecheck` tag adds `build_typecheck` target to type check using `tsc` * `rollup` tag adds `build_rollup` target to build the package using Rollup. Rollup input options are inferred from the `exports` field * `eslint` tag adds `lint_eslint` target to lint the package using ESLint * `playwright` tag adds `test_playwright` target to run E2E tests using Playwright. It also creates atomic targets: `test_playwright_`, which are all composed in composite `test_playwright` target Additionally, composite targets `lint` combining `lint_*`, `build` combining `build_*`, and `test` combining `test_*` are added too. This enables a consistent developer experience across packages—just run `nx build` in any package. The `author` and `contributors` fields are used to create `CODEOWNERS` file for GitLab. A sample package.json looks like this: ```json file=packages/package-a/package.json { "name": "@myorg/package-a", "version": "1.0.0", "type": "module", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.cjs", "default": "./dist/index.mjs" }, "./testing": { "types": "./dist/testing.d.ts", "import": "./dist/testing.mjs", "default": "./dist/testing.mjs" }, "./package.json": "./package.json" }, "author": "@user.name", "contributors": ["@user.name", "@team-name"], "nx": { "tags": ["library"] } } ``` #### Entrypoint: `src/index.ts` and others The package entry points must have source files in the `src/` directory. * `.` or `@myorg/package-a` is the main entry point, built from `src/index.ts` * `./testing` or `@myorg/package-a/testing` is an additional entry point for testing utilities, built from `src/testing.ts` * `./` or `@myorg/package-a/` is another entry point, built from `src/.ts` We support only one level for entry points, so `@myorg/package-a/foo/bar` is discouraged. The `./testing` is reserved for testing utilities such as stubs and mocks to support downstream testing. The entry point file must have a package documentation comment: ```ts file=packages/package-a/src/index.ts /** * @packageDocumentation * One line description of the package. */ ``` > As per Package-First Philosophy, if you cannot describe everything a package does in one line, then you should probably split the package. #### Documentation We used [TSDoc](https://tsdoc.org) for documentation. For most packages, inline documentation is more than enough, but when needed, markdown files can be used. The `readme.md` file (if it exists) is used as the package landing page with API reference generated from API model (`dist/*.api.json`) files. Documentation website screenshot #### Dev Server Direct Linking We used Vite dev-server and bypassed building individual packages using `resolve.alias` config. Since we had a convention for entry point files and single flexible but shared configuration, we could directly link package imports to source files. ```ts file=vite.config.mts import { defineConfig } from 'vite' import * as path from 'node:path' const rootDir = import.meta.dirname export default defineConfig({ resolve: { alias: [ { find: /@myorg\/([^/]+)/, replacement: path.join(rootDir, 'packages/$1/src/index.ts') }, { find: /@myorg\/([^/]+)\/([^/]+)/, replacement: path.join(rootDir, 'packages/$1/src/$2.ts') }, ], }, }) ```
### Monorepo 2 #### Package Structure In monorepo 2, the design was more flexible and less constrained by explicit boundaries. The package structure was simpler, focusing on rapid development and iteration. The package structure looked like this: ``` packages/ ├── package-a/ │ ├── src/ │ │ ├── index.mts │ │ ├── index.spec.mts │ │ ├── testing.mts │ │ └── testing.spec.mts │ ├── package.json │ ├── readme.md │ └── tsconfig.json ``` The structure is defined by simplicity: * `src/` contains the source code, including test files * No need for `dist/` directory as the code is either executed directly on Node.js with type stripping or served through Vite * No separate API documentation generation; the focus is on inline documentation and simplicity #### Package: `package.json` ```json file=packages/package-a/package.json { "name": "@myorg/package-a", "version": "1.0.0", "type": "module", "exports": { ".": "./src/index.mts", "./testing": "./src/testing.mts", "./package.json": "./package.json" }, "nx": { "tags": ["library"] } } ``` We used a simplified `exports` field, pointing directly to the source files. This allowed us to skip the build step for development, relying on type stripping for execution. The `library` tag behaved similarly to monorepo 1, adding targets for testing, type checking, and linting. However, the targets were simpler and more focused on rapid iteration. #### Entrypoint: `src/index.mts` and others The conventions for entry points are similar to monorepo 1. #### Documentation Documentation was primarily inline, with a focus on simplicity. We used TSDoc comments for API documentation, but there was no separate API model generation. In the future, when we consider open sourcing some packages, inline documentation will be extracted to markdown files. #### Dev Server Direct Linking Similar to monorepo 1, we used Vite for development with the same direct source linking approach. ### Conclusion While they share philosophical roots—like Package-First, Single Version Policy, and Zero-Config approach—each monorepo is a distinct reflection of the environment it serves. A monorepo is not an architectural ideal. It's a reflection. It reflects how your team thinks, communicates, and collaborates. Your monorepo is an extension of your team's internal structure and shared habits. So before choosing tools or setting rules, start by asking: * What do we know today, and how is that knowledge distributed? * What do we each own—and what do we need to borrow? * How do we want to build, review, and release code together? When the answers to these questions are clear, the right structure tends to follow. ## Reflection Every monorepo is shaped by the team behind it. I've learned that there's no universal template—only trade-offs that make sense within a given context. Design follows people, not preferences. The best outcomes come when structure grows from how people work. # Zero Config Approach Every monorepo starts out clean. A few packages. A handful of configs. Nothing too hard to manage. But scale changes everything. Before long, you're drowning in boilerplate — config files that differ only slightly, scattered across every package. Changing one means changing a dozen. Forget one, and things break quietly. This is the **config drift** that plagues monorepos. We solved this by embracing conventions and tag-driven automation, making configuration invisible to contributors while giving maintainers full control over how the monorepo scales. ## The Problem with Repetition Our monorepo was littered with nearly identical configuration files: * `vite.config.ts` * `project.json` * `rollup.config.js` Most varied only in name or minor details, yet they appeared everywhere. Why is this a problem? * Changes become error-prone—you might miss files, especially across branches * It's difficult to distinguish between intentional and accidental differences * Creating new packages becomes tedious, requiring boilerplate copying and tweaking * Best practices evolve, but packages drift away from them over time This isn't just messy—it's fundamentally unsustainable. ## A Shift in Perspective Our zero config approach doesn't mean no configuration. It means **removing configuration from places it doesn't belong**. We asked ourselves: *Why have per-package configuration when most of it is the same?* By defining package behavior in one central place, we eliminated repetitive boilerplate. This approach delivers: * **No per-package configuration** * **No guesswork** * **No boilerplate** ## Convention Over Configuration We established predictable conventions to define shared configuration across packages: * Every package follows a `src/` and `dist/` directory structure * The entry point is always `src/index.ts` * Rollup builds are inferred from `package.json#exports` * Vite aliases the package name to its source, enabling fast development linking * API documentation is generated from JSDoc comments * Undocumented APIs are automatically excluded from builds * Tests are in `.spec.ts` file next to the source * Linting and formatting are consistent across all packages These weren't revolutionary ideas—just consistent decisions applied uniformly across the repo. We placed all shared configuration in the executors of our local Nx plugin. ## Behavior from Tags We took this a step further by using **Nx tags as behavioral triggers**. For example, a package with the `library` tag automatically receives: * `build`, `lint`, and `test` targets set by the plugin * No need for a `project.json`, just add the tag to `package.json` * If `"private": true` in `package.json` is not set, the plugin adds `publish` and `release` targets Tags express *intent*. The plugin translates that intent into actionable behavior by defining dynamic Nx targets. ## Frictionless for the Common Case If your package fits the common pattern, you get zero config. But what about special cases? Simply omit the tags and add your own configuration. Our guideline: > The **second** time a package needs the same configuration, it's a signal to pull it into the plugin. This approach allows conventions to emerge naturally from repeated patterns, giving us: * Flexibility for edge cases * Consistency for everything else * A clear path to scale without drift ## Centralize the Smarts Our local Nx plugin became the brain of the monorepo: * One place to define how targets behave * One mental model shared across the team * One set of defaults tailored to our specific needs Contributors focus on writing code. Maintainers focus on evolving the configurations. Everyone benefits from improvements with no extra effort. ## Reflection Monorepos don't fail because of scale. They fail because of inconsistency. By encoding decisions into the local plugin and expressing behavior through tags and conventions, we've stopped chasing config drift and started scaling with confidence. The zero config approach isn't about eliminating configuration—it's a **strategy for sustainable growth**. *** For a detailed technical breakdown of our implementation, check out [Brian Schiller's excellent write-up](https://brianschiller.com/blog/2025/06/04/inferred-nx-config/). # Reclaim Toolchain Ownership In my previous posts, I explored the benefits of building software with **small, focused packages** and maintaining **a single version of each dependency** across a monorepo. These practices bring structure to your codebase and simplify dependency management. But structure and consistency alone won’t take you far enough. To create a monorepo that scales you need something more: **ownership of your toolchain**. ## Defaults Aren’t Destiny Most modern tools come with sensible defaults, which is helpful. But general defaults are just that—**general**. They don’t account for your specific needs: the number of web apps you manage, how your team works, or the way you want your documentation to look. Over time, these default choices can create friction, for example: * **Vite** runs dev servers on port `5173` by default. Add a second app, and you face port conflicts or random port assignments. * **Storybook** assumes one instance per package, but a single global instance that aggregates all stories is often more practical. * **API Documenter** generates one file per type, which can be overwhelming for small packages. A single-page reference is easier to navigate. Individually, these issues aren't deal-breakers. But in a monorepo, every rough edge compounds, creating unnecessary complexity. ## What Toolchain Ownership Looks Like Owning your toolchain doesn't mean reinventing every tool. It means tailoring the toolchain to fit *your* project. Here's an example: * Assigning stable, unique ports to each app—so you can run multiple servers without guessing. * Centralizing Storybook into one instance—so contributors can access the full set of components in one place. * Generating API docs as single pages—aligned with your package philosophy. These adjustments may seem small, but they significantly improve the developer experience. And they're achievable because you control the tooling. For instance, we use **Nx** to manage our monorepo. Instead of duplicating configurations across packages, we have a **local Nx plugin**. This plugin acts as a central hub for: * Defining custom executors (e.g., for `vite`, `storybook`, or `api-documenter`). * Configuring generators and project scaffolding. * Managing consistent setups across all packages. This plugin provides **a programmable layer of control**, scoped to our repo but aligned with the Nx ecosystem. It's a single place to encode decisions, defaults, and custom logic. ## Customize, Don’t Fork Customization can feel risky, as if you’re straying from the beaten path and making upgrades harder. But consider this: > You upgrade a few times a year. You live with your tooling every day. Daily experience matters more than theoretical upgradability. That said, customization doesn't mean going off-road. Most tools offer public extension points—**plugins, custom configurations, composable APIs**. Use these to stay close to the ecosystem while tailoring it to your needs. Ownership doesn't mean doing everything yourself—it means being intentional about the defaults you keep and the ones you reshape. ## Avoiding Fragmentation Without shared tooling, packages start to drift: * One might use a slightly different `vite.config.ts`. * Another might override the linter in ways that break formatting. * A third might have a test setup that confuses new contributors. This fragmentation leads to inefficiency and frustration—death by a thousand configs. Toolchain ownership is the antidote. It provides **a single place to encode your opinions** and ensures consistent application across the repo. ## Reflection Reclaiming your toolchain requires a mindset shift. It means treating tooling not as a black box or a necessary evil, but as an integral part of your product. When you shape it with care—when you own it—the entire engineering experience improves: faster feedback, fewer surprises, better collaboration. # Single Version Policy A while back, we hit a wall that's all too familiar in monorepo life: dependency chaos. It was a classic case of "One team upgrades a library, another lags behind, and suddenly the same codebase runs three versions of React. Debugging becomes archaeology. Upgrades turn into whack-a-mole. Everyone feels the pain." We needed a new approach—one that would bring order, predictability, and speed. So we made a bold move: **one version to rule them all**. ## Why Single Version? If a dependency is shared, its version should be too. A Single Version Policy (SVP) means every package in the monorepo shares the same version of any given dependency. This approach unlocks powerful benefits for teams working together at scale: * **Consistency**: No more version drift or mysterious bugs from mismatched libraries * **Simplicity**: Upgrades become a single change, not a scavenger hunt * **Smaller builds**: No duplicate dependencies bloating your bundles * **Confidence**: Everyone knows exactly what's running in production The result? Fewer surprises, faster reviews, and a codebase that feels like a team effort—not a patchwork. ## How We Made It Work The key was making the right thing easy. We used pnpm's catalog feature to define all external dependency versions in one place: ```yaml file=pnpm-workspace.yaml catalog: react: 18.2.0 typescript: ^5.0.0 @types/node: ^20.0.0 ``` Inside each package, dependencies point to the catalog: ```json file=package.json "dependencies": { "react": "catalog:" } ``` Adding a new dependency? Run `pnpm add` from the package directory, then standardize it with a codemod (`pnpm dlx codemod@0.11 pnpm/catalog`). Internal packages use the `workspace:` protocol, keeping everything in sync: ```json file=package.json "dependencies": { "@your-org/utils": "workspace:^" } ``` External = `catalog:`\ Internal = `workspace:^` No guesswork. No drift. ## Guardrails: Linting for Safety A policy is only as strong as its enforcement. We wrote a custom ESLint rule to check every `package.json`: * All external dependencies must use `catalog:` * All internal dependencies must use `workspace:` CI enforces it. If someone tries to sneak in a pinned version, the build fails. It's not glamorous, but it's essential. ## Tradeoffs and Lessons SVP isn't magic; it means recurring coordinated upgrades—sometimes you need to fix several packages at once. But it's a single, visible change, not a slow drift. And, the investment up front pays off in long-term sanity. For rare edge cases, we allow a second named catalog (like `next-react`), but only as an exception. SVP isn't just a rule—it's a foundation. It gives the team speed, safety, and focus. We wouldn't go back. # Before the Coffee Gets Cold You sit on a chair in a small basement café, you can go back or forward in time, but only until your coffee gets cold. The rules are strict: you can't change the past, you cannot get up from your seat, and you can only interact with people who are also in the café at that moment. At first glance, I thought what is the point of such a time travel experience? What surprised me was how those constraints made the stories more powerful. Each chapter is a quiet exploration of longing, regret, and unresolved feelings. The time travel becomes a device not for fixing the past, but for understanding it better.
![Before the Coffee Gets Cold](https://upload.wikimedia.org/wikipedia/en/9/9c/Before_the_Coffee_Gets_Cold_book_cover.webp) > *Before the Coffee Gets Cold* by Toshikazu Kawaguchi It’s a gentle, meditative book. The kind you read slowly. The kind that lingers. If you're looking for something tender and imaginative, Before the Coffee Gets Cold is worth sitting with—ideally while your own cup is still warm.
Would you go back in time if you cannot change the past? # Package-First Philosophy A year ago, we were facing a common challenge: a sprawling codebase where everything was interconnected. Fixing one issue often broke something else. Refactors felt like high-stakes operations. Code reviews were slow and confusing because no one was clear on ownership. It was a familiar problem, and it was draining. We decided to change that—not just with better tooling, but by reshaping the way we thought about code. So we made a simple bet: **make packages the center of the universe**. ## Why Package-First? If something deserves a name, it deserves a package. In our world, a package is a unit of structure, ownership, and contribution. It owns its code, tests, documentation, and public interface. It’s a contract—not just to the system, but to the team maintaining it. This model provides several benefits: * **Isolation**: Changes in one package stay within that package. * **Ownership**: Contributors are responsible for specific, focused units. * **Autonomy**: You don’t need to coordinate across teams to ship new features. * **Composability**: Small components can be combined to create larger systems. * **Focus**: The smaller the surface area, the easier it is to reason about. The side effect? Simpler code, fewer arguments, faster reviews, and clearer responsibilities. ## Habits, Triggers, and Code While reading *The Power of Habit* by Charles Duhigg, one idea stood out: habits are built on cues, routines, and rewards. Change the cue, and the routine often follows. That resonated with our experience. Once we made it easy to create a new package—just a few keystrokes and a scaffolded module was ready—developers started defaulting to it. The friction was gone. The cue shifted: instead of asking "where should I add this code?", the new instinct became "should I create a new package for this?" The result? A new habit. Modular thinking became automatic. Developers started creating focused packages, with clear interfaces and isolated tests. The reward was immediate: fewer merge conflicts, faster reviews, and a deeper sense of ownership. As Duhigg puts it, once a habit loop is established, it reinforces itself. The package-first mindset became that loop. ## Enabling Package-First Development Making package creation easy was a game-changer. Teams stopped overloading existing modules and started creating new packages instead. The monolith didn’t shrink, but it became much more understandable. We used Nx and pnpm to implement this, but the tools were just the means to an end. Here’s what we focused on: * **Nx generators**: These scaffold fully functional packages for known types—`utility`, `tool`, `feature`, or `app`. Each package comes with build, test, lint, and documentation preconfigured. * **Ready-to-use packages**: No extra setup required. Just start writing code. * **Explicit public interfaces**: All public interfaces are clearly marked with `@public`, and we enforce this through tooling. As friction disappeared, hesitation followed suit. Developers iterated faster, experimented more, and by default, wrote more modular code. ## Metadata is Ownership Every package clearly defines its owner, where to file bugs, and what it does. But the real power is in the metadata: the `package.json` often tells you more than the code itself. It communicates intent, capabilities, and responsibilities—all without opening a single `.ts` file. Here’s how it works: * **Ownership metadata** drives the routing of code reviews, ensuring that the right people are involved. * **Capability metadata** enables feature discovery and helps developers understand what each package is responsible for. * **Dependency metadata** powers impact analysis, letting you know how changes will affect other parts of the codebase. This isn’t just documentation—it’s operational infrastructure. By treating packages as self-describing units, we gain confidence across the board, and this structure supports automated tooling and decision-making. ## Scale by Design When everything is a package: * Teams onboard faster because they understand exactly where to find and contribute to code. * Features become portable, easily shared and reused. * Applications become compositions rather than monolithic systems. * Dependencies stay local, reducing complexity and risk. It’s not magic—it’s just structure. And structure scales. In the first six months, we saw over 80 new packages created, many by teams who had never worked in this repo before. That wasn’t just a win for tooling—it was a shift in how people thought about software: small, manageable units, clear contracts, and fast feedback. As we leaned further into the package-first model, it reinforced itself. Better structure led to better habits. Better habits led to better software. # Negative feedback comes from a good place It's not easy to say negative things to people. In fact, it takes a lot of courage to point out the shortcoming. If someone does that for me, honest and brutal, I keep them close. Brutal and honest friends are dearest to me. ## Feedback is hard We used to have 360-degree feedbacks at Myntra (I mean they still have, I am no more with them but I will again use "we"). I used to hate those and often treated them a waste of time. When I look back today, I hated it because I couldn't write about people and their work. I couldn't write because it's challenging to recall what they did this year and how could I remember their work when I was having a hard time remembering mine. What did I do this year? I have been working on this component for the past few weeks. And oh! I shipped that feature before that. And... and... what the hell did I do? And you're asking me feedback on your work? I used to face the same empty wall when I had to ask for feedback. We were living in now with no recollection of past, very little recollection. This wasn't working for me, and I guess neither for people working with me, I feel they even didn't know that we weren't a good team. The first 360-feedback was a disaster, I didn't write anything, about me or my work or them or their work, nothing at all. My manager called for a 1-on-1, "Why didn't you fill your self-review?". I didn't tell him that I didn't remember, "Do we really need to? I mean, I working well, you know that. Isn't it a waste of time?". That day I got to know that 360-feedbacks are a precursor to the forthcoming calibration meeting, unknown to me, these corporate shenanigans, I thought. The calibration meeting was a hidden championship where every employee was compared with every other in the same role. Hidden because you don't represent yourself; instead, your manager represents you. "But, you know what I have worked on", it struck me, shit—what the hell would he remember about my work when I couldn't. Maybe subconsciously I thought he's oracle or something, he would know, I can count on him, go be my champion and fetch me a promotion. But, it doesn't work like that, and I am lucky I got to know this very early in my career. ## It takes courage I started writing all significant tasks I was working on or peers who interacted with me. It was all one big text note, it had all I needed, what I worked on, what they worked on if found something wrong in their work or their behaviour, I did keep a note of everything necessary. It helped me in 1-on-1s to set my goals, to steer myself into a kind of position I wanted. Today when I look back, I feel I didn't do justice to my peers. I didn't tell them in time, I waited for the 360-feedback because I had content, I could write this time, and I did write. But that feedback was useless, it was late by 6 months, they couldn't do anything then, even if they eagerly wanted to do something. I set them up for failure. I did. But what else could I do, I didn't know you're supposed to tell people, nobody told me. Nobody asked me for feedback, not before the D-day. And I didn't dare to tell them unasked for. It takes courage to give feedback, even more, when unasked for. ## We react poorly to negative feedback If someone points out your mistakes or failure, it's natural that you would get in a defensive position. We try to divert, come up with excuses—"I wanted to write test cases but you know the deadline was so harsh, I could barely complete the task itself.", guess what the task is still incomplete and whatever you say to in defence of negative feedback, it's always what it is—a defence, an excuse, a diversion. Acceptance is a good response, a question is even better, in fact, is the best response—"I feel that's true, I think I have underestimated the task, how could I have done this better?". Ask questions, get more information, get actionable feedback. And keep telling your head, she is trying to help me, be rational, be reasonable, open to suggestions and don't shut her off, listen, listen, listen and ask. ## Not taking personally is a superpower "That's not a good way to do it, it could turn into some serious performance issue". We attach ourselves to our work so much that we tend to loose where do we end and where does our work start. An objective comment on a piece of code can become an attack on my code suddenly. My code, my precious. Keeping oneself detached from work is a superpower, and no one is born with it, neither can one be bitten by a radioactive spider to gain it. You have to practice, practice in the face of every criticism, every negative feedback that listens, listen, listen and ask. She is talking about this code and not me, there could be some merit in her suggestion, I should stay open to ideas. "I thought due to these constraints, we have a reasonable upper bound, maybe I missed something, why do you think it could run into performance issues?" ## Feedback Driven Development We all want to be better, we aspire to, and yet we never ask. Feedback is the fuel for personal development and you need to ask for it, for you to grow, to be better. I have learned something about using feedback as a fuel for personal development, partly from my experiences and partly from "The Making of a Manager" by Julie Zhou. I call it FDD, F stands for both—fuckup and feedback, both are necessary for one's growth. One makes mistakes to be better but needs feedback to learn from them. ### Ask for feedback You won't get anything unless you ask—promotion, relationship, and even feedback, so ask, ask and ask. Who do you ask? Ask your manager, ask your peers, ask your friends, ask your partner, ask your parents, ask that person who listened you talk, ask that person who read your article, ask that maintainer who reviewed your pull request, ask that person who depends on your work or your work depends on, ask everyone who you can, there is not a thing like too much feedback, ask as much you can. What do you ask? Ask what you want to be better at, ask something that you can quantify, ask something that demands honesty, ask something that opens up a dialogue. For example, I am learning to write, I want to be better at it, so I would ask "Does the content feel engaging?", "I want to share my learnings, but I don't want to appear authoritative, do you feel the content welcoming and honest?" "Is the flow of the good? Do you feel disconnected or losing thought while reading?" "Do you see any words or phrases that don't sound well in your culture?" "What did you like most? And what did you not like?" "What do you feel is working great and want to see more?" I mean ask a question that advances you towards your definition of better. ### Appreciate the feedback Thank them who give you feedback, appreciate their efforts. Giving feedback is not easy so let them know you are grateful, and it is helping you. Thank them even if they can't provide you with feedback, they are not bound to, and they can always ignore. They give you feedback or not, but they should know that you respect their time, you're thankful, and their feedback means a lot to you. ### Act on feedback All feedback is worthless, a waste of time if you don't act on it. I give you some feedback, but I see you doing actions against it like you haven't even heard what I said. I would feel like I am wasting my time and eventually I would stop engaging, you would like it too. Don't let the wellwishers die off, they want you to better, they want you to grow, they shouldn't feel irrelevant or unheard. ### Report back When you act on some feedback and find that it is actually making you better, don't miss the chance to report back, they went out of the line to help, you owe it to them, you report them your improvements. I would love to hear if something I suggested is working for you, it's feedback on feedback, it would help me to be a better help for someone else. What if it didn't work for you? nothing changes, you still report, I know it's hard to share failures, but failures need to be heard, failures make us better. We hear way less about failures even though they are more common, everyone fails at something or sometimes. If my suggestions do not work well for you, I expect to hear back, I don't want to set other people on the wrong path, I want to hear I failed, I want to learn, don't take away my failures. What if you already know it won't work because you've tried or heard about it? You still report back, if someone is courageous enough to give you feedback, they surely can handle feedback, or I'll assume they can. Do not hide failures and do share successes, we need to hear them all. ### Ask again The feedback worked for you, and it worked for me. We are our better selves, now what? Now we repeat, the best thing about "better" is that there is always a "better" better than "better". So, keep asking for feedback. Following is a recent email, I sent for seeking feedback, I hope you find it useful: > Hey ~~undefined~~, > > We have been working together for three weeks now. I value your feedback, and I'd like to be a more effective team member. Would you be willing to answer the questions below? Please be as honest as you can because that's what will help me the most—I promise nothing you say will offend me. Feedback is a gift, and I'm grateful for your taking the time. > > I spent my first week to understand the project and it's source code, and for the last two weeks, I have been working on InputDate component. How do you think I'm doing on that? Any suggestions on how I can do better here? > > Initially I estimated to complete the InputDate component in two weeks but while working on it, I found it builds upon other components like button, dropdown, popper, select box etc. which warranted building those base components first. What do you think I could have done better here? > > What am I doing well that you'd like to see me do more of? What should I stop doing? Any other suggestions? > > Thanks. > > \-- > Rahul Kadyan > ## Feedback is a gift I appreciate your time, I hope you enjoyed this and would love to hear some feedback from you. So, write to me if you could. I would leave you with a quote from **The Making of a Manager**: > "Feedback is a gift." It costs time and effort to share, but when we have it, we're better off. So let's give it generously. I recommend reading [The Making of a Manager](https://amzn.to/2MGVAx7) or at least the fourth chapter—The Art of Feedback. # Shipping Web Components with Vue I have been watching the web component spec. However, all the projects I have worked on were either using Vue or React, and these frameworks are more than capable of building complex applications. I never felt like using web components, but that changed yesterday. I was reading an article, and as usual, the reader mode on safari was turned on. There was an embedded tweet in the article, and it preserved the twitter styling even in the reading mode. It was surprising to me as I couldn't make the code snippets on my blog to maintain syntax highlights. I wondered what trickery was going behind the embedded — surprisingly, the magic was just a web component. I decided to write a web component for code snippets on my blog. ## Web Component Spec Web component spec is a meta-specification which depends on four other specs. * The Custom Elements specification * The shadow DOM specification * The HTML Template specification * The ES module specification The web components are part of the native web platform, and they allow us to write genuinely reusable elements. But, I miss the convenience of a Vue SFC. I miss computed properties and declarative event handler registration. Won't it be nice if we could write Vue components and ship web components? ## Shipping Web Components with Vue CLI Vue CLI supports exporting Vue components to web components. Let's take an example Vue component.
A sample Vue component
We have to create a wrapper to export this Vue component as a web component.
An exmaple of web component wrapper utility for Vue component
And using Vue CLI, we can export a web component.
```bash vue build -t wc -n my-component my-component-wrapper.js ```
Vue CLI command to export web component
The exported web component works like a charm, but the code outputted is obscure.
```js file=my-component.cli.js /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ // >>> redacted 900 lines // CONCATENATED MODULE: ./my-component.vue function injectStyles (context) { var style0 = __webpack_require__("0079") if (style0.__inject__) style0.__inject__(context) } /* normalize component */ var component = componentNormalizer_normalizeComponent( my_componentvue_type_script_lang_js_, render, staticRenderFns, false, injectStyles, null, null ,true ) /* harmony default export */ var my_component = (component.exports); // CONCATENATED MODULE: ./my-component-wrapper.js?shadow window.customElements.define('my-component', dist_vue_wc_wrapper(external_Vue_default.a, my_component)); // CONCATENATED MODULE: /usr/local/lib/node_modules/@vue/cli-service-global/node_modules/@vue/cli-service/lib/commands/build/entry-wc.js /***/ }) /******/ }); ```
Web component export of the example component using Vue CLI
## Shipping Web Components with Rollup We would use the same example component and export web component using rollup.
```js file=rollup.config.js import vue from 'rollup-plugin-vue' export default { input: './my-component-wrapper.js', output: { format: 'esm', file: './my-component.esm.js', sourcemap: false, }, plugins: [vue({ isWebComponent: true, template: { isProduction: true } })], external: ['vue', 'vue-runtime-helpers', '@vue/web-component-wrapper'], } ```
Rollup config to build Vue component as web component
And we get a much readable output source for the exported web component.
```js file=my-component.esm.js import Vue from 'vue' import wrap from '@vue/web-component-wrapper' import { normalizeComponent, createInjectorShadow } from 'vue-runtime-helpers' var script = { props: { name: String, }, } /* script */ const __vue_script__ = script /* template */ var __vue_render__ = function () { var _vm = this var _h = _vm.$createElement var _c = _vm._self._c || _h return _c('h1', { staticClass: 'hello' }, [_vm._v('Hello ' + _vm._s(_vm.name))]) } var __vue_staticRenderFns__ = [] /* style */ const __vue_inject_styles__ = function (inject) { if (!inject) return inject('data-v-facfc1de_0', { source: '.hello{color:#00f}', map: undefined, media: undefined }) } /* scoped */ const __vue_scope_id__ = undefined /* module identifier */ const __vue_module_identifier__ = undefined /* functional template */ const __vue_is_functional_template__ = false var MyComponent = normalizeComponent( { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }, __vue_inject_styles__, __vue_script__, __vue_scope_id__, __vue_is_functional_template__, __vue_module_identifier__, true, undefined, undefined, createInjectorShadow, ) window.customElements.define('my-component', wrap(Vue, MyComponent)) ```
Web component export of the example component using rollup-plugin-vue
The web component exported by the above Rollup config is an ES module, and you have to bundle it in your application. However, we can also ship browser compatible web components using Rollup. ### Web Component export as UMD package The UMD output format is directly executable in the browser, and we have to make small tweaks to above Rollup config to generate a UMD build.
```js file=rollup.config.js import vue from 'rollup-plugin-vue' import resolve from 'rollup-plugin-node-resolve' export default { input: './my-component-wrapper.js', output: { format: 'umd', file: './my-component.umd.js', sourcemap: false, globals: { vue: 'Vue' }, }, plugins: [vue({ isWebComponent: true, template: { isProduction: true } }), resolve()], external: ['vue'], } ```
Rollup config to build Vue component as web component in UMD format
And you can use this web component directly in HTML.
```html my-component demo ```
Example code to consume the above exported web component
> **NOTE:** > > The web components exported by Vue CLI or RollupPluginVue would require Vue as a dependency. > If you are planning to use the above-built web component in a React or Angular application, you have to ship Vue runtime too. # Simple Hard Things Simple things are easy; everyone says so. However, in reality, it may not be so. Have you ever met someone who likes complicated things? Well, I haven't! I'm pretty sure; everyone wants simple. We create simple interfaces, write simple, and ship simple products. I like simple things, as they are easy for most of us, in most situations. However, simple is not guaranteed to be easy. I want to share a simple hard thing, but before that tell me, "do you like the design of this blog?". I know at least one person likes it as Egoist says, "Your blog looks clean." > Your blog looks clean, mind me making it a [@saber\_land](https://twitter.com/saber_land) theme? 😬 > — [@\_egoistlily](https://twitter.com/_egoistlily/status/1132564698677690368) Clean or say it looks simple. Ask me, was it simple building it? The core idea here is simple and straightforward. I wanted to create a website with a grid in the background. All the contents i.e., text, paragraphs, headings, images, code snippets, etc. should align with the background grid. However, the implementation of this simple idea is quite hard. I started with the background. I need a repeating grid pattern for the background image, so I prayed the [SVG goddess](https://twitter.com/sarah_edo), and she blessed me with knowledge to write following snippet:
```svg ```
SVG image used to draw background grid pattern.
Next, I have to put some text on the page, and since everything should align with the grid, I'm restricted to use sizes and heights which are multiple of eight. I set the base `font-size` of the page to `16px` and use relative units for sizes and heights. [Suwardhan](https://suwardhan.com) has crunched an elegant type system for me, "The title of should use a 42px font.". However, 42 is not a multiple of eight. It doesn't align with my background grid, and I don't want to mess up with his design. What should I do? I can use line-height to fit the height constraint. A line-height of `3.5rem` or `56px` works well here. If the title spans over two lines, then the height of the title block would be two times the line-height (i.e. `112px`). However, the height of the title block does not ensure that the baseline of the text lies on the grid. ![Text baseline mis-aligned with the background grid.](../../_assets/mis-aligned-DUzkIacu.png) I use top padding of 5px to align the text baseline, and since the line-height is multiple of eight, all lines of text magically align with the background grid. ![Text baseline aligned with the background grid.](../../_assets/partially-aligned-BMJFz83W.png) However, the height of the title block is `117px` which breaks the height constraint and if one element breaks the constraint the effect cascades. I use the smallest bottom padding to make the height a multiple of eight. Here, it would be 3px (117px + 3px = 120px). The bottom padding complements top padding to become a multiple of eight i.e. 5px + 3px = 8px. ![Text baseline and height aligned with the background grid.](../../_assets/aligned-D_cSu0al.png) I got a pattern here: every text block would have a line-height which is multiple of eight, then top padding (which would always be less than 8px) to align text baseline and finally a complementing bottom padding. So I apply this pattern to all the text elements. Other elements like images and code snippets needed similar fine adjustments, and finally, I had a page with complete text and background alignment. "Noto Serif looks better than Helvetica, replace the primary font." - Suwardhan 🤦‍♂️ The two fonts have different baseline heights, so I have to adjust the top-paddings again. Readjusting text in dozens of places is a daunting task, and it would be even harder in the future. I decide to bring in atomic design pattern and use [accoutrement](https://www.oddbird.net/accoutrement/) for token management. So, all my adjustments are in one file:
```scss $sizes: ( 'unit': 8px, 'base': '#unit' ( 'times': 2, ), 'root': '#base', // Used to calculate rem from px. 'line-height-base': '#base' ( 'times': 1.5, ), 'font-size-ui': '#base' ( 'times': 0.75, 'convert-units': 'rem', ), 'line-height-ui': '#base' ( 'times': 1, 'convert-units': 'rem', ), 'vh-adjust-ui': '#unit' ( 'times': 4/8, ), 'font-size-h1': '#base' ( 'times': 2.625, 'convert-units': 'rem', ), 'line-height-h1': '#base' ( 'times': 3.5, 'convert-units': 'rem', ), 'vh-adjust-h1': '#unit' ( 'times': 5/8, ), // ... ); ```
Typesetting for znck.dev ([source](https://github.com/znck/znck.dev))
It is hard to implement this simple idea for me so the simple hard thing. *It is hard to provide the simple.* Whenever you feel, it is simple, and I can easily get it. **Thank the creator**; she has done the hard work. # Finally some colors in my life "I got a red shirt!", I scream, every time my friends jab that my wardrobe is grey and dull. They always call me out, "Get some colors, dude!". Colors, huh! ## Colors I like grey; it's my absolute favorite. Grey is a color. I see grey as `#808080`, call me a nerd, but I do see `#808080`. `#808080` is how we tell computers that I want grey. The number `#808080`, we call it hex code (short for hexadecimal code) of a color. One important thing to note here, this 80 in hex code is not the number eighty we learned in the math class. Why it's like that? Let us ignore why, for a while, and we revisit the elementary math class first. We have ten digits: 0, 1, 2, 3, 4, 5, 6, 7, 8, and 9. We use these digits to make numbers, and we get a system to make numbers. We are using ten digits in this number system, so it is a base10 number system, also known as the decimal number system. The 80 in the hex code is 128 in the decimal number system (let's ignore how for a while). I see the number 128 as a hundred, a twenty, and an eight. We can break it further, a hundred, two tens and an eight. $$ 1 \times 100 + 2 \times 10 + 8 \times 1 = 128 $$ We know hundred is ten times ten. $$ 1 \times (10 \times (10 \times 1)) + 2 \times (10 \times 1) + 8 \times 1 = 128 $$ As we move from right to left, the value of a position is increasing ten folds. It is increasing ten folds because we have ten digits or say it is a base10 number. We have a smarter way to represent these ten folds increase: power notation. In power notation, we say: $$ 1 = 10^0 $$ $$ 10 = 10^1 $$ $$ 100 = 10^2 $$ Let's write 128 with power notation. $$ 1 \times 10^2 + 2 \times 10^1 + 8 \times 10^0 = 128 $$ For brevity, we write these numbers in increasing order of power. $$ 8 \times 10^0 + 2 \times 10^1 + 1 \times 10^2 = 128 $$ Phew! However, we got one little problem computers do not understand decimal numbers. They use base2 number system (also called binary number system) which means only two digits: 0 and 1. The decimal number 128 is 1000 0000 in the binary number system. Similar to decimal number above, we can write 1000 0000 in power notation: $$ 0 \times 2^0 + 0 \times 2^1 + 0 \times 2^2 + 0 \times 2^3 + 0 \times 2^4 + 0 \times 2^5 + 0 \times 2^6 + 1 \times 2^7 = 128 $$ The grey color (hex code `#808080`) would be 1000 0000 1000 0000 1000 0000, and computers would understand it. However, we, humans, would have a tough time using binary numbers. Just imagine, if my designer friend asks "What is the color of the button?" and I say, "It looks like one zero zero zero zero zero zero zero zero one zero zero zero zero ...". It is pretty damn exhausting. Moreover, I am not sure if anyone can comprehend these kinds of numbers. We need a better system to convey these numbers, like the number in the hex code of the grey color. The hex code `#808080` is a hexadecimal number (a base16 number system). The hexadecimal number 80 in power notation would be: $$ 0 \times 16^0 + 8 \times 16^1 = 128 $$ This number is relatively more comfortable to use as I can reply to my designer friend, "It looks like hex eighty eighty eighty." Why do we need three eighties for one color? Yay! I can talk about Color Theory. I feel like a designer now. Screens use lights to display colors. However, we don't need lights in all colors. A small set of colored lights can be mixed to create various colors. One such set is lights of red, green and blue color. So the three eighties in the hex code of grey are amounts of red, green, and blue colors, and it is also the reason that hex code is also called RGB code. Hence, grey has some red, some green, and some blue which makes me think grey is full of colors, then why does it look so... grey? ![White light passing through prism](../../_assets/prism-Da-XBeii.png) Colored lights mix additively or subtractively to give a resultant color. In our case, equal amounts of red, green, and blue cancel each other out, resulting into grey color. I cannot mix these colored lights in my head and write the correct hex code for the color I want to use. I need some other way to tell colors. Fortunately, there is a way: a color notation called HSL, which stands for hue, saturation, and lightness. Scary, huh! It seemed very complicated to me and chose to stick to color pickers until I saw [Miriam's talk at VueConf US 2019](https://youtu.be/VsG5l5e44ZY). She simplified HSL for me: * Hue is the color. * Saturation is the amount of color. * Lightness is the amount of light. Imagine if we paint a rainbow on a circular disc such that every radial line is in a different color, gradually changing from red to yellow, yellow to green, green to cyan, cyan to blue, and blue to red. On such a circle, the angle between any radial line with the red radial can be used to choose a color. We have red at 0deg, yellow at 60deg, green at 120deg, cyan at 180deg, blue at 240deg, again red at 360deg, and every other angle in between these would give a color which is a mixture of the two. Now, I know what could be the hue of the color I want. ![A disk demonstrating hues of HSL](../../_assets/hsl-CYBNVHMg.png) After picking hue, I choose the amount of the color, i.e., the saturation. It goes no color to full color, and we use percentage to set the amount required. Finally, we pick a lightness to set the brightness of the color. It goes from dark black to bright white and similar to saturation; we use a percentage value. Let's pick the grey color: * Hue does not matter as it's grey. * We don't want any color, so we set it to 0% * The correct lightness value would be around 50%, in between black and white. Hence, the grey color in HSL would be `hsl(0deg, 0%, 50.25%)` which is equivalent to `#808080`. Now I know how to make colors. I am adding colors to my blog. ## The dark theme I have been redesigning my blog for a while. With some help from [Suwardhan](https://suwardhan.com), I finally got the new design out. ![Screenshot: Default Theme on znck.dev](../../_assets/default-CK3a9_P8.png) It looks clean and smooth, and I love it. However, the brightness hurt my eyes. I am more of a dark mode person. I have to build a dark theme for me, and it is, precisely the opportunity I have been looking to throw in lots of colors. In the default theme, I have a light yellowish background, black text, and bright orange highlights. So, I need three colors for the dark mode too: a text color, a background color, and a highlight color. I choose white for text to get maximum contrast as the background would be dark. For background, I need some color with very low lightness. I pick: `hsl(0deg, 100%, 3%)`. I notice that text contrast remains the same, no matter what is the hue, which allows me to throw in lots of colors. I wonder, if somehow I can pass a random hue value to the background color, I can get as many colors as I want. ## The color of the day "Wonder! every day there's a new color.", I want it, I want it, I want it. How do I get a new hue every day? Can I use date? I think I can. There are 365 days in a year, and 360 degrees of hues (hue is circular so 361deg is equal to 1deg), I can use the day number as the value of the hue. I call it the hue of the day. So, the background becomes `hsl(var(--hue-of-the-day), 100%, 3%)`. I need one more color for highlights. I choose to use the contrasting hue of the day. It is amazing how easy it is to get contrasting color in HSL, subtract the hue value from 180deg. So, I have white for text, `hsl(var(--hue-of-the-day), 100%, 3%)` for background, and `hsl(calc(180deg - var(--hue-of-the-day)), 100%, 60%)` for highlights. ![Screenshot: Dark Theme on znck.dev](../../_assets/dark-YiuBcyfb.png) I use the following JavaScript snippet to set the hue of the day: ```js function hue() { const now = new Date() const start = new Date(now.getFullYear(), 0, 0) const diff = now - start const oneDay = 1000 * 60 * 60 * 24 const day = Math.floor(diff / oneDay) return day + 'deg' } document.body.style['--hue-of-the-day'] = hue() ``` That's all folks. I have a dark mode full of colors. Now, I got 723 colors: 360 backgrounds and 360 text colors of the dark theme, the background color of the light theme, the text color of the light theme, and "I got a red shirt!". # Grammarly in Code There's no simpler writing tool than markdown. There's no elegant editor than [code](https://code.visualstudio.com). There's no better writing environment than markdown in code. It's perfect, but whenever I write, I find myself copy-pasting back and forth between Grammarly editor and VS Code editor. Grammarly makes my writing much better, but it also forces me to use their editor. Don't take me wrong I don't hate their editor, in fact, I kind of like it — clean, spacious, and responsive. But it's not sufficient for me as my articles generally have images, code snippets, figures, and sometimes markup. The lack of syntax highlight for markdown and markup is an absolute deal-breaker. If Grammarly worked in VS Code editor, wouldn't that be perfect? I searched the code marketplace for a Grammarly extension, but there wasn't one. So, I continued jumping between Grammarly and Code, waiting for someone to remedy my pain. A year passed, but nobody came up with a solution. I couldn't bear it anymore; I had to help me; I had to build the integration myself. And, the journey of getting Grammarly in Code began. ## Hunt for Grammarly API "Grammarly API" — I entered in the Google white box and hit enter; the results disappointed me. > No API--yet! We'll keep you posted! > — [@Grammarly](https://twitter.com/Grammarly/status/388621218586578944) No API -- yet! It's been six years since they first acknowledged that they don't have a public API. I don't think developer environments ever make to their priority list. Hence, "Grammarly API Github" — I hit Google again. ![Screenshot of Google search results for query "Grammarly API Github"](../../_assets/screenshot-google-search-2-Dto8dABI.png) I found something — a reverse-engineered [Grammarly client](https://github.com/stewartmcgown/grammarly-api). It's a sound library, and I decided to base my extension on this library. ## Building the Extension I wanted to validate the idea with minimum efforts, so I looked for a project scaffold to kickstart the extension. VS Code has [yeoman](https://yeoman.io) generator. Hence, all I needed was to run the command `yo code`. ![Screenshot of Yeoman Visual Studio Code Generator running in iTerm2 zsh shell](../../_assets/screenshot-yeoman-CnmZr87_.png) I picked the very first option: "New Extension (TypeScript)", and I got a good starting point. Next, I needed to create a small language server to analyze text content with Grammarly API and post grammar diagnostics. The Code's [Language Server Extension Guide](https://code.visualstudio.com/api/language-extensions/language-server-extension-guide) proved to be an excellent resource. I got the extension working in very few lines of code.
```ts import { createConnection, TextDocuments } from 'vscode-languageserver' import { Grammarly } from '@stewartmcgown/grammarly-api' // Create a connection for the server. const connection = createConnection() // Create a simple text document manager. const documents: TextDocuments = new TextDocuments() connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind, }, })) documents.onDidChangeContent(async ({ document }) => { const results = await new Grammarly().analyze(document.getText()) const diagnostics = results.alerts.map(() => { /* convert results to Code diagnostics. ... */ }) // Send diagnostics connection.sendDiagnostics({ uri: document.uri, diagnostics, }) }) ```
A naive language server implementation to provide grammar diagnostics using Grammarly API
I got it working. I could see red twiddling underlines screaming at me. ![Screenshot of VS Code editor highlighting grammar issues and displaying Grammarly diagnostics](../../_assets/screenshot-grammarly-clUtP2JH.png) I started writing, but Grammarly diagnostics was extremely slow. I had to wait for seconds, which is a lot for writing, I expected a near-immediate response. However, the response in Grammarly editor is quite snappy. I guess I was doing something wrong. I wondered how do Grammarly editor work? ## Peeking into Grammarly API By looking at the network logs in Chrome DevTools, I found Grammarly uses a WebSocket for connecting to the grammar service at `wss://capi.grammarly.com/freews`. ![Screenshot of Chrome DevTools Network tab showing web socket connection to Grammarly API](../../_assets/screenshot-ws-B6r0839Q.png) On further inspection of messages, I found every message has a fixed structure: a message `id`, `action`, and the payload required for the `action`. The `id` value is from a sequence starting from 0 and incremented on every subsequent message. I guess, the `action` is the name of the function executed on the server, it looks like an RPC API. For every message sent, the server returned a response with the same `id` as the message. I needed more data to understand the API, so I started fiddling with the Grammarly editor while monitoring the socket connection. The editor starts a conversation with the server by sending a message with `start` action, which looks like the following snippet:
```json { "id": 1, "action": "start", "client": "denali_editor", "clientSubtype": "general", "clientVersion": "1.5.43-2120+master", "dialect": "american", "docid": "419610520", "documentContext": { "goals": ["convince", "describe", "inform"], "domain": "technical", "audience": "expert", "style": "neutral", "emotion": "mild", "emotions": ["neutral", "confident", "joyful", "optimistic", "respectful", "urgent", "friendly", "analytical"], "dialect": "american" }, "clientSupports": [ "text_info", "free_inline_advanced_alerts", "readability_check", "sentence_variety_check", "filler_words_check", "alerts_update", "alerts_changes", "free_clarity_alerts", "super_alerts", "consistency_check", "hidden_alerts_update", "set_goals_link" ] } ```
The first message sent to Grammarly WebSocket server
And the editor always waited for the response for the `start` action:
```json { "sid": 1, "action": "start", "id": 1 } ```
Ack from Grammarly server for the start message
After receiving the acknowledgment for the `start` action, the editor sends another message with `submit_ot` action. The `submit_ot` action sends the contents of the document as payload.
```json { "id": 1, "action": "submit_ot", "rev": 0, "doc_len": 0, "deltas": [ { "ops": [ { "insert": "I got grammarly working in code.\n\nAnd it's pointing at speling mistakes.\n" } ] } ] } ```
Initial message sent to Grammarly server for grammar anaylsis
In response to the `submit_ot` action, the server sends a series of `alert` actions. Each `alert` action represents some issue in the document. The `alert` actions are followed by a `finished` action which signifies end of diagnostics list. In [Stewart McGown implementation of Grammarly API](https://github.com/stewartmcgown/grammarly-api), the `finished` message is used as the end of the transaction, and all `alert` messages received so far are returned as resolved promise form the `analyze` method. I feel it's sufficient for immutable text, but I don't have immutable text, my text changes with every keypress. So, I started editing in Grammarly editor and monitored the socket connection. The editor sends `submit_ot` action on every change.
```json { "id": 3, "action": "submit_ot", "rev": 1, "doc_len": 74, "deltas": [{ "ops": [{ "retain": 73 }, { "insert": "a" }] }] } ``` ```json { "id": 4, "action": "submit_ot", "rev": 2, "doc_len": 75, "deltas": [{ "ops": [{ "retain": 73 }, { "delete": 1 }] }] } ```
Example insertion and deletion messages from Grammarly Operation Transformation messages
The `submit_ot` message includes document length (`doc_len`), revision (`rev`), and insertion or deletion operations (`deltas` array with `ops`). Recently, I have been reading about conflict-free replication in distributed data structures, and I feel the `ot` in `submit_ot` stands for [operational transformation](https://en.wikipedia.org/wiki/Operational_transformation). Grammarly's OT (or operational transformation) implementation seems to use revision (`rev`) and document length (`doc_len`) for state assertion and `deltas` for transformation messages. In response to `submit_ot` action, the server would send a series of `alert` actions, which got affected by the change triggered by `submit_ot` action, followed by a `finished` action. I wondered, if I could generate these operational transformation messages from Code's content change events, I could potentially get realtime diagnostics from Grammarly. ## Generationg Operational Transformations The language server protocol support incremental document synchronization with the Code editor, however, Code's content change events are slightly different. The change event uses range replacement, which makes synchronization simple and it requires less number of messages.
```ts connection.onDidChangeTextDocument((event) => { const { document, change } = event const { range, text } = change const content = document.getText() const offsetStart = document.offsetAt(range.start) const offsetEnd = document.offsetAt(range.end) // New revision of document. const newDocument = new TextDocument( document.rev + 1, content.substr(0, offsetStart) + text + content.substr(offsetEnd), ) }) ```
Example implementation of incremental document sync using content change events
But, for Grammarly, we have to transform these range replacement events to operational transformation messages. There are three possible scenarios in range replacement: 1. Insert non-empty text in an empty range
```ts const event = { range: { start: { line: 3, column: 0 }, end: { line: 3, column: 0 }, }, text: 'a', } const OT = { ops: [{ retain: 73 }, { insert: 'a' }], // 73 is offest at Line 3 Column 0 } ```
Code displaying same message as VS Code change event and Grammarly OT
2. Insert empty text in a non-empty range
```ts const event = { range: { start: { line: 3, column: 0 }, end: { line: 3, column: 1 }, }, text: '', } const OT = { ops: [{ retain: 73 }, { delete: 1 }], } ```
Code displaying same message as VS Code change event and Grammarly OT
3. Insert non-empty text in a non-empty range
```ts const event = { range: { start: { line: 3, column: 0 }, end: { line: 3, column: 1 }, }, text: 'b', } const OT1 = { ops: [{ retain: 73 }, { delete: 1 }], } const OT2 = { ops: [{ retain: 72 }, { insert: 'b' }], } ```
Code displaying same message as VS Code change event and Grammarly OT
So, I ended up reimplementing the Grammarly API hooked it up with Code's content change events. And I got the near-real-time feedback from Grammarly service. I skipped how I handled authentication in this article, it was tricky one, but I got a good starting point from [Stewart McGown implementation of Grammarly API](https://github.com/stewartmcgown/grammarly-api). ## Shipping the extension I followed the [VS Code Publishing Extension Guide](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) and got my extension on the marketplace. There were some hurdles in bundling the extension, but that deserves an article of its own. So, here I present ["Grammarly in Code"](https://marketplace.visualstudio.com/items?itemName=znck.grammarly). ![Screenshot of Grammarly extension on VS Code marketplace](../../_assets/screenshot-ext-Ckxyxl0T.png) > Markdown. Code. Grammarly.\ > Now, hear me ~~undefined~~ write. The [Grammarly extension is open source](https://github.com/znck/grammarly), and you can contribute or file issues if you face any problems. # Comments in Code So one more pull request got rejected for having a comment; "Good code is self-documenting.", said the reviewer. ## Good code What is good code? My code runs fast, is it "good code"? My code requires low memory, is it "good code"? It is difficult to answer, "what is good code?" without knowing, "why do we write code?". In my opinion, we write code to tell other people what we want a computer to do. We write for people. The preface of the book [Structure and Interpretation of Computer Programs](https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book-Z-H-7.html), by Abelson, Sussman, and Sussman, captures the intent appropriately: > Programs must be written for people to read, and only incidentally for machines to execute. Code is for people to read. In an ideal world, with a perfect language, a code is for people to read. However, ours is not an ideal world, and any programming language is far from perfect. Once in a while, we have to cut corners to improve performance or to fix a tricky bug or to handle an edge-case. After all, we want to ship better products, and it diverts us from the primary objective of writing code: "for people to read." Every once in a while, we have to complicate parts of some code to make the product better. Machines are happy with this complicated code, so are our customers. However, humans (a.k.a. developers) are troubled. Every time, a developer visits this piece of complicated code, she wonders why did we do this? Indeed the "why" is missing from any code. As I said before, we write code to tell other people what we want a computer to do. Yes! "what"; The code says "what" (or more precisely "how"). We need something to share the "why" behind the code, and comments fill that gap. The comments are part of any language syntax to convey, why the code is written this way? ## Comments tell Why Let's take an example of lazy `v-model` directive in Vue. The `v-model` directive binds a variable in JavaScript to an `` field such that the variables value updates as user types in the input field. However, for languages which use [IME](https://en.wikipedia.org/wiki/Input_method), this approach does not work, so Vue provides a lazy modifier. Following is the code snippet which handles lazy modifier: ```js if (!binding.modifiers.lazy) { el.addEventListener('compositionstart', onCompositionStart) el.addEventListener('compositionend', onCompositionEnd) el.addEventListener('change', onCompositionEnd) } ``` It is clear from the code that the `onCompositionEnd` method listens to `compositionend` and `change` events. [The spec](https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionend_event) says that the `compositionend` event is fired when composition completes or cancels, then why does this piece of code attach extra event listener. It looks like Evan forgot to remove it, let's send a PR. It makes sense absolute sense to remove that line of code, however only because the why is missing! Let's add the "why" to above code: ```js if (!binding.modifiers.lazy) { el.addEventListener('compositionstart', onCompositionStart) el.addEventListener('compositionend', onCompositionEnd) // Safari < 10.2 & UIWebView doesn't fire compositionend when // switching focus before confirming composition choice // this also fixes the issue where some browsers e.g. iOS Chrome // fires "change" instead of "input" on autocomplete. el.addEventListener('change', onCompositionEnd) } ``` What do you think now? Does it make any sense to remove the `change` event listener? Comments provide essential information about the code without adding any runtime overhead. Does that mean all comments are good? What do all comments mean anyways? ## Types of Comments Comments are free-form text. Depending on the type of text, we get different kinds of comments. Following types are as I see them: * Clarification Comments * Documentation Comments * License Comments * Reference Comments * Tagged Comments * Control Comments * Dead-code Comments ### Clarification Comments As evident from the name, these comments clarify some obscure piece of code or logic. Let's take another example from vue source:
```js let getNow = Date.now if (inBrowser && !isIE) { const performance = window.performance if (performance && typeof performance.now === 'function' && getNow() > document.createEvent('Event').timeStamp) { getNow = () => performance.now() } } ```
core/observer/scheduler.js with comments removed
This code snippet patches an inconsistency in browsers event timestamp API. From looking at the code, we get that we want to use `Date.now`; However, for some condition, we switch to `performance.now`. It is unclear why would we need this, but the comments can clarify:
```js let getNow = Date.now // Determine what event timestamp the browser is using. Annoyingly, the // timestamp can either be hi-res (relative to page load) or low-res // (relative to UNIX epoch), so in order to compare time we have to use the // same timestamp type when saving the flush timestamp. // All IE versions use low-res event timestamps, and have problematic clock // implementations (#9632) if (inBrowser && !isIE) { const performance = window.performance if (performance && typeof performance.now === 'function' && getNow() > document.createEvent('Event').timeStamp) { // if the event timestamp, although evaluated AFTER the Date.now(), is // smaller than it, it means the event is using a hi-res timestamp, // and we need to use the hi-res version for event listener timestamps as // well. getNow = () => performance.now() } } ```
core/observer/scheduler.js from vue source
The comments which tell "why" and explain the reason behind the code, I call them clarification comment. I recommend using clarification comments as long as you maintain them with the code associated. Yes, they add a little maintenance overhead but also keep the code understandable for humans. ### Documentation Comments Even the purist self-documenting developers have nothing against documentation comments (or doc comments). Doc comments provide additional information for APIs and make them approachable by humans. ```js /** * Computes (a^b) * * @param {number} a - The base number * @param {number} b - The exponent. */ function pow(a, b) { // ... } ``` Editors use the structured information from doc comments to provide rich developer experience. ![Screenshot: Editor intellisense using doc comments](../../_assets/doc-comment-C4Pd9mxy.png) Doc comments and editor intellisense give superpowers to us, we know what would a piece of code do without looking at the source. Also, Doc comments are used to automate API documentation generation. ### License Comments Today, we do dependency driven development. Every project depends on thousands of other projects. Licenses play an essential role in DDD. Some licenses allow modifying the original code. For example, the virtual DOM implementation in vue is a fork of Snabbdom.
```js /** * Virtual DOM patching algorithm based on Snabbdom by * Simon Friis Vindum (@paldepind) * Licensed under the MIT License * https://github.com/paldepind/snabbdom/blob/master/LICENSE * * modified by Evan You (@yyx990803) * * Not type-checking this because this file is perf-critical and the cost * of making flow understand it is not worth it. */ ```
core/vdom/patch.js:L1-10 from vue source
License comments, in derived work, are an excellent way of honoring another person's work. ### Reference Comments A significant part of a developer's work is copy-pasting from StackOverflow; we all know that. Sometimes we copy code without even thinking about it. There's no shame in that, but provide the reference to the next person working on that code. We can use a reference comment for that:
```js if (tag.indexOf('-') > -1) { // http://stackoverflow.com/a/28210364/1070244 return (unknownElementCache[tag] = el.constructor === window.HTMLUnknownElement || el.constructor === window.HTMLElement) } ```
platforms/web/util/element.js:L66-72 from vue source
The reference comments are often permalinks to StackOverflow (or similar platform) answers, although they can be page numbers from any book or snippets from other projects too. Add a reference comment whenever you copy-paste something in your code. ### Tagged Comments Tagged comments are inline reminders, something like a note to future self. They start with a fixed prefix, called tag here. Most common tag is `TODO`, but you can use any tag. ```js // TODO: Write some code here ``` Let's take another example from vue source:
```js prevChildren.forEach((c) => { c.data.transition = transitionData // TODO: record before patch positions if (map[c.key]) { kept.push(c) } else { removed.push(c) } }) ```
platforms/weex/runtime/components/transition-group.js:L60-70 from vue source
Tagged comments help us to continue concentrating on the current task while leaving hints for what to do next. ### Control Comments Today, we have a plethora of tools, making it easier to write code. Linters make us adhere to a code style.\ Transpilers allow us to use the latest syntax.\ Bundlers help us to ship code better. These tools work well in most cases, but there are some scenarios where they need some help from the developer. Comments again provide an excellent way to provide information and context to these tools. For example, webpack allows async imports to be preloaded or prefetched, and we can configure this inline: ```js const HomePage = () => import( /* webpackPreload: true */ './pages/home.vue' ) const AboutPage = () => import( /* webpackPrefetch: true */ './pages/about.vue' ) ``` The comment `/* webpackPreload: true */` configures webpack inline to preload the home page. Similar, control comments are offered by eslint too: ```js // eslint-disable-next-line eval(...) ``` The comment `// eslint-disable-next-line` tells eslint to shut up for the next line. Control comments give superpowers to toolings and developers to finetune their applications. ### Dead-code Comments The commented code or dead-code comments are the worst kinds of comments. There is absolutely no reason for their existence, delete them! Let's see an example from vue source:
```js // children.forEach(callPendingCbs) // Promise.all(children.map(c => { // const oldPos = c.data.pos // const newPos = c.data.newPos // const dx = oldPos.left - newPos.left // const dy = oldPos.top - newPos.top // if (dx || dy) { // c.data.moved = true // return this.animate(c.elm, { // styles: { // transform: `translate(${dx}px,${dy}px)` // } // }) // } // })) ```
platforms/weex/runtime/components/transition-group.js:L97-128 from vue source
I hate dead-code comments; they are easy to add but difficult to remove. When you come back to dead-code comments, you always wonder, why did you comment this code in the first place? Please remove dead-code comments from vue, send a pull request. ## Comment! Comment! Comment Comments are compelling tools in programming languages. Use them to complement your code, to answer questions which are unanswered by the code. Do not use comments to create more questions. # Running an awesome conference I love attending conferences. For inspiring talks, for making new friends and meeting old friends, or for learning new things, conferences have been influential in my journey as a developer. I like many conferences, but I want to be in every vue conference. If you been to a vue Conference, then you know that there’s something different about it. The environment is so welcoming and friendly. Sad! I have to travel across the world to be in one. I have longed to see a vue conference in India, my home, for a long time now. I decided, I was done waiting! *Let’s start a vue conference*, I urged Swapnil. He was in. We were ready. But we didn’t have the slightest clue how to. So, we started looking at other conferences. ReactFoo, JSFoo, VueConf, Connect.Tech and VueFes. These conferences are enormous. A lot goes into running them, and that scared me. It was not doable for me, for Swapnil, for a team of two who has been struggling to keep the vue meetup running. The dream would have ended there if Sid wasn’t there. One evening, Sid dropped something in my Twitter inbox: > Hi! I have a CFP for you. 😅 That random CFP has been very phenomenal for me. Who knew one random message, on the evening of 23rd December 2018, would make a conference organizer one day. Thanks a lot, Sid for the birthday gift! > Hi 👋 > > This is random but I'm organizing a tiny react conference on Jan 5 > > very few attendees compared to a normal conf (25) and plenty of time between talks to discuss and have conversations > > I realize that's the part I most enjoy at conferences, so it would be cool to shape the entire conference around it > > would you like to give a talk on vue features that the react community should steal 😁 > > (not announced yet) It was a different kind of CFP — “here’s a topic, you’re speaking on it.” If you know me, you know that I love taking vue to react conferences. I was in. [TinyConf](https://tinyconf.in) was excellent, I had a great time. I got a valuable lesson out of it: conferences can be small. A small conference! A hope! A possibility! The dream was back on! We decided to have a small conference, 30 people. Next, we needed speakers, a catchy name, and a venue. Everything we’re already doing for meetups but this time for the conference. We started with the name to get the domain. Don’t we all like to buy a domain the moment idea strikes! The first choice was: **EliteConf** - best vue developers at one place. The name didn’t paint the picture of inclusiveness and warmth of a vue conference. Next set of choices were: OpenConf, FocusConf, AllConf, VuetifulConf, VueUnconf Nah! None of these. What about **CoreConf**? But we don’t have all core members speaking. **MiniConf**? Very close to Sid’s tinyconf, it has been our inspiration but sounds like a ripoff. **SuperConf**? Nah! We want first-time speakers, it’s not very inviting. Maybe **DebutConf**? Maybe! It can be a meetup theme, though. **AwesomeConf**? +1 Nothing against AwesomeConf. Even if I had something, it would have been impossible to convince Swapnil otherwise. That guy is crazy for awesome lists! Got a name! Lucky us, google came up with .dev TLD around then. But we had to wait for the public release and hope nobody gets awesomeconf.dev during pre-release days. The days were hard, and the nights were long. Finally, we got it - [awesomeconf.dev](https://awesomeconf.dev) > Something exciting is brewing! 🤫 > — [@VueBLR](https://twitter.com/VueBLR/status/1101209663834533888) Next immediate step, build a website. We needed a theme, an identity for the conference. A theme which represents the part we enjoy most: conversations. If you’re a speaker, it’s easier to start a conversation as people already know what you're working on. We decided, let's make everyone speak! And we got a unique conference: * 30 attendees * Everyone speaks! * All about vue Swapnil immediately put out a website and CFP for the AwesomeConf. > The wait is over! Announcing an Awesome Vue.js Conference... 🎉 > > * The best of Vue.js devs at one place. 😎 > * No one shall be a passive attendee, i.e. every participant has to speak. 🤓 > * Talks can range anywhere from 5 to 20 minutes. 💪 > > May 4, Bangalore > — [@VueBLR](https://twitter.com/VueBLR/status/1102617517292322816) Next step was to find a venue. We had the regular meetup venue as a backup. But we wanted better, something like a hotel conference room! So, we started looking for sponsors. What do we have for sponsors? * 30 vue developers * One room * Growing frontend ecosystem * Networking opportunity It was unconventional, far from regular conferences. We reached out to a few companies. Swapnil could lock-in a deal with [Meesho](https://meesho.com), and we got a fantastic hotel venue. We were targeting to have 30 people and had all arrangements. But, we didn’t realize it would be impossible to have 30 sessions. In fact, we could not accommodate more than 15 talks. We decided to go with 15 talks. For other 15 available seats, we decided to allow +1 with every speaker. > Hello everyone! 👋 > > Speaking at a conference can be stressful and intimidating. A familiar face can be very comforting and confidence boosting. We encourage you to ask your significant other or your friend or your colleague to join AwesomeConf for a day with Vue. > > If you plan to bring +1 to the conference, please confirm by replying to this thread. A day before the conference. We double checked the venue, perfected the schedule. Everything was perfect. Well, except the one most important thing: > All set for [@AnAwesomeConf](https://twitter.com/AnAwesomeConf)! > > Just one thing left! 🤪 DM me your best [@vuejs](https://twitter.com/vuejs) puns [#AnAwesomeConf](https://twitter.com/hashtag/AnAwesomeConf) [#Vue](https://twitter.com/hashtag/Vue) [#VueBLR](https://twitter.com/hashtag/VueBLR) > — [@znck0](https://twitter.com/znck0/status/1124453377792864257) I self-appointed myself, the master of ceremonies. A good MC must throw in bad puns. Chris and Ben shared their most prized collection of vue puns. Now, I was ready. I was hosting a conference. I was a **conference organizer**. ![AwesomeConf: No one shall be a passive attendee](../../_assets/ac-standee-BKyjrjYJ.jpg) The conference day, we were excited, terrified, nervous, and every other thing. All attendees were there on time, which had rarely happened in our meetups. But there’s always someone who’s stuck in Bangalore traffic, but this one was our first speaker. It’s a different story, you should read [the article](https://dev.to/gokatz/first-time-speaker-fts-with-few-tips-45po) by Gokul. ***Enjoy the Vue*** - Swapnil completed his welcome address and the conference begins. We had 5 sessions of 3 talks each, filled with breaks for discussions and a great lunch. Some people enjoyed talks, others enjoyed conversations but most enjoyed the lunch. ![Lunch: Secret of making a regular conference into an awesome conference](../../_assets/ac-lunch-Dr_uv9oN.jpg) ## Did we meet our expectations? 15 talks, 8 first time speakers, 1 student. We did an excellent job with the first vue conference in India, and it seems likewise from the twitter reactions. ![Tweets from AwesomeConf attendees](../../_assets/tweets-A_fAqIev.jpg) I wanted to host a vue conference where every attendee is actively involved, and we did it. I am pleased with AwesomeConf, and so are the attendees. Also, no one was hurt by my bad jokes and puns, so that's a plus. > When someone asks you to explain your joke. [@znck0](https://twitter.com/znck0) [#AnAwesomeConf](https://twitter.com/hashtag/AnAwesomeConf) ![Rahul explaining a joke at AwesomeConf](https://t.co/higeRbCglc) > — [@SwapAgarwal](https://twitter.com/SwapAgarwal/status/1124556436711849985) ## What didn't work? **The projector**. We had some issues with the projector throughout the day. Working with projectors is terrible. Daisy chaining dongles to get the correct port. Checking loose connections. Changing laptops after every talk. It was too much work. We should have tested it a day before. Also, a multi-input projector would have been much better. **Recording**. We did try to record the conference. Our lack of experience with professional recording setup resulted in terrible videos. The talks were excellent, and they deserve to be shared with the world. We will plan better next time. ## It was an awesome conference! I feel the conference provided the required platform to bring out those eight first-time speakers. *You are a conference speaker now!*\ *You are awesome.*\ *You spoke at an awesome conference.*\ *Which conference?* – I ended my closing address.\ *An awesome conference*. ![That's all folks. It was an awesome conf.](../../_assets/fin-BGkQVUmD.jpg) Thanks a lot for being part of the AwesomeConf. Thanks [Sid](https://twitter.com/@siddharthkp) for inspiring us and introducing to the idea of [a tiny conference](https://sid.studio/post/tinyconf). Thanks [Zainab](https://twitter.com/@zainabbawa) for helping and mentoring us. Thanks [Meesho](https://twitter.com/@MeeshoApp) for supporting us. Thanks [Suwardhan](https://instagram.com/suwardhan.design). [Swap](https://twitter.com/@SwapAgarwal) & [Znck](https://twitter.com/@znck0) would be back soon with next iteration of AwesomeConf. ![AwesomeConf 2019 Pass](../../_assets/signed-C6DkAyQG.jpg) # Type Vuex without TypeScript > This is a continuation article to [Type Vue without TypeScript](../2018-11-11-type-vue/index.html.md), if you haven’t already read it, I suggest you read that first. ## Vuex does support types! The official state management library for Vue does support TypeScript. It provides MutationTree and ActionTree type helpers to get auto-completion and intellisense in mutations and actions. ![Vuex store implementation written in TypeScript](../../_assets/1-BnqozjI5.png) We can also get types in JavaScript with JSDoc `@type` annotation. ![Vuex store implementation written in JavaScript](../../_assets/2-BQlXO-Xp.png) If we use options syntax for creating a store, most of the boilerplate comments are unnecessary. ![Vuex store implementation written in JavaScript using options syntax](../../_assets/3-Bslfh-Hi.png) The TypeScript API for Vuex is limited however. Type checks and intellisense in commit and dispatch are very primitive.
Screenshot: Intellisense in commit methods of vuex store Screenshot: Intellisense in dispatch methods of vuex store
Intellisense in commit (left) and dispatch (right) methods of Vuex store
As you can see above, we get intellisense to let us know that commit and dispatch take strings as the first argument, but we don’t get a list of registered actions and mutations. There is not much we can do here, with or without TypeScript. However, there are lots of third-party packages trying to get the types right in the Vuex store and modules. ![Vuex helper packages for TypeScript](../../_assets/6-DIBqQFA6.gif) We won’t be introducing new dependencies to a project only to get the types right, as most people are using just Vuex. However, [vuex-class](https://github.com/ktsn/vuex-class), [vuex-typescript](https://github.com/istrib/vuex-typescript) and [vuex-module-decorators](https://github.com/championswimmer/vuex-module-decorators) are worth mentioning third-party packages assisting Vuex stores. > All the TypeScript + Vue users out there, what do you use to write Vuex modules and why? > — [@znck0](https://twitter.com/znck0/status/1062919689515368448) The next version of Vuex would be built with an API which plays well with TypeScript. We cannot do much with committing mutations and dispatching actions due to limitations in the TypeScript API for Vuex. Though with namespaced modules, we can limit the scope of a store and keep individual store modules comprehendible. However, there is another area of Vuex which can use types and intellisense. ### Using Vuex Store in Vue components Vuex provides mapState, mapGetters, mapActions and mapMutations helpers to simplify binding store state in Vue components. However, the API is string literals based and sort of impossible to provide types and intellisense. ![Using Vuex store in Vue component](../../_assets/7-M3zxuUol.png) This problem is still unsolved in the TypeScript world, and there is not much we can do in the JavaScript world. However, there exists a pattern, and to my knowledge, popularised by [Chris Fritz](https://twitter.com/chrisvfritz)’s [Enterprise Boilerplate](https://github.com/chrisvfritz/vue-enterprise-boilerplate) which can be enhanced. ### Helpers pattern in Vuex According to Chris’ helper pattern, we create a store helper file, and all the mapXxx method calls of Vuex store usages are collected here. We can create contextual groups of computed and methods properties which can be added to components directly. ![Vuex helper pattern example](../../_assets/8-CxNvcxE4.png) Other than providing possibilities of intellisense enhancements, this pattern reduces the surface area affected by Vuex in an application. Let’s say if Vuex introduces breaking changes then we have to update only helpers.js file. Also, it reduces code footprint and increases reuse of mapXxx across packages. ### Intellisense for Vuex stores written with helpers pattern We can annotate exports from helpers in such a way that we get better intellisense when used in the component. e.g., In the following code snippet, addTodo method accepts a text argument of type string. ![Intellisense in Vuex mapped methods](../../_assets/9-ahcXzF5b.png) Intellisense in above snippet is not automatic; it requires some type-casts using JSDocs. e.g., There should be addTodo method on the type of todosMethods object exported by helpers module. So with a type definition using @typedef, we can name this type TodoMethods. ![Vuex helpers with JSDoc type annotations](../../_assets/10-rVszHpV-.png) In the above type definition, TodoMethods has a property addTodo of type function. To define a function type, we can use a double curly bracket, similar to Vue’s moustache syntax (`{{ ... }}`). In the double curly brackets, we add types in format (param: type): return-type and hence for a function accepting one string argument, we have `{{ (text: string): void }}`. As we know an action always returns a promise, we can augment addTodo method type to `{{ (text: string): Promise }}`. Similarly, we can add a type to computed properties, but as required by API, these should be functions. ![VS Code Type Inference](../../_assets/11-DpGttMf7.png) This approach is a little brittle as any changes in properties used in `mapXxx` could potentially effect JSDoc type definitions. Also, any new property added to mapXxx functions requires type definition to be added to the corresponding typedef. Also, if you noticed the type of `completedTodos` in the above snippet, it says `any[]`. It’s due to a bug in VS Code which does not show imported types correctly. ![JSDoc type hint](../../_assets/12-Bks9Zzdt.png) ![Bug in VS Code which fails to display imported files (outdated)](../../_assets/13-DaHLSGuR.png) That’s all folks for Vuex and types without TypeScript. If you have question about it, you can reach me on Twitter ([@znck0](https://twitter.com/znck0)). # Type Vue without TypeScript A practical guide for type checking vue components written in JS and getting things done. ## Why do we need types? In the last decade, browsers have become very powerful, allowing developers to build rich interactive applications. The interactivity and richness have come with an increase in complexity and size of the codebase in the frontend. Large complex codebases demand more attention and frontend teams have become larger. When more than one developer collaborates, there is a requirement of clear interfaces and APIs. JavaScript is very flexible, but you cannot deduce the type of arguments a function would accept. e.g., What do you think print function’s argument type is? ```js function print(value) { console.log(value) } ``` The type of value is ambiguous, even after looking at the source code of the print function which poses problems in large codebases where you cannot afford to jump into the source to deduce argument type of a function. One way to resolve this issue is to document the function’s API, but docs have a tendency of getting outdated. **What if there were a way to know a function’s argument types by just glancing over the function name?** Yes, there is a way: types or TypeScript. ```ts function print(value: string) { console.log(value) } ``` As soon as we look at the print function, we know value should be a string. That little information immediately boosts collaboration and productivity. In some sense, types make APIs self-documenting. Apart from this, code editors/IDEs also benefit a lot from types. IDEs can provide intelligent suggestions, and display type mismatches inline.
Screenshot: Intellisense/Type-based suggestion Screenshot: Type mismatch error
Intellisense/Type-based suggestion (top) — Type mismatch error (bottom)
> Any application that *can* be written in ~~undefined~~ TypeScript, *will* eventually be written in ~~undefined~~ TypeScript. > > – ~~undefined~~ Rahul Kadyan ## So TypeScript is it? TypeScript makes interfaces explicit and enables collaboration, but it comes with its complexity and downsides. * Additional time would be spent adding types. * JavaScript’s flexibility is lost as we have to type everything and sometimes it gets very complicated to add types. * A steep learning curve and retraining of developer staff. ## Vue and TypeScript Vue provides a fluent object-based API for authoring composable components.
```js export default { data() { return { items: [], } }, methods: { add(text) { this.items.push({ text, done: false }) }, complete(index) { this.items[index].done = true }, }, } ```
Todo list component (JavaScript)
The above component can be written in TypeScript as:
```ts import { Component, Vue } from 'vue-property-decorator' interface TodoItem { text: string done: boolean } @Component export default class Todo extends Vue { private items!: TodoItem[] private add(text: string) { this.items.push({ text, done: false }) } private complete(index: number) { this.items[index].done = true } } ```
Todo list component (TypeScript)
With TypeScript, you get type based intellisense which boosts developer productivity.
VS Code type inference in Vue components written in JavaScript VS Code type inference in Vue components written in TypeScript
VS Code type inference in Vue components written in JavaScript(left) and TypeScript(right)
TypeScript support in Vue was an afterthought, and it becomes quite verbose in real-world applications. ***Good thing is that’s going to change with Vue 3 coming mid-next year.*** Adding types to Vuex is even more complex and unpleasant. Also, the intellisense on state/getter/actions mapped to components is almost non-existent. > Most common bugs in Vue apps are not due to type errors. > > — Chris Fritz, Curator of Vue Docs Yes, that’s true, and tools like ESLint are far more helpful in catching these bugs. ## So, do we need TypeScript at all? Maybe but you don’t need to jump on it today. TypeScript ensures type safety, documents API interfaces and enables intellisense in IDEs. I feel intellisense is far more critical in Vue application projects than type safety. The best thing about TypeScript is that **you can take advantage of TypeScript without using TypeScript**. Yes, with VS Code and TypeScript, we can get intellisense in JavaScript files. We can enforce type safety too if we want that. VS Code infers JSDoc annotations to generate TypeScript definition on the fly. Let’s set up a JavaScript project with types and intellisense. ## Intellisense in JavaScript In JavaScript files, VS Code infers types for static values to provide intellisense, including object property names, function return value type, and property types.
VS Code type inference on object properties return value from function
VS Code type inference on object properties (top) and return value from function (bottom)
VS Code has [automatic type acquisition](https://code.visualstudio.com/docs/languages/javascript#_automatic-type-acquisition) which uses npm package’s bundled types or community types from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) to provide intellisense, including method signature and parameter info. Also, using Vue’s type information and [Vetur plugin](https://marketplace.visualstudio.com/items?itemName=octref.vetur), it can provide rich completions and type information in .vue files too. ![Autocomplete Suggestions](../../_assets/8-DFrj1CFY.png) ![Type Inference](../../_assets/9-CVbkwbCz.png) However, static type inference for array literals and dynamic values is not possible, for such cases, VS Code can use [JSDoc](http://usejsdoc.org) annotations to collect type information. In the following snippet, type of this.items is detected as an array of type any, as it’s known statically that this.items is an array, but there is no information about values in the array. We can use a@type annotation for adding type information to this.items, @type allows adding type information, similar to any typed language (or TypeScript). ![VS Code type inference in Vue components written in JavaScript](../../_assets/9-CVbkwbCz.png) ![VS Code type inference in Vue components written in JavaScript with JSDoc annotation](../../_assets/11-BNrKlz7P.png) VS Code type inference from JSDoc comments is as reasonable as TypeScript. See the following code snippets written in JavaScript and TypeScript. ![VS Code type inference in Vue components written in JavaScript](../../_assets/11-BNrKlz7P.png) ![VS Code type inference in Vue components written in Typescript](../../_assets/4-D0jie-PY.png) ## Using JSDoc to add type information to Vue components Vue component options have data, props, computed and methods which when provided with type information can significantly improve the developer experience. ## 1. Data We have to provide @type annotations for properties which are impossible infer statically. e.g.:
Vue component definition Inferred type data object
Vue component definition (left) and inferred type data object (right)
In the above snippet, items and currentItem have incomplete type information, so we need@type annotations only for those two properties.
Vue component definition inferred type data object
Vue component definition (left) and inferred type data object (right)
Type definitions for items and currentItem are quite similar. If we were writing TypeScript, we would have created an interface for the item type. With JSDoc, we can define custom types or interfaces using a@typedef annotation.
JSDoc typedef example inferred type in VS Code
JSDoc typedef example (left) and inferred type in VS Code (right)
Autocomplete suggestions and type information for data are available on this context in life-cycle hooks, methods, computed and watch handlers. ## 2. Props For primitive props, VS Code can infer type information automatically. ![Vue component options with props as object map of types](../../_assets/20-Cg4DW3vD.png) ![Vue component options with props as validator options having type key](../../_assets/21-CmyH3I-R.png) However, if you have Object or Array as the type, then auto-inferred type information is useless. In such cases, we have to provide type information with a @type annotation. ![Vue component options with props as object map of types having complex types](../../_assets/22-DR5o5qSS.png) It even works with the validator options syntax for props. ![Vue component options with props as validator options having type key having complex types](../../_assets/23-DVc9JlQh.png) When defining prop names as an array, it's a little complex to provide type information. It is discouraged to use names array for props definition. ![Discouraged syntax for prop definition in Vue component options](../../_assets/24-CbLZXw7V.png) ## 3. Computed For computed properties, automatic return type inference does not work as expected but it does provide computed property names in suggestions. ![Vue component’s computed property in VS Code suggestions](../../_assets/25-DjRbyL78.png) So with a @returns annotation, we can provide type information for the returned value as well. ![Vue component’s computed property with type information inferred from JSDoc in VS Code suggestions](../../_assets/26-C0DEYuO4.png) ## 4. Methods Method names are available in VS Code suggestions on this context, but they lack type information. ![Vue component’s method name autocomplete suggestion in VS Code](../../_assets/27-CWVjhiti.png) Here, we see the add method accepts one parameter text and returns a number, the auto inferred information is already beneficial, and if we can provide type information for the text parameter, we get the complete experience of a typed language. Moreover, we can add types to parameters using the `@param` annotation. ![Vue component’s method name autocomplete suggestion in VS Code with complete type information](../../_assets/28-B1G3HcBE.png) We can even provide a small description of the method if it’s not clear from its name. ![Vue component’s method name autocomplete suggestion in VS Code with complete type information and custom description](../../_assets/29-Ck_l4EDS.png) VS Code supports even more JSDoc annotations; you can find the complete list of supported annotations on the [VS Code wiki page](https://github.com/Microsoft/TypeScript/wiki/JsDoc-support-in-JavaScript). ## Can we do strict type checks in JavaScript? Yes, it’s possible, add `// @ts-check` at the start of the script in .vue file. ![Strict type check errors in JavaScript](../../_assets/30-jfmENuE8.png) If you want to enable strict type checks in the whole project, then you should add a jsconfig.json file to the project root. ```json { "compilerOptions": { "checkJs": true } } ``` You can find more options at the [jsconfig.json reference](https://code.visualstudio.com/docs/languages/jsconfig). > ~~undefined~~ [Type Vuex without TypeScript](https://znck.dev/blog/2018-type-vuex) # Accessible Components: Test Driven Approach The web is undoubtedly the most vital resource today. Modern websites are built-in small building blocks; **components**. Practices from test-driven software development have employed to automate the correctness of these building blocks. The internet should be accessible to all. It is essential to provide equal access and opportunity to people with disabilities. You can find many tools to check accessibility on web pages. But, in my opinion, when you’re testing web pages in a modern application, you’re very late. Accessibility testing should be employed at the lowest level; you should create accessible components. ## Unit Testing Accessibility Let’s simple component which renders following HTML: ```html ``` The `