<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>SHIINAYANE (en)</title><description>YK&apos;s Blog</description><link>https://www.shiinayane.com/</link><language>en</language><item><title>I Turned My AI Workflow into Code. Then I Tore It Apart.</title><link>https://www.shiinayane.com/en/posts/i-turned-my-ai-workflow-into-code-then-i-tore-it-apart/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/i-turned-my-ai-workflow-into-code-then-i-tore-it-apart/</guid><description>The day after I published I Turned My AI Workflow into Code, BiliKit M5.0 gave me a counterexample.</description><pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;On July 25, I published &lt;a href=&quot;/en/posts/i-turned-my-ai-workflow-into-code/&quot;&gt;I Turned My AI Workflow into Code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One day later, I tore apart its central premise.&lt;/p&gt;
&lt;p&gt;The timing was almost funny. When I finished that post, I thought the engineering workflow BiliKit had forced me to develop was finally becoming stable: risk levels, quality gates, independent review, complexity budgets, and eventually a pair of reusable Codex Skills.&lt;/p&gt;
&lt;p&gt;The next day, BiliKit itself gave me a counterexample.&lt;/p&gt;
&lt;h2&gt;The scroll position that kept getting more complete&lt;/h2&gt;
&lt;p&gt;I was working on M5.0. The requirement sounded simple:&lt;/p&gt;
&lt;p&gt;Open a video from a browsing page, return from playback, and see roughly the same part of the page as before.&lt;/p&gt;
&lt;p&gt;BiliKit was using a custom routing model. Opening playback removed the browsing view from the tree. Returning created it again, which meant its content and scroll position had to be restored manually.&lt;/p&gt;
&lt;p&gt;The first version remembered the selected video ID. That could bring the card back on screen, but it could not restore the viewport I had actually left.&lt;/p&gt;
&lt;p&gt;So I started storing a raw offset.&lt;/p&gt;
&lt;p&gt;Every problem that followed was reasonable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The old offset might become unreachable after the content height changed.&lt;/li&gt;
&lt;li&gt;A callback from the old &lt;code&gt;ScrollView&lt;/code&gt; could contaminate a new snapshot.&lt;/li&gt;
&lt;li&gt;Late asynchronous results needed request identity and generation checks.&lt;/li&gt;
&lt;li&gt;User scrolling had to release the programmatic restoration state.&lt;/li&gt;
&lt;li&gt;Deterministic tests and XCUI had to prove that returning really looked like leaving.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each fix made the implementation more complete.&lt;/p&gt;
&lt;p&gt;It passed the tests too.&lt;/p&gt;
&lt;p&gt;That was the troublesome part. Tests, reviews, and gates were all checking whether the restoration mechanism was correct. Nobody kept asking:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why does the browsing page have to be destroyed at all?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I eventually stopped and built a small SwiftUI probe comparing conditional replacement, explicit restoration, and a native &lt;code&gt;NavigationStack&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The result was direct.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;NavigationStack&lt;/code&gt;, the root view was not created again and its state identity survived. A substantial part of the restoration problem came from the navigation structure we had chosen ourselves.&lt;/p&gt;
&lt;p&gt;BiliKit then moved to a native &lt;code&gt;TabView(.sidebarAdaptable)&lt;/code&gt;, with a separate &lt;code&gt;NavigationStack(path:)&lt;/code&gt; for each tab. The custom &lt;code&gt;AppRoute&lt;/code&gt; and &lt;code&gt;AppReturnSnapshot&lt;/code&gt; went away. Bounded browsing worksets stayed, along with request cancellation and playback lifecycle handling. SwiftUI now owns the scroll position through semantic IDs.&lt;/p&gt;
&lt;p&gt;Two real popular-feed round trips measured:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0.195066 → 0.194867
0.389877 → 0.389447
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There was no longer a reason for the ViewModel to store a raw numerical offset and own the clamping, restoration latch, and stale callback isolation around it.&lt;/p&gt;
&lt;h2&gt;The process worked exactly as designed&lt;/h2&gt;
&lt;p&gt;No agent was obviously slacking off here.&lt;/p&gt;
&lt;p&gt;The repository rules were read. The risks were identified. The validation level was reasonable. The implementation work found real bugs involving stale callbacks, unreachable offsets, lifecycle cleanup, and XCUI driving.&lt;/p&gt;
&lt;p&gt;Every part of the process had accepted the same premise: keep the existing navigation structure and make state restoration reliable.&lt;/p&gt;
&lt;p&gt;Once that premise entered the task contract, the agents became very good at following it.&lt;/p&gt;
&lt;p&gt;Reviewers checked boundaries. The red reviewer searched for failure paths. Tests accumulated, and the documents became more complete. Every layer improved the correctness of the current approach while making it look more like the only approach worth continuing.&lt;/p&gt;
&lt;p&gt;In the previous post, I described &lt;code&gt;project-governance-bootstrap&lt;/code&gt; as a small compiler:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;repository facts
+ architecture documents
+ tests
+ security boundaries
→ project rules
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is a large problem with that analogy.&lt;/p&gt;
&lt;p&gt;A compiler can only process what it receives.&lt;/p&gt;
&lt;p&gt;If the architecture, task contract, and current implementation all omit a simpler route, the Skill can only compile the existing route more strictly. Path dependence does not disappear. It gets organized into better documents, more checks, and stronger momentum.&lt;/p&gt;
&lt;h2&gt;I nearly turned the correction into another process&lt;/h2&gt;
&lt;p&gt;My first reaction was still to turn this experience into rules.&lt;/p&gt;
&lt;p&gt;Perhaps every high-risk task should list alternatives first. Perhaps it needed a mandatory challenger, a decision ledger, and another reviewer dedicated to checking the problem definition.&lt;/p&gt;
&lt;p&gt;It started feeling wrong while I was writing it.&lt;/p&gt;
&lt;p&gt;I was adding process to prevent too much process. To keep agents from mechanically following a task contract, I was preparing a longer task contract.&lt;/p&gt;
&lt;p&gt;That was not far from what I had just torn apart.&lt;/p&gt;
&lt;p&gt;So I did not make a workflow v2.&lt;/p&gt;
&lt;p&gt;I simplified BiliKit&apos;s collaboration rules directly. One commit removed 766 lines of configuration and documentation, along with five fixed agent definitions. The current &lt;code&gt;AGENTS.md&lt;/code&gt; keeps project facts, architectural boundaries, safety constraints, working validation commands, and commit conventions.&lt;/p&gt;
&lt;p&gt;The quality checks are still there.&lt;/p&gt;
&lt;p&gt;SwiftPM, &lt;code&gt;xcodebuild&lt;/code&gt;, lifecycle tests, and security boundary checks still answer useful questions: whether the build broke, cancellation is correct, resources are released, and the real app can run.&lt;/p&gt;
&lt;p&gt;Fixed risk colors, task-contract forms, reviewer chains, and complexity budgets are no longer default ceremonies.&lt;/p&gt;
&lt;p&gt;They remain available when a change actually needs them. Every task no longer has to prove that it followed the process.&lt;/p&gt;
&lt;h2&gt;What happens to the two Skills?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;apple-dev-loop&lt;/code&gt; is mostly fine for now.&lt;/p&gt;
&lt;p&gt;It routes Apple-platform validation: when SwiftPM is enough, when &lt;code&gt;xcodebuild&lt;/code&gt; is needed, and when evidence requires an &lt;code&gt;.xcresult&lt;/code&gt;, a signed app, XCUI, or Instruments.&lt;/p&gt;
&lt;p&gt;The boundaries of that job are fairly clear.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;project-governance-bootstrap&lt;/code&gt; is different.&lt;/p&gt;
&lt;p&gt;It was designed to derive governance from a repository. That carries an assumption that enough repository context can produce the right workflow for the project.&lt;/p&gt;
&lt;p&gt;BiliKit just demonstrated that a repository can describe the wrong route in great detail.&lt;/p&gt;
&lt;p&gt;I am going to reconsider that Skill. It may shrink into a tool that only collects existing facts and validation entry points. I may archive it instead. I have not decided yet.&lt;/p&gt;
&lt;h2&gt;No new answer yet&lt;/h2&gt;
&lt;p&gt;The main BiliKit source and test directories now contain almost 25,800 lines of Swift.&lt;/p&gt;
&lt;p&gt;The project is still too large for one prompt followed by one diff review. The old rules were not useless either. They caught plenty of real problems.&lt;/p&gt;
&lt;p&gt;I no longer think I can turn that experience into a universal workflow and have it choose the right route for the next project.&lt;/p&gt;
&lt;p&gt;At least it could not this time.&lt;/p&gt;
&lt;p&gt;I am keeping the July 25 post. It records something I genuinely believed at the time. Quietly rewriting or deleting it would make the story less useful.&lt;/p&gt;
&lt;p&gt;This post records what happened the next day.&lt;/p&gt;
&lt;p&gt;I will decide what comes after that when the project forces me to change again.&lt;/p&gt;
</content:encoded></item><item><title>I Turned My AI Workflow into Code</title><link>https://www.shiinayane.com/en/posts/i-turned-my-ai-workflow-into-code/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/i-turned-my-ai-workflow-into-code/</guid><description>BiliKit passed 21,000 lines before V1. I extracted the workflow it forced me to build into two reusable Codex Skills.</description><pubDate>Sat, 25 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update, July 27, 2026:&lt;/strong&gt; The day after publishing this post, BiliKit M5.0 exposed the biggest problem with this workflow: it could make a chosen route increasingly rigorous without ever checking whether the problem should be solved that way. I am leaving the original post intact and wrote a follow-up: &lt;a href=&quot;/posts/i-turned-my-ai-workflow-into-code-then-i-tore-it-apart/&quot;&gt;I Turned My AI Workflow into Code. Then I Tore It Apart.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;BiliKit V1 is not finished yet, and the Swift codebase has already passed 21,000 lines.&lt;/p&gt;
&lt;p&gt;When the project was small, my AI workflow was straightforward: write a longer prompt, let the agent make the change, and check the diff myself. Even when it got something wrong, the change usually touched few enough files that I could spot it quickly.&lt;/p&gt;
&lt;p&gt;That stopped being enough as the repository grew.&lt;/p&gt;
&lt;p&gt;The same instruction—&quot;read the project rules before making changes&quot;—could produce very different results in a new session. Sometimes every test passed, but the change landed in the wrong module. Sometimes a small fix came back with an entirely new abstraction.&lt;/p&gt;
&lt;p&gt;BiliKit also has several areas that a green test suite cannot fully cover: Keychain access, playback lifecycles, cancellation, danmaku rendering, and the local server. A successful build does not tell me whether those paths work in a signed app or survive actual use.&lt;/p&gt;
&lt;p&gt;So the repository gradually accumulated more machinery:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;risk levels&lt;/li&gt;
&lt;li&gt;quality gates&lt;/li&gt;
&lt;li&gt;independent review&lt;/li&gt;
&lt;li&gt;complexity budgets&lt;/li&gt;
&lt;li&gt;different validation paths for different kinds of work&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After a while, the workflow was reasonably stable inside BiliKit. Then I started another project and realized I would have to assemble it all over again.&lt;/p&gt;
&lt;p&gt;Some rules lived in &lt;code&gt;AGENTS.md&lt;/code&gt;, some in gate documents and scripts, and some were still habits in my head. I could rewrite them from memory or copy the BiliKit files.&lt;/p&gt;
&lt;p&gt;The first option would miss things. The second would bring far too much baggage.&lt;/p&gt;
&lt;p&gt;A static website does not need BiliKit&apos;s rules for playback, Keychain, local servers, or media redirects. A utility with fewer than 1,000 lines may still need strict handling if those lines touch credentials.&lt;/p&gt;
&lt;p&gt;What I wanted to reuse was the way those rules were derived.&lt;/p&gt;
&lt;p&gt;I created a repository called &lt;code&gt;codex-engineering-skills&lt;/code&gt; and started with two Codex Skills:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;project-governance-bootstrap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apple-dev-loop&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first reads a project and builds engineering rules that fit it. The second handles the build, test, and validation loop for Apple-platform work.&lt;/p&gt;
&lt;h2&gt;Copying BiliKit&apos;s AGENTS.md did not work&lt;/h2&gt;
&lt;p&gt;My first idea was to turn BiliKit&apos;s &lt;code&gt;AGENTS.md&lt;/code&gt; into a generic template.&lt;/p&gt;
&lt;p&gt;It did not take long to find the problem.&lt;/p&gt;
&lt;p&gt;BiliKit treats authentication, redirects, local servers, playback, concurrency, renderers, and destructive migration as high-risk work. That list makes sense because the project actually has those failure modes. Put the same list in a static website and it becomes ceremony. Ask a command-line parser to validate Keychain behavior in a signed app and the workflow becomes ridiculous.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;project-governance-bootstrap&lt;/code&gt; therefore starts by reading the repository:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;existing instructions&lt;/li&gt;
&lt;li&gt;architecture decisions&lt;/li&gt;
&lt;li&gt;manifests and dependencies&lt;/li&gt;
&lt;li&gt;tests&lt;/li&gt;
&lt;li&gt;CI&lt;/li&gt;
&lt;li&gt;security boundaries&lt;/li&gt;
&lt;li&gt;release process&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It then chooses one of three rough profiles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;light&lt;/strong&gt;: small, easy to roll back, and mainly one validation path;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;standard&lt;/strong&gt;: multiple modules, public boundaries, persistence, CI, or several test layers;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;critical&lt;/strong&gt;: credentials, authorization, destructive migration, untrusted input, local servers, media lifecycles, or production infrastructure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Line count is only a clue. One hundred thousand lines of generated code may be harmless; a short migration that deletes data is not.&lt;/p&gt;
&lt;p&gt;The result is still the project&apos;s own policy. The reusable part is the process used to produce it.&lt;/p&gt;
&lt;h2&gt;It ended up looking more like a compiler&lt;/h2&gt;
&lt;p&gt;I now think of &lt;code&gt;project-governance-bootstrap&lt;/code&gt; as a small compiler.&lt;/p&gt;
&lt;p&gt;Its input looks roughly like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;code
+ manifests
+ architecture documents
+ tests
+ CI
+ security boundaries
+ release rules
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Its output looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source-of-truth order
+ architectural boundaries
+ risk areas
+ validation entry points
+ authorization limits
+ optional reviewer roles
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are templates in the repository, but they are only starting points. If a section does not apply, it should disappear.&lt;/p&gt;
&lt;p&gt;Templates have a habit of keeping themselves alive. Once a generic &quot;Security&quot; section exists, both people and AI tend to fill it with text even when the project has no meaningful security boundary. The document becomes complete-looking and mostly useless.&lt;/p&gt;
&lt;p&gt;If the repository already has a command that proves the relevant result, there is no reason to add another gate script. If there is no special security boundary, a few paragraphs of generic security advice do not help.&lt;/p&gt;
&lt;p&gt;Enough is enough.&lt;/p&gt;
&lt;h2&gt;Why there are two Skills&lt;/h2&gt;
&lt;p&gt;The original BiliKit workflow mixed two questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What does this repository require?&lt;/li&gt;
&lt;li&gt;How should this Apple-platform change be validated?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;They often appear together in BiliKit, but I split them when making the workflow reusable.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;project-governance-bootstrap&lt;/code&gt; owns the repository rules. It may decide that a project needs signed-app evidence, UI testing, or a performance recording, but it does not contain every Xcode procedure.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;apple-dev-loop&lt;/code&gt; owns the actual validation. It knows when to use SwiftPM, &lt;code&gt;xcodebuild&lt;/code&gt;, &lt;code&gt;.xcresult&lt;/code&gt;, XCUI, a signed app, Computer Use, or Instruments. It does not decide the application&apos;s architecture or risk policy.&lt;/p&gt;
&lt;p&gt;They can be used together, but neither depends on the other. The governance Skill can also work on Rust, TypeScript, or documentation repositories. The Apple Skill can work in a Swift project that already has its own engineering rules.&lt;/p&gt;
&lt;h2&gt;A Skill also needs to know when to stay out&lt;/h2&gt;
&lt;p&gt;While writing the Skills, I found that the trigger description needed as much care as the workflow itself.&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;apple-dev-loop&lt;/code&gt; ran whenever a request mentioned Swift, a syntax question could suddenly turn into Xcode preflight, scheme discovery, and a full build. Thorough, perhaps, but also completely unnecessary.&lt;/p&gt;
&lt;p&gt;Its trigger therefore says when the full loop applies and when it does not. It is useful when a task really needs the Apple toolchain, signing, a device, UI validation, or performance evidence. A source-only Swift explanation or a narrow package edit usually does not.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;project-governance-bootstrap&lt;/code&gt; has a similar boundary. The existence of an &lt;code&gt;AGENTS.md&lt;/code&gt; file is not, by itself, a request to redesign the repository&apos;s governance.&lt;/p&gt;
&lt;p&gt;I used to think extra instructions were mostly harmless. In practice, they consume context and change tool choices. Detailed Instruments instructions make Instruments look tempting even in a simple package-test task.&lt;/p&gt;
&lt;p&gt;More context can create more work.&lt;/p&gt;
&lt;h2&gt;Keeping SKILL.md small&lt;/h2&gt;
&lt;p&gt;The first design almost became two enormous &lt;code&gt;SKILL.md&lt;/code&gt; files.&lt;/p&gt;
&lt;p&gt;The governance Skill would have contained every risk category, template, reviewer role, and Apple exception. The Apple Skill would have contained every Xcode, XCTest, signing, simulator, UI, and Instruments recipe.&lt;/p&gt;
&lt;p&gt;Now each Skill keeps a short core workflow and reads supporting material only when it is needed.&lt;/p&gt;
&lt;p&gt;An ordinary repository never loads the Apple profile. A task that does not need independent roles never loads the agent-routing guide. A package test does not load Instruments instructions in advance.&lt;/p&gt;
&lt;p&gt;This is usually called progressive disclosure. I think of it as dependency management for context.&lt;/p&gt;
&lt;p&gt;The details of a tool make it more likely to be selected. Loading less is useful when the omitted material cannot help with the current decision anyway.&lt;/p&gt;
&lt;h2&gt;How far should Apple validation go?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;apple-dev-loop&lt;/code&gt; uses an evidence ladder:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;source inspection and static constraints&lt;/li&gt;
&lt;li&gt;SwiftPM, unit, and integration tests&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xcodebuild&lt;/code&gt; with a structured &lt;code&gt;.xcresult&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Xcode-aware diagnostics and actions&lt;/li&gt;
&lt;li&gt;deterministic XCUI tests&lt;/li&gt;
&lt;li&gt;a signed app and real UI interaction&lt;/li&gt;
&lt;li&gt;targeted &lt;code&gt;xctrace&lt;/code&gt; or Instruments recording&lt;/li&gt;
&lt;li&gt;CI, device matrices, and independent review&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The point is not to reach the last step every time. It should stop at the first level that can prove the current claim.&lt;/p&gt;
&lt;p&gt;A screenshot cannot replace a unit test. An unsigned build cannot prove Keychain access. Launching the app does not prove a lifetime issue is fixed. A trace from one Mac does not represent every supported device.&lt;/p&gt;
&lt;p&gt;The Skill also includes a few small scripts. One records the repository, workspace, scheme, Xcode, and Developer Directory before an expensive run. Another reads structured results from &lt;code&gt;.xcresult&lt;/code&gt; instead of guessing from thousands of lines of console output.&lt;/p&gt;
&lt;p&gt;I did not try to rebuild Xcode in shell. The scripts only cover bounded steps where automation removes ambiguity.&lt;/p&gt;
&lt;h2&gt;Even the installer needs guardrails&lt;/h2&gt;
&lt;p&gt;The repository has a script that installs the Skills as symbolic links so Codex can discover them.&lt;/p&gt;
&lt;p&gt;If the destination already points to the correct location, the script does nothing. If it is another link or a real directory, the script refuses to overwrite it.&lt;/p&gt;
&lt;p&gt;It is a small detail, but an installer that silently replaces an existing Skill is an easy way to lose data. The same applies to changing global &lt;code&gt;xcode-select&lt;/code&gt; just to make one build pass: it may fix the current project and leave every other Xcode project with a surprise.&lt;/p&gt;
&lt;p&gt;The Skills prefer configuration scoped to the current task. Ambiguous state causes a stop, and a missing optional tool is only a blocker when the current claim actually requires it.&lt;/p&gt;
&lt;h2&gt;Vague rules became harder to keep&lt;/h2&gt;
&lt;p&gt;Inside BiliKit, a sentence such as this felt clear enough:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Important changes require independent review.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In a reusable Skill, it immediately raises more questions. What counts as important? Does every multi-file edit need review? What should the reviewer know? What happens when two reviews disagree?&lt;/p&gt;
&lt;p&gt;The rule had to become narrower.&lt;/p&gt;
&lt;p&gt;Green work does not need a review ceremony. Non-mechanical yellow work may get an independent read-only review. Red work focuses review on failure paths, cancellation, ownership, security, cleanup, and rollback. It also gets a complexity budget, because &quot;being rigorous&quot; can otherwise keep adding process forever.&lt;/p&gt;
&lt;p&gt;Other familiar instructions had the same problem.&lt;/p&gt;
&lt;p&gt;&quot;Run the full test suite&quot; is wrong when the highest deterministic mode already includes the lower modes. &quot;Test on a real device&quot; only makes sense when deterministic tests cannot prove the claim. &quot;Use Xcode MCP&quot; still requires checking which Xcode process, window, workspace, and scheme it is connected to.&lt;/p&gt;
&lt;p&gt;Rules that survived through project context needed explicit conditions once they became reusable.&lt;/p&gt;
&lt;h2&gt;The Skills need tests too&lt;/h2&gt;
&lt;p&gt;The repository has a validator for metadata, internal links, unresolved placeholders, shell syntax, and formatting. The link installer has separate conflict checks.&lt;/p&gt;
&lt;p&gt;Those checks only prove that the files are structurally valid.&lt;/p&gt;
&lt;p&gt;A Skill can have clean metadata and valid shell while still making poor decisions. It can trigger too broadly, produce rules that are too heavy, ignore existing project constraints, or recommend evidence that does not answer the question.&lt;/p&gt;
&lt;p&gt;The useful test is trying it from a fresh context on different repositories.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;project-governance-bootstrap&lt;/code&gt;, I want to try a small reversible tool, a normal multi-module application, and a project with real security or lifecycle risk. The generated rules should be different.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;apple-dev-loop&lt;/code&gt;, I want to see whether it stops in the right place: package tests for a package change, &lt;code&gt;xcodebuild&lt;/code&gt; for an Xcode project, a signed app for Keychain, and Instruments only for an actual performance question.&lt;/p&gt;
&lt;p&gt;If the result is still BiliKit&apos;s rules with the project name replaced, I will have to go back and change it.&lt;/p&gt;
</content:encoded></item><item><title>AI Made Code Cheap. It Made Engineering More Expensive.</title><link>https://www.shiinayane.com/en/posts/ai-made-code-cheap/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/ai-made-code-cheap/</guid><description>BiliKit reached 21,300 lines of Swift before v1. Here is why my original prompt-and-review workflow stopped working, and what I changed.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;BiliKit has not reached version 1 yet, but it already contains 21,300 lines of Swift.&lt;/p&gt;
&lt;p&gt;That number includes about 12,700 lines of production code and 8,400 lines of tests across 125 Swift files. The app can browse and search Bilibili, log in through a web QR flow, play DASH video with AVPlayer, show subtitles, schedule danmaku on the playback timeline, and keep the renderer running under sustained load.&lt;/p&gt;
&lt;p&gt;It still needs a proper UI pass, more Mac-specific work, signing, notarization, release engineering, and the final v1 regression matrix.&lt;/p&gt;
&lt;p&gt;This is also the first project where my original AI workflow stopped being enough.&lt;/p&gt;
&lt;p&gt;In the beginning, I would describe a feature, let the model inspect the repository, ask it to implement the change, run the tests, and then read the diff. This worked quite well while the repository was small. Even when the model put something in the wrong place, the change was usually small enough to understand and repair.&lt;/p&gt;
&lt;p&gt;As BiliKit grew, the failures became harder to spot.&lt;/p&gt;
&lt;h2&gt;The code usually compiled&lt;/h2&gt;
&lt;p&gt;The annoying part was that AI rarely failed by producing obviously broken Swift.&lt;/p&gt;
&lt;p&gt;Playback alone crosses networking, HTTP byte ranges, SIDX parsing, generated HLS playlists, a loopback HTTP server, AVPlayer lifecycle, cancellation, seeking, and CDN fallback. Authentication involves a remote state machine, ephemeral URL sessions, redirect policy, Keychain storage, request authorization, logout cleanup, and UI state. Danmaku goes through protobuf decoding, segment prefetch, deduplication, a shared media timeline, lane allocation, Core Animation, and object lifetime.&lt;/p&gt;
&lt;p&gt;A change could look perfectly reasonable inside one file and still be wrong for the whole path.&lt;/p&gt;
&lt;p&gt;Some actual failure modes were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a type living in the most convenient module instead of the module that owned its meaning;&lt;/li&gt;
&lt;li&gt;a test passing because one &lt;code&gt;Task.yield()&lt;/code&gt; happened to be enough on that run;&lt;/li&gt;
&lt;li&gt;cancellation stopping the UI update while leaving the underlying resource alive;&lt;/li&gt;
&lt;li&gt;an anonymous media request inheriting authenticated headers;&lt;/li&gt;
&lt;li&gt;a benchmark harness growing into a small framework before answering the original question;&lt;/li&gt;
&lt;li&gt;a document describing a planned capability as if it already existed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of these survive a quick diff review. Some even survive the full test suite.&lt;/p&gt;
&lt;p&gt;My first reaction was to write longer prompts. That helped for a while, but every new conversation needed the same background again: product scope, architecture, old decisions, risky areas, and how to verify the change. Important details were mixed with whatever I happened to be working on that day.&lt;/p&gt;
&lt;p&gt;At some point, adding more text to the prompt became a pretty bad way to maintain the project.&lt;/p&gt;
&lt;h2&gt;Moving the context into the repository&lt;/h2&gt;
&lt;p&gt;BiliKit gradually accumulated a roadmap, Architecture Decision Records, threat models, dated validation records, and an engineering guide.&lt;/p&gt;
&lt;p&gt;These files do different jobs.&lt;/p&gt;
&lt;p&gt;The current code and build configuration describe what exists now. Accepted decisions record boundaries that should not be reopened casually. The roadmap separates current work from later ideas. Validation records preserve what happened in a particular environment, including results that may become outdated.&lt;/p&gt;
&lt;p&gt;This distinction matters because the project has plenty of attractive distractions. Downloads, transcoding, live streaming, multiple accounts, and region unlocking can all produce a convincing implementation plan in a few minutes. They are still outside the v1 scope.&lt;/p&gt;
&lt;p&gt;I also started writing a small contract before non-trivial changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Goal:&lt;/strong&gt; the observable behavior that should change;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context:&lt;/strong&gt; relevant entry points, decisions, and known limitations;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constraints:&lt;/strong&gt; dependency direction, security, cancellation, lifetime, and scope;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Done when:&lt;/strong&gt; the test, probe, measurement, or visible behavior that closes the task.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If I cannot describe the goal in one sentence, the task is usually carrying more than one change. If “done when” only says that tests pass, I probably have not identified the risky behavior yet.&lt;/p&gt;
&lt;p&gt;This made new conversations much easier to start. They no longer needed the entire history of the previous conversation. They could read the contract and the files that already belonged to the project.&lt;/p&gt;
&lt;h2&gt;Building one real path at a time&lt;/h2&gt;
&lt;p&gt;AI is very good at filling an empty architecture.&lt;/p&gt;
&lt;p&gt;It can generate all domain models, all repository protocols, all use cases, and all views in one pass. The result often looks impressively organized. It may also contain protocols with no real caller, placeholder targets, and abstractions based on features that do not exist.&lt;/p&gt;
&lt;p&gt;BiliKit now prefers vertical slices.&lt;/p&gt;
&lt;p&gt;The architecture is still layered:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Scene → View → ViewModel → Use Case → Repository Port → Adapter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Implementation moves through those layers for one observable behavior.&lt;/p&gt;
&lt;p&gt;Authentication started with the QR state, credential ownership, one authorized endpoint, one UI flow, restart recovery, and logout. Danmaku started with one media timeline, one segment contract, bounded scheduling, and then a renderer connected through the real playback chain.&lt;/p&gt;
&lt;p&gt;This is slower than asking AI to scaffold an entire subsystem in one shot, but there is much less empty code to clean up later.&lt;/p&gt;
&lt;h2&gt;Five changed lines can still be red&lt;/h2&gt;
&lt;p&gt;I used to associate a small diff with a small risk. BiliKit cured me of that fairly quickly.&lt;/p&gt;
&lt;p&gt;Five lines in redirect handling can expose credentials. A small task-ownership change can introduce a leak or stale-result race. A short persistence migration can destroy user data. Meanwhile, a large generated protobuf file can be almost entirely mechanical.&lt;/p&gt;
&lt;p&gt;The repository now classifies work by failure mode:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Green:&lt;/strong&gt; local presentation changes and narrow mechanical edits;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Yellow:&lt;/strong&gt; ordinary features, use cases, cross-file refactors, and public APIs;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Red:&lt;/strong&gt; authentication, Keychain, media, redirects, local servers, concurrency, resource lifetime, renderers, migrations, deletion, and irreversible changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Green work gets the baseline checks and a focused diff review. Yellow work gets a written contract and usually a fresh read-only review. Red work needs a decision gate, a bounded experiment when the route is uncertain, measurements that match the risk, and explicit approval before a risky experiment becomes production code.&lt;/p&gt;
&lt;p&gt;The classification actually reduced the amount of process. A CSS change does not need the same ceremony as Keychain or AVPlayer lifecycle work.&lt;/p&gt;
&lt;p&gt;I also started giving uncertain experiments a complexity budget. A renderer comparison does not need ten candidates, five output formats, and a reusable benchmark framework. It needs enough candidates and measurements to answer the product question.&lt;/p&gt;
&lt;p&gt;The danmaku renderer was developed this way. The comparison lived on a non-mergeable spike branch and used synthetic load to choose a route. Once it had answered that question, production implementation started separately. The benchmark did not get promoted just because it already worked.&lt;/p&gt;
&lt;h2&gt;Tests only prove what they observe&lt;/h2&gt;
&lt;p&gt;One subtitle test taught me this in a very direct way.&lt;/p&gt;
&lt;p&gt;The test called &lt;code&gt;Task.yield()&lt;/code&gt; once and assumed the asynchronous stream would publish observable state. It usually passed. The race appeared only when the unified project gate ran under a different timing profile.&lt;/p&gt;
&lt;p&gt;Rerunning it until green would have hidden the problem again. The test had to wait for the state it actually cared about.&lt;/p&gt;
&lt;p&gt;BiliKit now has one reproducible entry point for static checks, package tests, and the full app build. Risky paths add their own evidence:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;deterministic fixtures for protocol behavior;&lt;/li&gt;
&lt;li&gt;negative tests for source and redirect policy;&lt;/li&gt;
&lt;li&gt;a signed smoke test for Keychain;&lt;/li&gt;
&lt;li&gt;real AVPlayer probes for playback;&lt;/li&gt;
&lt;li&gt;controlled high-density runs for danmaku;&lt;/li&gt;
&lt;li&gt;longer measurements for memory and cleanup;&lt;/li&gt;
&lt;li&gt;manual checks for UI behavior that automation cannot judge usefully.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An unsigned build cannot prove that Keychain works. A screenshot cannot prove cancellation. A successful launch says very little about a renderer after 30 minutes.&lt;/p&gt;
&lt;p&gt;Running every available tool is also pointless. A Swift Package change may be finished after package tests. Instruments belongs to an actual performance question.&lt;/p&gt;
&lt;h2&gt;The workflow I ended up with&lt;/h2&gt;
&lt;p&gt;For ordinary BiliKit work, the loop now looks roughly like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read the current code, roadmap, relevant decisions, and tests.&lt;/li&gt;
&lt;li&gt;Write one observable goal and decide what would prove it.&lt;/li&gt;
&lt;li&gt;Classify the task by the cost of failure.&lt;/li&gt;
&lt;li&gt;Set a complexity budget if the implementation route is uncertain.&lt;/li&gt;
&lt;li&gt;Build the smallest vertical slice.&lt;/li&gt;
&lt;li&gt;Use a fresh, read-only context for meaningful reviews.&lt;/li&gt;
&lt;li&gt;Run the shared automated gate.&lt;/li&gt;
&lt;li&gt;Add the environment-specific evidence required by that change.&lt;/li&gt;
&lt;li&gt;Update current documentation without rewriting old validation history.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This would be ridiculous for a weekend script. BiliKit has authentication, playback, a local server, concurrent state, and a renderer, so the simple prompt-and-review loop stopped scaling much earlier than I expected.&lt;/p&gt;
&lt;p&gt;The project is still before v1. The workflow will probably change again before release.&lt;/p&gt;
&lt;p&gt;For now, at least, a new conversation no longer has to remember the whole project by itself.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;I later extracted part of this workflow into reusable Codex Skills. The follow-up, &lt;a href=&quot;/en/posts/i-turned-my-ai-workflow-into-code/&quot;&gt;I Turned My AI Workflow into Code&lt;/a&gt;, covers that process.&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>What&apos;s New in Swift — WWDC26 Notes</title><link>https://www.shiinayane.com/en/posts/whats-new-in-swift-wwdc26/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/whats-new-in-swift-wwdc26/</guid><description>My notes from WWDC26&apos;s What&apos;s New in Swift session, covering Swift 6.3 and 6.4.</description><pubDate>Thu, 11 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I watched this year&apos;s &lt;em&gt;What&apos;s New in Swift&lt;/em&gt; session and wrote down the parts I will probably need later. Becca and Evan covered Swift 6.3 and 6.4, with quite a lot packed into one session: small syntax changes, library additions, Android and Wasm, and more ownership work.&lt;/p&gt;
&lt;p&gt;This is a reference note, so I have kept the examples instead of trying to turn everything into one big takeaway.&lt;/p&gt;
&lt;h2&gt;Everyday Swift&lt;/h2&gt;
&lt;h3&gt;Optional parentheses around &lt;code&gt;some&lt;/code&gt; and &lt;code&gt;any&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;An optional &lt;code&gt;some&lt;/code&gt; or &lt;code&gt;any&lt;/code&gt; type no longer needs another pair of parentheses:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Before
func delegate() -&amp;gt; (any Renderer)?

// After
func delegate() -&amp;gt; any Renderer?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Small change, but definitely easier to read.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;weak let&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;A weak reference previously had to be a &lt;code&gt;var&lt;/code&gt;. That was awkward for &lt;code&gt;Sendable&lt;/code&gt; types and sometimes led to &lt;code&gt;@unchecked Sendable&lt;/code&gt; just to get around it.&lt;/p&gt;
&lt;p&gt;Swift 6.4 allows immutable weak references to use &lt;code&gt;weak let&lt;/code&gt;, so this case can express real &lt;code&gt;Sendable&lt;/code&gt; conformance without the escape hatch.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;~Sendable&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;~Sendable&lt;/code&gt; explicitly marks a type as non-&lt;code&gt;Sendable&lt;/code&gt; without preventing its subclasses from becoming &lt;code&gt;Sendable&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Task errors now get a warning&lt;/h3&gt;
&lt;p&gt;Ignoring an error thrown from a &lt;code&gt;Task&lt;/code&gt; now produces a warning. Silently losing an error from an unstructured task is very easy to miss, so I am glad the compiler finally points it out.&lt;/p&gt;
&lt;h3&gt;Async work in &lt;code&gt;defer&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;An &lt;code&gt;async&lt;/code&gt; function can now be called from a &lt;code&gt;defer&lt;/code&gt; block.&lt;/p&gt;
&lt;h3&gt;Two memberwise initializers&lt;/h3&gt;
&lt;p&gt;For a struct containing both &lt;code&gt;internal&lt;/code&gt; and &lt;code&gt;private&lt;/code&gt; stored properties, Swift can synthesize two memberwise initializers at the corresponding access levels. There is no need to write one manually just because the properties have mixed visibility.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;@diagnose&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@diagnose&lt;/code&gt; controls one diagnostic on one declaration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@diagnose(DeprecatedDeclaration, as: ignored, reason: &quot;Flying with surplus hardware&quot;)
func makeApolloSoyuzMission() -&amp;gt; Mission { ... }

@diagnose(StrictMemorySafety, as: warning)
func uplinkCommand(from receiver: inout Receiver, to computer: inout Computer) { ... }

@diagnose(ErrorInFutureSwiftVersion, as: error)
func fetchPosition() -&amp;gt; (x: Double, y: Double, z: Double) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is probably my favorite small addition in the session. A special call site no longer requires changing the warning level for the whole project, and &lt;code&gt;reason:&lt;/code&gt; leaves an explanation next to the exception.&lt;/p&gt;
&lt;h3&gt;Module selectors with &lt;code&gt;::&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Swift 6.3 added &lt;code&gt;::&lt;/code&gt; for resolving symbols with the same name in different modules:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Rocket
import GiftShopToys

let r1 = SaturnV()          // ambiguous
let r2 = Rocket::SaturnV()  // the type from Rocket
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also works on members. For example, &lt;code&gt;technician.HumanResources::fire()&lt;/code&gt; selects the member from a particular module.&lt;/p&gt;
&lt;h2&gt;Library updates&lt;/h2&gt;
&lt;h3&gt;Standard library&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;withTaskCancellationShield { ... }&lt;/code&gt; runs a critical section even when the surrounding task is cancelled. The session uses sending a final SOS packet as an example.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Dictionary.mapKeyedValues&lt;/code&gt; transforms values while also giving the closure access to each key:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;missions.mapKeyedValues { mission, window in
    makeDisplayName(for: mission, in: window)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;FilePath&lt;/code&gt; is a cross-platform path type with structured components:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var path: FilePath = &quot;/var/www/static&quot;
path.components.append(&quot;WWDC&quot;)
// [ &quot;var&quot;, &quot;www&quot;, &quot;static&quot;, &quot;WWDC&quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Swift Testing&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Issue.record(..., severity: .warning)&lt;/code&gt; records a warning without failing the test.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;try Test.cancel(&quot;reason&quot;)&lt;/code&gt; stops a parameterized case when it does not apply.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;swift test&lt;/code&gt; can repeat a test until it passes or fails, with a configurable maximum. This should be useful when tracking down a flaky test.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#expect&lt;/code&gt; now works inside &lt;code&gt;XCTestCase&lt;/code&gt;, while XCTest assertion failures appear as Swift Testing issues. Existing XCTest suites can therefore migrate gradually.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Subprocess 1.0&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;Subprocess&lt;/code&gt; package reached 1.0. It has a cleaner API, improved error handling, and cross-platform line-by-line output streaming:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let result = try await Subprocess.run(
    .name(&quot;ls&quot;),
    input: .none,
    output: .sequence,
    error: .string(limit: 4096)
) { execution in
    execution.standardOutput.strings().filter { $0.hasSuffix(&quot;.obj&quot;) }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The package handles differences such as platform file descriptors and termination statuses.&lt;/p&gt;
&lt;h3&gt;Foundation &lt;code&gt;ProgressManager&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ProgressManager&lt;/code&gt; is a new progress API designed for &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt;. A parent assigns part of its total with &lt;code&gt;subprogress(assigningCount:)&lt;/code&gt;; the child reports its own local stages without needing to know the parent&apos;s total:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let manager = ProgressManager(totalCount: 100)
try await rocket.launch(manager.subprogress(assigningCount: 100))

extension Rocket {
    func launch(_ progress: consuming Subprogress? = nil) async throws {
        let stage = progress?.start(totalCount: 3)
        try await ignite();          stage?.complete(count: 1)
        try await liftoff();         stage?.complete(count: 1)
        try await stageSeparation(); stage?.complete(count: 1)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Foundation&apos;s move to pure Swift also continues. The session mentions faster &lt;code&gt;Data&lt;/code&gt; operations and bridging, plus one unified Swift implementation behind &lt;code&gt;NSURL&lt;/code&gt; and &lt;code&gt;CFURL&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Swift outside Apple platforms&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;anyAppleOS&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The new availability shortcut replaces a long list of Apple platforms:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Before
@available(macOS 27, iOS 27, watchOS 27, tvOS 27, visionOS 27, *)
func showStatus() { ... }

// After
@available(anyAppleOS 27, *)
func showStatus() { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;anyAppleOS&lt;/code&gt; also works in &lt;code&gt;#if os(anyAppleOS)&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;@C&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@C&lt;/code&gt; exposes a Swift function to C, and can also be used when Swift implements a C function. The signature must use C-compatible types such as integers, pointers, imported C structs, and raw-value enums:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@C
func averageLaunchWindowLength(_ windows: Span&amp;lt;LaunchWindow&amp;gt;) -&amp;gt; TimeInterval { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Java and Android&lt;/h3&gt;
&lt;p&gt;Java can call Swift functions that are &lt;code&gt;async&lt;/code&gt; or throwing, and Java classes can conform to Swift protocols. There is also an official Swift SDK for Android on swift.org.&lt;/p&gt;
&lt;h3&gt;WebAssembly&lt;/h3&gt;
&lt;p&gt;Swift compiles to Wasm with an open-source toolchain. JavaScriptKit&apos;s type-safe bridge is now 35–40 times faster than its dynamic path. Goodnotes was the example in the session: it moved Swift code from the native iOS app to the web through Wasm.&lt;/p&gt;
&lt;h3&gt;Embedded Swift&lt;/h3&gt;
&lt;p&gt;Embedded Swift now supports existential types, untyped &lt;code&gt;throws&lt;/code&gt;, and DWARF debug information for inspecting coredumps on constrained hardware. The new &lt;code&gt;EmbeddedRestrictions&lt;/code&gt; warning group reports features that will not work in an embedded environment.&lt;/p&gt;
&lt;h3&gt;Editor support&lt;/h3&gt;
&lt;p&gt;The Swift VS Code extension integrates Swiftly for toolchain management and is available through OpenVSX, which also makes it available in editors such as Cursor and VSCodium. It now includes a getting-started checklist as well.&lt;/p&gt;
&lt;h2&gt;Performance and ownership&lt;/h2&gt;
&lt;h3&gt;Optimizer hints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@inline(always)&lt;/code&gt; is now a supported attribute. Class methods should use it together with &lt;code&gt;final&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Swift 6.3&apos;s &lt;code&gt;@specialized&lt;/code&gt; can pre-specialize a generic function for a known hot type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@specialized(where Values == [UInt8])
func histogram&amp;lt;Values&amp;gt;(of values: Values) -&amp;gt; [256 of Int]
    where Values: Sequence&amp;lt;UInt8&amp;gt; { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Ownership in the standard library&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Equatable&lt;/code&gt;, &lt;code&gt;Comparable&lt;/code&gt;, and &lt;code&gt;Hashable&lt;/code&gt; now work with noncopyable types. &lt;code&gt;Equatable&lt;/code&gt; and &lt;code&gt;Comparable&lt;/code&gt; also work with non-escapable types, and associated types may be &lt;code&gt;~Copyable&lt;/code&gt; or &lt;code&gt;~Escapable&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The new &lt;code&gt;Iterable&lt;/code&gt; protocol borrows elements in a &lt;code&gt;for&lt;/code&gt; loop instead of copying them.&lt;/p&gt;
&lt;p&gt;Custom &lt;code&gt;borrow&lt;/code&gt; and &lt;code&gt;mutate&lt;/code&gt; accessors make it possible to expose these semantics from a container:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public struct UniqueBox&amp;lt;Value: ~Copyable&amp;gt;: ~Copyable {
    private let valuePointer: UnsafeMutablePointer&amp;lt;Value&amp;gt;

    public var value: Value {
        borrow { valuePointer.pointee }
        mutate { &amp;amp;valuePointer.pointee }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;New low-overhead types&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;UniqueBox&lt;/code&gt; and &lt;code&gt;UniqueArray&lt;/code&gt; provide noncopyable storage without reference-counting overhead.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Continuation&lt;/code&gt; checks single resumption at compile time, with the safety of &lt;code&gt;CheckedContinuation&lt;/code&gt; and the cost of &lt;code&gt;UnsafeContinuation&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Ref&lt;/code&gt; and &lt;code&gt;MutableRef&lt;/code&gt; provide safe borrowed references into a collection slot:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var countRef = MutableRef(&amp;amp;counts[key, default: 0])
countRef.value += 1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;withTemporaryAllocation&lt;/code&gt; now gives its closure an &lt;code&gt;OutputSpan&lt;/code&gt; instead of an &lt;code&gt;UnsafeMutableBufferPointer&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I will probably use &lt;code&gt;@diagnose&lt;/code&gt;, the ignored-Task-error warning, and the Swift Testing additions first. The ownership types are more interesting, but I need an actual project with the right problem before they become useful.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Full session: &lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2026/262/&quot;&gt;What&apos;s new in Swift — WWDC26&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Configuration Is Easy; Maintenance Is the Work</title><link>https://www.shiinayane.com/en/posts/maintenance/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/maintenance/</guid><description>A clean development environment drifts quietly. These small checks make that drift visible before cleanup becomes a project of its own.</description><pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every clean environment I have built has stayed clean for about six months. The last time I looked closely, I found three Python versions where I expected one, a Brewfile that no longer described the machine, and a &lt;code&gt;mise list&lt;/code&gt; full of runtimes installed for experiments I could no longer remember. Nothing was broken. The machine had simply drifted while I was using it.&lt;/p&gt;
&lt;p&gt;This is the last article in the series. The previous six covered configuration—layers, settings, and dotfiles—but that is the part I can finish in an afternoon. Whether the setup is still clean a year later depends on a much smaller, recurring job: noticing drift and removing what no longer belongs.&lt;/p&gt;
&lt;h2&gt;What drift looks like&lt;/h2&gt;
&lt;p&gt;I usually find one of three things.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Silent auto-install.&lt;/strong&gt; A tool provisions something without asking, as &lt;code&gt;uv&lt;/code&gt; did with its own Python in the &lt;a href=&quot;/posts/python/&quot;&gt;Python article&lt;/a&gt;. The machine gains state I did not choose, and there may be no warning at all, which makes this the easiest kind of drift to miss. Declarative settings prevent some of it: &lt;code&gt;python-preference = only-system&lt;/code&gt;, for example, turns that silent download into a visible error. They do not catch every tool, so I still need a way to inspect the result.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Forgotten sync.&lt;/strong&gt; I install something deliberately but forget to declare it. The Brewfile from the &lt;a href=&quot;/posts/apps/&quot;&gt;apps article&lt;/a&gt; then falls behind the actual machine. This rarely matters day to day; it matters during a migration, when every missing entry becomes “oh, I forgot I had that.” The fix is to reconcile the two states before I need the file for recovery.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Accumulated cruft.&lt;/strong&gt; A runtime, global tool, or package was installed intentionally, used once, and never removed. This is the hardest kind to clean up: each item is valid on its own, so nothing reports an error. Only the accumulated list reveals the problem.&lt;/p&gt;
&lt;h2&gt;A health check should only make drift visible&lt;/h2&gt;
&lt;p&gt;I keep a few shell functions for this. Earlier articles introduced &lt;code&gt;brewdiff&lt;/code&gt;, which compares the installed applications with the Brewfile, and &lt;code&gt;zhealth&lt;/code&gt;, which finds stray zsh files in the home directory. Python needs a similar check because it is particularly easy for several tools to acquire different interpreters without making the disagreement obvious:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 30-functions.zsh — surface Python version drift across tools
pyversions() {
  echo &quot;shell PATH   : $(command -v python)&quot;
  echo &quot;  reports    : $(python --version 2&amp;gt;&amp;amp;1)&quot;
  echo &quot;mise current : $(mise current python 2&amp;gt;/dev/null || echo &apos;—&apos;)&quot;
  echo &quot;uv would use : $(uv run python --version 2&amp;gt;&amp;amp;1)&quot;
  echo &quot;mise list    :&quot;
  mise list python 2&amp;gt;/dev/null | sed &apos;s/^/  /&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pyversions&lt;/code&gt; puts the shell&apos;s &lt;code&gt;python&lt;/code&gt;, &lt;code&gt;mise current&lt;/code&gt;, &lt;code&gt;uv run python&lt;/code&gt;, and the installed &lt;code&gt;mise&lt;/code&gt; versions in one view. If the first three agree, the Python layer is healthy. If they do not, some tool owns a Python I did not expect, and I can repeat the investigation from the Python article without first rediscovering all the commands.&lt;/p&gt;
&lt;p&gt;The same pattern covers the other layers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;compare &lt;code&gt;mise list&lt;/code&gt; with the &lt;code&gt;mise.toml&lt;/code&gt; files I actually use;&lt;/li&gt;
&lt;li&gt;compare the machine with the Brewfile through &lt;code&gt;brew bundle check&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;compare the contents of &lt;code&gt;$HOME&lt;/code&gt; with the expectation that it contains one zsh file.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These checks do not repair anything. That is intentional. A small reporting command is easy to trust and easy to run; it turns drift from a surprise into a decision.&lt;/p&gt;
&lt;h2&gt;The 30-day test&lt;/h2&gt;
&lt;p&gt;For me, a mature setup is one with very few tools that are no longer used. That makes maintenance mostly a matter of subtraction.&lt;/p&gt;
&lt;p&gt;For anything questionable, I ask whether I have actually used it in the last 30 days. A runtime, global tool, or application that has gone untouched for a month becomes a candidate for removal—not an automatic deletion, but something that now needs a reason to stay.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;mise&lt;/code&gt; runtimes, the pass is concrete:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ mise list                 # what&apos;s installed
$ mise uninstall python@3.11 # remove a version no project uses
$ mise prune                 # drop versions nothing references
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;/posts/polyglot/&quot;&gt;Ruby decision&lt;/a&gt; in the previous article applies the same test before installation. An old system Ruby is not, by itself, a reason to add a &lt;code&gt;mise&lt;/code&gt;-managed Ruby to a machine with no Ruby projects. Waiting until a project needs it avoids creating another runtime to audit and eventually remove.&lt;/p&gt;
&lt;p&gt;I do this as a periodic cleanup instead of waiting until I feel motivated. Installing a new tool feels productive; removing one feels like losing something or admitting the original installation was a mistake. Sunk cost makes leaving it alone even easier. A schedule is less dramatic: run the checks, review what has not been used for 30 days, and remove what I cannot justify.&lt;/p&gt;
&lt;p&gt;An empty line in &lt;code&gt;mise list&lt;/code&gt;, a short Brewfile, or a home directory with one dotfile is not missing configuration. Sometimes it is the desired result.&lt;/p&gt;
&lt;h2&gt;Do not turn maintenance into another system&lt;/h2&gt;
&lt;p&gt;The cleanup itself can drift into over-engineering. It would be easy to build a dashboard, scheduled jobs, and a collection of checks elaborate enough to require their own maintenance.&lt;/p&gt;
&lt;p&gt;That defeats the point. A &lt;code&gt;brewdiff&lt;/code&gt; that takes two seconds gets used. A monitoring system that needs attention does not. Reconciliation only remains useful while its cost is lower than the mess it prevents, so I keep these tools small and let them report rather than automate every decision.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;/posts/manifesto/&quot;&gt;manifesto&lt;/a&gt; started this series with four layers: System, Runtime version, Package manager, and Project dependencies. There is a fifth part that none of those layers can provide: occasionally looking at the machine, comparing it with the declared state, and bringing the two back together before something breaks.&lt;/p&gt;
&lt;p&gt;On a machine I have not checked recently, I start with &lt;code&gt;which python&lt;/code&gt; and &lt;code&gt;uv run python --version&lt;/code&gt;, then look through &lt;code&gt;mise list&lt;/code&gt; for anything untouched in a month. That is usually enough to show whether the environment I remember is still the one I have.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This is the final article in the Sovereign Tools series. The full set, in reading order, is on the &lt;a href=&quot;/series/sovereign-tools&quot;&gt;series index&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>One Architecture, Many Languages</title><link>https://www.shiinayane.com/en/posts/polyglot/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/polyglot/</guid><description>A practical reference for applying the same four-layer architecture to Node, Java, Swift, Rust, Go, and Ruby.</description><pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When I start a project in an unfamiliar ecosystem, I want a short answer to four questions: who installs the runtime, who manages packages, which lockfile goes into git, and which generated files stay out. This article collects those answers for Node, Java, Swift, Rust, Go, and Ruby. It is a reference to revisit one section at a time.&lt;/p&gt;
&lt;p&gt;The answers use the four layers introduced in the &lt;a href=&quot;/posts/manifesto/&quot;&gt;first article of this series&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;my-6&quot;&amp;gt;
&amp;lt;svg viewBox=&quot;0 0 600 330&quot; role=&quot;img&quot; aria-labelledby=&quot;diagram-layers-title-6&quot; style=&quot;width:100%;height:auto;color:inherit&quot;&amp;gt;
&amp;lt;title id=&quot;diagram-layers-title-6&quot;&amp;gt;The four-layer stack: System, Runtime version, Package manager, Project dependencies&amp;lt;/title&amp;gt;
&amp;lt;g font-family=&quot;ui-sans-serif, system-ui, sans-serif&quot;&amp;gt;
&amp;lt;rect x=&quot;10&quot; y=&quot;10&quot; width=&quot;580&quot; height=&quot;66&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.15&quot;/&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;38&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; fill=&quot;var(--primary)&quot;&amp;gt;Layer 3&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;58&quot; font-size=&quot;15&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&amp;gt;Project dependencies&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;570&quot; y=&quot;44&quot; font-size=&quot;13&quot; text-anchor=&quot;end&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;lockfiles in git&amp;lt;/text&amp;gt;
&amp;lt;rect x=&quot;10&quot; y=&quot;86&quot; width=&quot;580&quot; height=&quot;66&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.15&quot;/&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;114&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; fill=&quot;var(--primary)&quot;&amp;gt;Layer 2&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;134&quot; font-size=&quot;15&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&amp;gt;Package manager&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;570&quot; y=&quot;120&quot; font-size=&quot;13&quot; text-anchor=&quot;end&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;uv, pnpm, cargo&amp;lt;/text&amp;gt;
&amp;lt;rect x=&quot;10&quot; y=&quot;162&quot; width=&quot;580&quot; height=&quot;66&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.15&quot;/&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;190&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; fill=&quot;var(--primary)&quot;&amp;gt;Layer 1&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;210&quot; font-size=&quot;15&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&amp;gt;Runtime version&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;570&quot; y=&quot;196&quot; font-size=&quot;13&quot; text-anchor=&quot;end&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;mise, or a sovereign tool&amp;lt;/text&amp;gt;
&amp;lt;rect x=&quot;10&quot; y=&quot;238&quot; width=&quot;580&quot; height=&quot;66&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.08&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.2&quot;/&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;266&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; fill=&quot;var(--primary)&quot;&amp;gt;Layer 0&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;286&quot; font-size=&quot;15&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&amp;gt;System&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;570&quot; y=&quot;272&quot; font-size=&quot;13&quot; text-anchor=&quot;end&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;Homebrew + Xcode CLT&amp;lt;/text&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;/svg&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;For &lt;strong&gt;Layer 1 — Runtime version&lt;/strong&gt;, I follow the rule from the &lt;a href=&quot;/posts/sovereignty/&quot;&gt;sovereignty article&lt;/a&gt;: use the language&apos;s strong official tool when it has one; otherwise, let &lt;code&gt;mise&lt;/code&gt; do the job. The sections below focus on the resulting setup and the places where it usually goes wrong.&lt;/p&gt;
&lt;h2&gt;Node.js / TypeScript&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;mise&lt;/code&gt; owns the Node version because Node has no sovereign version manager. I use &lt;code&gt;pnpm&lt;/code&gt; for packages, but enable it through &lt;code&gt;corepack&lt;/code&gt; rather than adding a separate &lt;code&gt;brew install&lt;/code&gt;. This lets the &lt;code&gt;packageManager&lt;/code&gt; field in &lt;code&gt;package.json&lt;/code&gt; pin the &lt;code&gt;pnpm&lt;/code&gt; version for the project.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# mise.toml
[tools]
node = &quot;22&quot;

[settings]
# let corepack manage the pnpm version from package.json&apos;s packageManager field
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;corepack enable      # ships with Node; activates pnpm/yarn shims
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Avoid both &lt;code&gt;brew install node&lt;/code&gt; and &lt;code&gt;brew install pnpm&lt;/code&gt;. The first makes Homebrew the owner of Layer 1; the second bypasses the project&apos;s package-manager pin. Node does not need a separate virtual environment: &lt;code&gt;node_modules&lt;/code&gt; is already local to the project, providing the isolation for which Python uses &lt;code&gt;.venv&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For Node-based command-line tools, I normally use &lt;code&gt;pnpm dlx&lt;/code&gt; when I only need to run something once. If a tool must be installed, I keep a deliberate &lt;code&gt;PNPM_HOME&lt;/code&gt; on &lt;code&gt;PATH&lt;/code&gt; instead of accumulating an uncontrolled set of &lt;code&gt;npm install -g&lt;/code&gt; packages.&lt;/p&gt;
&lt;p&gt;Commit &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;, and ignore:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node_modules/
*.tsbuildinfo
.turbo/
dist/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Java&lt;/h2&gt;
&lt;p&gt;Java has no single sovereign version tool, and the JDK itself comes in several distributions. I let &lt;code&gt;mise&lt;/code&gt; install Temurin, a neutral and well-maintained default:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# mise.toml
[tools]
java = &quot;temurin-21&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check that &lt;code&gt;mise&lt;/code&gt; is setting &lt;code&gt;JAVA_HOME&lt;/code&gt;, not merely putting &lt;code&gt;java&lt;/code&gt; on &lt;code&gt;PATH&lt;/code&gt;. Many Java tools read &lt;code&gt;JAVA_HOME&lt;/code&gt; directly, so an old value can silently override the JDK you meant to use. &lt;code&gt;mise where java&lt;/code&gt; and &lt;code&gt;echo $JAVA_HOME&lt;/code&gt; should point to the same installation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mise where java
echo $JAVA_HOME
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the build tool, use the wrapper committed by the project—&lt;code&gt;./gradlew&lt;/code&gt; or &lt;code&gt;./mvnw&lt;/code&gt;—rather than a global Gradle or Maven. The wrapper pins the build-tool version in the repository, applying the same Layer 3 discipline beyond ordinary dependencies.&lt;/p&gt;
&lt;p&gt;Android is a separate case. Android Studio brings its own JDK and SDK, so I let it own that environment just as Xcode owns Swift.&lt;/p&gt;
&lt;p&gt;Gradle and Maven declare dependencies in their build files. Commit those files and the wrapper, then ignore the build output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.gradle/
build/
target/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Swift / iOS&lt;/h2&gt;
&lt;p&gt;Xcode is Swift&apos;s sovereign tool on macOS. It covers &lt;strong&gt;Layer 0 — System&lt;/strong&gt; and &lt;strong&gt;Layer 1 — Runtime version&lt;/strong&gt; together: toolchain, SDK, and build system all arrive as one application. I install it from the Mac App Store, which is why the &lt;a href=&quot;/posts/apps/&quot;&gt;apps article&lt;/a&gt; routes Xcode through &lt;code&gt;mas&lt;/code&gt;, and prefer Swift Package Manager for dependencies.&lt;/p&gt;
&lt;p&gt;Do not &lt;code&gt;brew install swift&lt;/code&gt; on macOS. It creates a second claimant for the toolchain while Xcode still wins in less obvious parts of the build. CocoaPods also adds a Ruby dependency that most new projects no longer need; when SPM supports the packages involved, it keeps dependency management inside the Apple toolchain.&lt;/p&gt;
&lt;p&gt;One of my projects has a boundary that has worked particularly well: KotobaLab is written in Swift, while its companion DictionaryBuilder is written in Python. They do not share a runtime, package manager, or build system. DictionaryBuilder writes a SQLite file and KotobaLab reads it. That neutral data interface lets both toolchains remain independent instead of making either ecosystem reach inside the other.&lt;/p&gt;
&lt;p&gt;Commit &lt;code&gt;Package.resolved&lt;/code&gt;, SPM&apos;s lockfile. Ignore the per-user and build artifacts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xcuserdata/
DerivedData/
.build/
*.xcuserstate
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Rust&lt;/h2&gt;
&lt;p&gt;Rust has the cleanest ownership model in this group. &lt;code&gt;rustup&lt;/code&gt; owns Layer 1, while &lt;code&gt;cargo&lt;/code&gt;, shipped with it, handles Layer 2 as well as building, testing, dependency management, and publishing.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# rustup installs the toolchain; cargo comes with it
rustup default stable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If a polyglot repository already has a &lt;code&gt;mise.toml&lt;/code&gt;, it can instead contain &lt;code&gt;rust = &quot;1.78&quot;&lt;/code&gt; and let &lt;code&gt;mise&lt;/code&gt; proxy to &lt;code&gt;rustup&lt;/code&gt;. This is the &lt;strong&gt;mise as proxy&lt;/strong&gt; arrangement described in the &lt;a href=&quot;/posts/sovereignty/&quot;&gt;sovereignty article&lt;/a&gt;. Both setups work; do not use both to manage the same project&apos;s Rust version.&lt;/p&gt;
&lt;p&gt;Cargo already keeps dependencies per project, so Rust has no need for a virtual environment. For global command-line tools written in Rust, such as &lt;code&gt;ripgrep&lt;/code&gt;, &lt;code&gt;fd&lt;/code&gt;, and &lt;code&gt;bat&lt;/code&gt;, I prefer the &lt;code&gt;brew&lt;/code&gt; formula. It provides a prebuilt binary, whereas &lt;code&gt;cargo install&lt;/code&gt; compiles the tool from source. I reserve &lt;code&gt;cargo install&lt;/code&gt; for tools that have not been packaged yet.&lt;/p&gt;
&lt;p&gt;Commit &lt;code&gt;Cargo.lock&lt;/code&gt; for applications and binaries. The long-standing convention for libraries is to omit it so downstream users resolve their own versions. Ignore:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/target/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Go&lt;/h2&gt;
&lt;p&gt;Go&apos;s official &lt;code&gt;dl&lt;/code&gt; installer is not a strong version manager, so I am comfortable letting &lt;code&gt;mise&lt;/code&gt; own the Go version:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# mise.toml
[tools]
go = &quot;1.23&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Modern Go uses modules, so projects no longer need the old &lt;code&gt;GOPATH&lt;/code&gt; workspace layout and can live anywhere on disk. I do set &lt;code&gt;GOBIN&lt;/code&gt;, however, so &lt;code&gt;go install&lt;/code&gt; writes executables to a directory I control and already have on &lt;code&gt;PATH&lt;/code&gt;. I use &lt;code&gt;~/.local/bin&lt;/code&gt;, the same location as personal scripts in the &lt;a href=&quot;/posts/python/&quot;&gt;Python article&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 00-env.zsh
export GOBIN=&quot;$HOME/.local/bin&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Commit both &lt;code&gt;go.mod&lt;/code&gt;, which declares dependencies, and &lt;code&gt;go.sum&lt;/code&gt;, which pins their checksums. Go leaves few incidental files in a repository; ignore compiled binaries by name or put them in an ignored output directory.&lt;/p&gt;
&lt;h2&gt;Ruby&lt;/h2&gt;
&lt;p&gt;Ruby has no sovereign version tool, so &lt;code&gt;mise&lt;/code&gt; can own Layer 1. I only install it when a project actually requires Ruby.&lt;/p&gt;
&lt;p&gt;Never use the system Ruby included with macOS for project dependencies. It is old, Apple discourages modifying it, and &lt;code&gt;sudo gem install&lt;/code&gt; against it is a reliable way to damage the system environment.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# mise.toml — only when a project genuinely needs it
[tools]
ruby = &quot;3.3&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CocoaPods is still a common reason for macOS developers to install Ruby, but SPM has removed that requirement from many Swift projects. If SPM covers the project, Ruby can stay uninstalled. This follows the on-demand approach from the &lt;a href=&quot;/posts/maintenance/&quot;&gt;maintenance article&lt;/a&gt;: the presence of an old system Ruby does not require adding a &lt;code&gt;mise&lt;/code&gt;-managed Ruby to a machine with no Ruby projects.&lt;/p&gt;
&lt;p&gt;When Ruby is necessary, commit &lt;code&gt;Gemfile.lock&lt;/code&gt;. If Bundler uses a local path such as &lt;code&gt;vendor/bundle/&lt;/code&gt;, ignore that directory.&lt;/p&gt;
&lt;h2&gt;What stays consistent across languages&lt;/h2&gt;
&lt;p&gt;A repository should declare its runtime rather than relying on a contributor&apos;s global default. Check in &lt;code&gt;mise.toml&lt;/code&gt; or &lt;code&gt;.tool-versions&lt;/code&gt;, and a fresh clone can provision the required versions with &lt;code&gt;mise install&lt;/code&gt;. Global defaults are still useful for scratch work.&lt;/p&gt;
&lt;p&gt;Lockfiles belong in git; derived artifacts do not. That means committing &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;, &lt;code&gt;Cargo.lock&lt;/code&gt;, &lt;code&gt;go.sum&lt;/code&gt;, &lt;code&gt;Package.resolved&lt;/code&gt;, &lt;code&gt;uv.lock&lt;/code&gt;, and &lt;code&gt;Gemfile.lock&lt;/code&gt;, while keeping &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;target/&lt;/code&gt;, &lt;code&gt;DerivedData/&lt;/code&gt;, &lt;code&gt;.venv&lt;/code&gt;, and similar output in &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;One &lt;code&gt;mise.toml&lt;/code&gt; can list &lt;code&gt;node&lt;/code&gt;, &lt;code&gt;python&lt;/code&gt;, and &lt;code&gt;go&lt;/code&gt; together. A polyglot project does not need a separate runtime configuration for every language, and the whole toolset can be provisioned with one command. This unified interface remains useful even when &lt;code&gt;mise&lt;/code&gt; delegates to a sovereign tool.&lt;/p&gt;
&lt;p&gt;The same fact should not be declared twice, but superficially similar declarations may have different jobs. In Python, &lt;code&gt;requires-python&lt;/code&gt; in &lt;code&gt;pyproject.toml&lt;/code&gt; describes the range supported by the code. &lt;code&gt;mise.toml&lt;/code&gt; selects the one version used on the current machine. Keeping both is not duplication; collapsing them would lose information. Actual duplication creates two sources of truth, which is the failure the &lt;a href=&quot;/posts/manifesto/&quot;&gt;first article&lt;/a&gt; set out to avoid.&lt;/p&gt;
&lt;p&gt;There is also a hard limit to this model. C and C++ have no comparable owner for “the C version.” System compilers, SDKs, and multiple build systems blur the layers. I do not try to force these toolchains into the same shape. When C or C++ appears as a native extension or build dependency, I let the higher-level tool that introduced it handle the requirement.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;/posts/maintenance/&quot;&gt;final article in the series&lt;/a&gt; covers what happens after this setup: keeping the environment usable without turning maintenance into another project.&lt;/p&gt;
</content:encoded></item><item><title>When to Let the Official Tool Win</title><link>https://www.shiinayane.com/en/posts/sovereignty/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/sovereignty/</guid><description>Don&apos;t put every language under mise. One test decides who owns runtime versions: does the language ship a strong, official tool of its own?</description><pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The &lt;a href=&quot;/posts/python/&quot;&gt;previous article&lt;/a&gt; made &lt;code&gt;mise&lt;/code&gt; the single owner of Python versions and kept &lt;code&gt;uv&lt;/code&gt; at the dependency layer. On the same machine, though, I let &lt;code&gt;rustup&lt;/code&gt; own Rust even though &lt;code&gt;mise&lt;/code&gt; can install Rust too.&lt;/p&gt;
&lt;p&gt;The difference is not a special exception for Rust. Before assigning Layer 1—the runtime version—I ask one question: &lt;strong&gt;does this language have a strong, official sovereign tool?&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;What counts as a sovereign tool&lt;/h2&gt;
&lt;p&gt;Here, a &lt;strong&gt;sovereign tool&lt;/strong&gt; means an official version and toolchain manager that belongs to the language project itself. &lt;code&gt;rustup&lt;/code&gt; for Rust and Xcode for Swift are the clearest examples. If one exists and is strong, I leave the layer to it. If the official option is absent or too limited, &lt;code&gt;mise&lt;/code&gt; fills the gap.&lt;/p&gt;
&lt;p&gt;“Official” alone is not enough. I look for four things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It is part of the language project.&lt;/strong&gt; It comes from the people shipping the compiler, rather than a third party trying to catch up with it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It owns more than a version switch.&lt;/strong&gt; That can include toolchain components, stable/beta/nightly channels, cross-compilation targets, and project pins recognized across the ecosystem. Merely selecting the active version is already something &lt;code&gt;mise&lt;/code&gt; handles well.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The community has converged on it.&lt;/strong&gt; “How do I install this language?” should have one routine, near-universal answer. Three competing answers mean there is no sovereign tool yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Its contract is stable across upgrades.&lt;/strong&gt; Giving a tool this layer for years only works if routine upgrades do not break existing projects.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;rustup&lt;/code&gt; clears all four: it is part of Rust, manages toolchains, targets, and release channels, is the standard installation path, and has kept a stable contract for years. Go&apos;s official &lt;code&gt;dl&lt;/code&gt; installer is a useful counterexample. It fetches a particular Go version, but does little beyond that and has not become the accepted owner of version management. It is official, but it does not clear criteria 2 and 3, so I still treat it as weak.&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;my-6&quot;&amp;gt;
&amp;lt;svg viewBox=&quot;0 0 600 300&quot; role=&quot;img&quot; aria-labelledby=&quot;diagram-sov-title&quot; style=&quot;width:100%;height:auto;color:inherit&quot;&amp;gt;
&amp;lt;title id=&quot;diagram-sov-title&quot;&amp;gt;Decision tree: does the language have a strong sovereign tool?&amp;lt;/title&amp;gt;
&amp;lt;g font-family=&quot;ui-sans-serif, system-ui, sans-serif&quot;&amp;gt;
&amp;lt;!-- root --&amp;gt;
&amp;lt;rect x=&quot;140&quot; y=&quot;20&quot; width=&quot;320&quot; height=&quot;58&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.2&quot;/&amp;gt;
&amp;lt;text x=&quot;300&quot; y=&quot;45&quot; font-size=&quot;13.5&quot; font-weight=&quot;600&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot;&amp;gt;Strong, official sovereign tool?&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;300&quot; y=&quot;65&quot; font-size=&quot;11.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.55&quot;&amp;gt;part of the language · owns toolchain · consensus · stable&amp;lt;/text&amp;gt;
&amp;lt;!-- branch labels --&amp;gt;
&amp;lt;text x=&quot;150&quot; y=&quot;108&quot; font-size=&quot;12&quot; font-weight=&quot;700&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--primary)&quot;&amp;gt;Yes&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;450&quot; y=&quot;108&quot; font-size=&quot;12&quot; font-weight=&quot;700&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--primary)&quot;&amp;gt;No / weak&amp;lt;/text&amp;gt;
&amp;lt;!-- connectors --&amp;gt;
&amp;lt;path d=&quot;M260 78 L150 120&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.3&quot; fill=&quot;none&quot;/&amp;gt;
&amp;lt;path d=&quot;M340 78 L450 120&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.3&quot; fill=&quot;none&quot;/&amp;gt;
&amp;lt;!-- leaf left --&amp;gt;
&amp;lt;rect x=&quot;30&quot; y=&quot;125&quot; width=&quot;240&quot; height=&quot;150&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.15&quot;/&amp;gt;
&amp;lt;text x=&quot;150&quot; y=&quot;152&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot;&amp;gt;Defer to the sovereign tool&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;150&quot; y=&quot;176&quot; font-size=&quot;12&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;it owns Layer 1&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;150&quot; y=&quot;210&quot; font-size=&quot;12.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&amp;gt;rustup  →  Rust&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;150&quot; y=&quot;232&quot; font-size=&quot;12.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&amp;gt;Xcode  →  Swift&amp;lt;/text&amp;gt;
&amp;lt;!-- leaf right --&amp;gt;
&amp;lt;rect x=&quot;330&quot; y=&quot;125&quot; width=&quot;240&quot; height=&quot;150&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.15&quot;/&amp;gt;
&amp;lt;text x=&quot;450&quot; y=&quot;152&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot;&amp;gt;mise fills the gap&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;450&quot; y=&quot;176&quot; font-size=&quot;12&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;mise owns Layer 1&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;450&quot; y=&quot;208&quot; font-size=&quot;12.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&amp;gt;Python · Node · Java&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;450&quot; y=&quot;230&quot; font-size=&quot;12.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&amp;gt;Ruby · Go (weak)&amp;lt;/text&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;/svg&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;h2&gt;Applying it to the languages I use&lt;/h2&gt;
&lt;p&gt;These judgments are all about Layer 1: the runtime version.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python:&lt;/strong&gt; no sovereign tool. &lt;code&gt;pyenv&lt;/code&gt;, &lt;code&gt;mise&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, the system Python, and other approaches coexist. I therefore let &lt;code&gt;mise&lt;/code&gt; own Layer 1 and keep &lt;code&gt;uv&lt;/code&gt; at Layer 2, as described in the &lt;a href=&quot;/posts/python/&quot;&gt;Python article&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rust:&lt;/strong&gt; &lt;code&gt;rustup&lt;/code&gt; is the sovereign tool. It installs Rust on my machine; &lt;code&gt;mise&lt;/code&gt; does not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node.js:&lt;/strong&gt; &lt;code&gt;nvm&lt;/code&gt;, &lt;code&gt;fnm&lt;/code&gt;, &lt;code&gt;volta&lt;/code&gt;, and &lt;code&gt;mise&lt;/code&gt; compete, with no official answer. I use &lt;code&gt;mise&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Java:&lt;/strong&gt; there is no sovereign tool, and Java also comes in multiple distributions. I use &lt;code&gt;mise&lt;/code&gt; with Temurin as the default, a neutral choice that avoids reopening the distribution question for every project.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Swift / iOS:&lt;/strong&gt; Xcode owns the toolchain, SDK, and build system and is shipped by Apple. On macOS, installing Swift through another manager means working against the platform, so I defer completely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Go:&lt;/strong&gt; the official option is too weak to own the layer. I am comfortable putting Go under &lt;code&gt;mise&lt;/code&gt;, as many other users do.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ruby:&lt;/strong&gt; &lt;code&gt;rbenv&lt;/code&gt;, &lt;code&gt;rvm&lt;/code&gt;, &lt;code&gt;chruby&lt;/code&gt;, and &lt;code&gt;mise&lt;/code&gt; all exist; none is sovereign. When I need Ruby, &lt;code&gt;mise&lt;/code&gt; owns its version.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;code&gt;mise&lt;/code&gt; can be a proxy&lt;/h2&gt;
&lt;p&gt;Deferring to &lt;code&gt;rustup&lt;/code&gt; does not necessarily mean typing &lt;code&gt;rustup&lt;/code&gt; for every operation. A project can still declare Rust in &lt;code&gt;mise.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# mise.toml
[tools]
rust = &quot;1.78&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mise&lt;/code&gt; delegates to &lt;code&gt;rustup&lt;/code&gt;; it does not reimplement Rust toolchain management. I think of this as &lt;strong&gt;&lt;code&gt;mise&lt;/code&gt; as proxy&lt;/strong&gt;: it provides the shared interface while &lt;code&gt;rustup&lt;/code&gt; remains the owner and source of truth.&lt;/p&gt;
&lt;p&gt;That leaves two sensible arrangements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;rustup&lt;/code&gt;-native:&lt;/strong&gt; a Rust-only project talks to &lt;code&gt;rustup&lt;/code&gt; directly. There is no polyglot setup to unify.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;mise&lt;/code&gt; as the project interface:&lt;/strong&gt; a polyglot project already pinning Python and Node in &lt;code&gt;mise.toml&lt;/code&gt; can add &lt;code&gt;rust = &quot;1.78&quot;&lt;/code&gt;. Then &lt;code&gt;mise install&lt;/code&gt; provisions the project through one declaration while delegating Rust to &lt;code&gt;rustup&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The trap is mixing these arrangements inside one project until neither file is authoritative. The interface may be &lt;code&gt;mise&lt;/code&gt; or &lt;code&gt;rustup&lt;/code&gt;, but ownership of the Rust toolchain must remain with &lt;code&gt;rustup&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;The answers can change&lt;/h2&gt;
&lt;p&gt;I used the same test when looking at Zig. At the time, it did not have a strong official manager with community consensus, so &lt;code&gt;mise&lt;/code&gt; or a simple manual installation was reasonable. Mojo falls into the same young-ecosystem category. Haskell is a useful edge case: GHCup is strong enough that deferring to it makes sense. Lua has no consensus tool, so &lt;code&gt;mise&lt;/code&gt; fills the gap.&lt;/p&gt;
&lt;p&gt;These are current judgments, not permanent assignments. If Python eventually ships a strong official manager and the community converges on it, I would move Python out of &lt;code&gt;mise&lt;/code&gt; and defer to that tool just as I defer to &lt;code&gt;rustup&lt;/code&gt; today.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;/posts/manifesto/&quot;&gt;first article&lt;/a&gt; said that each kind of resource should have one owner. This test decides who that owner is for runtime versions. The &lt;a href=&quot;/posts/polyglot/&quot;&gt;next article&lt;/a&gt; turns the resulting choices into a concrete stack, including the pitfalls, lockfiles, and &lt;code&gt;gitignore&lt;/code&gt; patterns for each language.&lt;/p&gt;
</content:encoded></item><item><title>Making mise and uv Agree on Python</title><link>https://www.shiinayane.com/en/posts/python/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/python/</guid><description>uv had installed a Python that mise did not know about. Setting python-preference to only-system restored one clear owner, with a few consequences for existing projects.</description><pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I noticed the problem because the same command returned different versions in two directories:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cd ~/project &amp;amp;&amp;amp; uv run python --version
Python 3.12.7
$ cd ~ &amp;amp;&amp;amp; uv run python --version
Python 3.13.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The project result was expected: each project can pin its own runtime. The result in my home directory was not. I did not remember installing Python 3.13.1, and &lt;code&gt;mise&lt;/code&gt;—which I use as the sole owner of runtime versions, as described in the &lt;a href=&quot;/posts/manifesto/&quot;&gt;first article in this series&lt;/a&gt;—did not know about it.&lt;/p&gt;
&lt;h2&gt;Finding the other Python&lt;/h2&gt;
&lt;p&gt;I first checked what &lt;code&gt;python&lt;/code&gt; referred to in each context. The shell and &lt;code&gt;uv run&lt;/code&gt; were using different binaries:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ which python
/Users/me/.local/share/mise/installs/python/3.12.7/bin/python
$ uv run python -c &apos;import sys; print(sys.executable)&apos;
/Users/me/.local/share/uv/python/cpython-3.13.1-macos-aarch64-none/bin/python3.13
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The shell found the interpreter installed by &lt;code&gt;mise&lt;/code&gt; and exposed on &lt;code&gt;PATH&lt;/code&gt;. Outside a pinned project, however, &lt;code&gt;uv run&lt;/code&gt; selected an interpreter under &lt;code&gt;~/.local/share/uv/python&lt;/code&gt;, a directory I had never intentionally populated.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;uv&lt;/code&gt;&apos;s own list made the split explicit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ uv python list --only-installed
cpython-3.13.1-macos-aarch64-none    /Users/me/.local/share/uv/python/cpython-3.13.1-.../bin/python3.13
cpython-3.12.7-macos-aarch64-none    /Users/me/.local/share/mise/installs/python/3.12.7/bin/python3.12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There were two separate CPython builds. &lt;code&gt;readlink -f&lt;/code&gt; confirmed that these were not two paths to the same binary: one belonged to &lt;code&gt;mise&lt;/code&gt;, while the other had been downloaded and stored by &lt;code&gt;uv&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is normal &lt;code&gt;uv&lt;/code&gt; behavior, not a bug. Its &lt;code&gt;python-preference&lt;/code&gt; setting defaults to &lt;code&gt;managed&lt;/code&gt;: prefer a uv-managed Python and, when no available interpreter satisfies a project&apos;s &lt;code&gt;requires-python&lt;/code&gt;, download one. That is convenient when &lt;code&gt;uv&lt;/code&gt; owns both the runtime and the packages. It conflicts with my setup because &lt;code&gt;mise&lt;/code&gt; is supposed to be the only tool deciding which Python versions exist.&lt;/p&gt;
&lt;h2&gt;Interpreter, virtual environment, and cache&lt;/h2&gt;
&lt;p&gt;Before changing the setting, I needed to separate three things that all look like “Python files in hidden directories” but have different lifetimes and owners.&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;my-6&quot;&amp;gt;
&amp;lt;svg viewBox=&quot;0 0 640 300&quot; role=&quot;img&quot; aria-labelledby=&quot;diagram-factory-title&quot; style=&quot;width:100%;height:auto;color:inherit&quot;&amp;gt;
&amp;lt;title id=&quot;diagram-factory-title&quot;&amp;gt;Blueprint, sample unit, and shared library: mise install, .venv, and the uv cache&amp;lt;/title&amp;gt;
&amp;lt;g font-family=&quot;ui-sans-serif, system-ui, sans-serif&quot;&amp;gt;
&amp;lt;!-- Blueprint --&amp;gt;
&amp;lt;rect x=&quot;10&quot; y=&quot;60&quot; width=&quot;180&quot; height=&quot;180&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.15&quot;/&amp;gt;
&amp;lt;text x=&quot;100&quot; y=&quot;40&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--primary)&quot;&amp;gt;Blueprint&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;100&quot; y=&quot;92&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot;&amp;gt;mise install&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;100&quot; y=&quot;116&quot; font-size=&quot;12&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;python 3.14.0&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;100&quot; y=&quot;150&quot; font-size=&quot;11.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.5&quot;&amp;gt;one full interpreter&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;100&quot; y=&quot;168&quot; font-size=&quot;11.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.5&quot;&amp;gt;per version number&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;100&quot; y=&quot;200&quot; font-size=&quot;11.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.5&quot;&amp;gt;many versions&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;100&quot; y=&quot;218&quot; font-size=&quot;11.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.5&quot;&amp;gt;coexist&amp;lt;/text&amp;gt;
&amp;lt;!-- Sample unit --&amp;gt;
&amp;lt;rect x=&quot;230&quot; y=&quot;60&quot; width=&quot;180&quot; height=&quot;180&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.15&quot;/&amp;gt;
&amp;lt;text x=&quot;320&quot; y=&quot;40&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--primary)&quot;&amp;gt;Sample unit&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;320&quot; y=&quot;92&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot;&amp;gt;project/.venv&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;320&quot; y=&quot;124&quot; font-size=&quot;11.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;bin/python → symlink&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;320&quot; y=&quot;142&quot; font-size=&quot;11.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;to the blueprint&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;320&quot; y=&quot;186&quot; font-size=&quot;11.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;site-packages =&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;320&quot; y=&quot;204&quot; font-size=&quot;11.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;the only private part&amp;lt;/text&amp;gt;
&amp;lt;!-- Shared library --&amp;gt;
&amp;lt;rect x=&quot;450&quot; y=&quot;60&quot; width=&quot;180&quot; height=&quot;180&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.15&quot;/&amp;gt;
&amp;lt;text x=&quot;540&quot; y=&quot;40&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--primary)&quot;&amp;gt;Shared library&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;540&quot; y=&quot;92&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot;&amp;gt;uv cache&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;540&quot; y=&quot;124&quot; font-size=&quot;11.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;content-addressable&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;540&quot; y=&quot;160&quot; font-size=&quot;11.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;one copy of a package&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;540&quot; y=&quot;178&quot; font-size=&quot;11.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;shared across all&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;540&quot; y=&quot;196&quot; font-size=&quot;11.5&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;projects&amp;lt;/text&amp;gt;
&amp;lt;!-- arrows --&amp;gt;
&amp;lt;text x=&quot;210&quot; y=&quot;135&quot; font-size=&quot;18&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.4&quot;&amp;gt;←&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;210&quot; y=&quot;152&quot; font-size=&quot;10&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.5&quot;&amp;gt;symlink&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;430&quot; y=&quot;135&quot; font-size=&quot;18&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.4&quot;&amp;gt;→&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;430&quot; y=&quot;152&quot; font-size=&quot;10&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.5&quot;&amp;gt;link in&amp;lt;/text&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;/svg&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;I think of a version installed by &lt;code&gt;mise install python@3.14&lt;/code&gt; as a &lt;strong&gt;blueprint&lt;/strong&gt;: one complete interpreter identified by a version number. Several versions can coexist in &lt;code&gt;mise&lt;/code&gt;&apos;s install directory, so 3.12.7 and 3.14.0 are simply two independent blueprints.&lt;/p&gt;
&lt;p&gt;A project&apos;s &lt;strong&gt;&lt;code&gt;.venv&lt;/code&gt;&lt;/strong&gt; is a &lt;strong&gt;sample unit&lt;/strong&gt; made from one of them. A virtual environment is mostly symlinks; its &lt;code&gt;bin/python&lt;/code&gt; points back to the source interpreter instead of containing another copy. Its genuinely project-private part is &lt;code&gt;site-packages&lt;/code&gt;. This makes a &lt;code&gt;.venv&lt;/code&gt; small and quick to create, but it also means that removing a &lt;code&gt;mise&lt;/code&gt;-managed Python breaks every environment whose &lt;code&gt;bin/python&lt;/code&gt; points to it.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;uv cache&lt;/strong&gt; is the shared library. It is content-addressable: a package version is stored once, then linked into each project&apos;s &lt;code&gt;site-packages&lt;/code&gt; rather than repeatedly copied. If ten projects use the same version of &lt;code&gt;numpy&lt;/code&gt;, they can refer to the same cached copy. Much of &lt;code&gt;uv&lt;/code&gt;&apos;s speed comes from creating these links instead of downloading and unpacking the same files again.&lt;/p&gt;
&lt;p&gt;In this model, &lt;code&gt;mise&lt;/code&gt; supplies interpreters. &lt;code&gt;uv&lt;/code&gt; creates project environments from them and fills those environments from its package cache. I did not need &lt;code&gt;uv&lt;/code&gt; to provision another interpreter as well.&lt;/p&gt;
&lt;h2&gt;Restricting uv to system Python&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;uv&lt;/code&gt; reads global configuration from &lt;code&gt;~/.config/uv/uv.toml&lt;/code&gt;. I added one setting:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/uv/uv.toml
python-preference = &quot;only-system&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The four possible values matter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;only-managed&lt;/code&gt; uses only uv-managed Pythons and ignores system interpreters. It provides the strongest isolation from the system and is the furthest from a &lt;code&gt;mise&lt;/code&gt;-owned setup.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;managed&lt;/code&gt;, the default, prefers uv-managed Pythons, falls back to system Pythons, and downloads a managed interpreter if neither satisfies &lt;code&gt;requires-python&lt;/code&gt;. This was the source of my unexpected installation.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;system&lt;/code&gt; prefers an interpreter already on &lt;code&gt;PATH&lt;/code&gt;, but can still download a managed one when no suitable system interpreter exists.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;only-system&lt;/code&gt; uses only system Pythons, including those that &lt;code&gt;mise&lt;/code&gt; places on &lt;code&gt;PATH&lt;/code&gt;, and never downloads one. If none satisfies the requirement, the command fails.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For my setup, that final failure mode is useful: a missing version remains visible until I install it through &lt;code&gt;mise&lt;/code&gt;. Someone who wants to prefer &lt;code&gt;mise&lt;/code&gt; without giving up &lt;code&gt;uv&lt;/code&gt;&apos;s fallback can choose &lt;code&gt;system&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;After changing the setting, I removed &lt;code&gt;uv&lt;/code&gt;&apos;s copy of Python 3.13.1 and rebuilt the affected environment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ uv python uninstall 3.13.1
$ cd ~/project &amp;amp;&amp;amp; rm -rf .venv &amp;amp;&amp;amp; uv sync
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;uv run python&lt;/code&gt; and the shell&apos;s &lt;code&gt;python&lt;/code&gt; then agreed because both resolved to an interpreter owned by &lt;code&gt;mise&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Existing projects can fail after the change&lt;/h2&gt;
&lt;p&gt;Changing a global selection rule also affects environments created under the old rule. I had older projects whose &lt;code&gt;mise.toml&lt;/code&gt; requested Python versions that I had since removed from &lt;code&gt;mise&lt;/code&gt;. Previously, &lt;code&gt;uv sync&lt;/code&gt; could download the missing interpreter and continue. With &lt;code&gt;only-system&lt;/code&gt;, it stopped:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: No interpreter found for Python 3.11 in system path
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That error means the project requests a Python that is not currently installed. The explicit repair is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ mise install python@3.11   # provision the blueprint, deliberately
$ uv sync                    # now succeeds, using mise&apos;s Python
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When reopening an older project, I now:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check &lt;code&gt;requires-python&lt;/code&gt; in &lt;code&gt;pyproject.toml&lt;/code&gt; and any version pinned in &lt;code&gt;mise.toml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;mise install&lt;/code&gt; if that interpreter is missing.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;rm -rf .venv &amp;amp;&amp;amp; uv sync&lt;/code&gt; so the environment points to the installed interpreter.&lt;/li&gt;
&lt;li&gt;Confirm the result with &lt;code&gt;uv run python --version&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This adds a deliberate &lt;code&gt;mise install&lt;/code&gt; step where the default configuration might have fixed the mismatch silently. I prefer seeing the missing runtime, but it is a real trade-off rather than free strictness.&lt;/p&gt;
&lt;p&gt;I also keep &lt;code&gt;uv.toml&lt;/code&gt; in my chezmoi source repository, alongside the files discussed in the &lt;a href=&quot;/posts/dotfiles/&quot;&gt;dotfiles article&lt;/a&gt;, so the same rule follows me to another machine.&lt;/p&gt;
&lt;h2&gt;Small scripts do not need full projects&lt;/h2&gt;
&lt;p&gt;For a 40-line script that needs &lt;code&gt;httpx&lt;/code&gt;, creating &lt;code&gt;pyproject.toml&lt;/code&gt;, a &lt;code&gt;.venv&lt;/code&gt;, and a lockfile is unnecessary. &lt;code&gt;uv&lt;/code&gt; offers smaller paths that still obey &lt;code&gt;only-system&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For a throwaway run, &lt;code&gt;--with&lt;/code&gt; adds dependencies to a temporary environment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ uv run --with httpx --with rich script.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a script worth keeping, PEP 723 metadata records the Python requirement and dependencies in the file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /// script
# requires-python = &quot;&amp;gt;=3.12&quot;
# dependencies = [&quot;httpx&quot;, &quot;rich&quot;]
# ///
import httpx
from rich import print
print(httpx.get(&quot;https://example.com&quot;).status_code)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running &lt;code&gt;uv run script.py&lt;/code&gt; makes &lt;code&gt;uv&lt;/code&gt; read that metadata, assemble an environment from its cache, and execute the script without a project &lt;code&gt;.venv&lt;/code&gt;. With a &lt;code&gt;uv&lt;/code&gt; shebang, &lt;code&gt;chmod +x&lt;/code&gt;, and a location such as &lt;code&gt;~/.local/bin&lt;/code&gt;, the script can also act like any other command on &lt;code&gt;PATH&lt;/code&gt; while keeping its dependencies in the file. This has been much cleaner than the personal utilities I used to install into a global interpreter with &lt;code&gt;pip install&lt;/code&gt; and later forget about.&lt;/p&gt;
&lt;p&gt;Actual command-line tools belong in isolated tool environments. &lt;code&gt;uv tool install&lt;/code&gt; handles persistent installations, while &lt;code&gt;uvx&lt;/code&gt; runs a tool once; both fit utilities such as &lt;code&gt;ruff&lt;/code&gt; or &lt;code&gt;httpie&lt;/code&gt; better than a global &lt;code&gt;pip install&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;mise&lt;/code&gt; is absent, letting &lt;code&gt;uv&lt;/code&gt; own both Python and its packages is internally consistent, and the default &lt;code&gt;managed&lt;/code&gt; behavior may be exactly right. My conflict existed only because two tools were making runtime decisions. The &lt;a href=&quot;/posts/sovereignty/&quot;&gt;next article&lt;/a&gt; looks at how I decide when the official tool should own that layer instead.&lt;/p&gt;
</content:encoded></item><item><title>The Brewfile Compromise: Eventual Consistency for Mac Apps</title><link>https://www.shiinayane.com/en/posts/apps/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/apps/</guid><description>Not every layer deserves strict declarative management. The app layer is fine with eventual consistency, as long as the drift stays visible.</description><pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;My Brewfile is often slightly out of sync with the applications installed on my Mac. I am fine with that.&lt;/p&gt;
&lt;p&gt;This is looser than the setup in the &lt;a href=&quot;/posts/python/&quot;&gt;Python article&lt;/a&gt;, where one owner controls the environment, or the &lt;a href=&quot;/posts/dotfiles/&quot;&gt;dotfiles article&lt;/a&gt;, where stray configuration files are something to account for. Applications have a different failure mode, so I do not think they need the same enforcement.&lt;/p&gt;
&lt;h2&gt;Why I tolerate drift here&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Layer 3 — Project dependencies&lt;/strong&gt; needs strict consistency. When a lockfile specifies one version but a developer machine has another, the build may fail or behave differently from CI. The failure is immediate, and somebody else may have to pay for it. Exact versions and reproducible installs are worth the ceremony.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 0 — System&lt;/strong&gt; contains the applications and command-line tools installed by Homebrew. Suppose I run &lt;code&gt;brew install&lt;/code&gt; today and forget to add the tool to my Brewfile. The current machine still works. I usually discover the omission much later, during a migration, when I restore from the Brewfile and the tool is missing. Then I install it and add the forgotten line.&lt;/p&gt;
&lt;p&gt;That is a real cost, but it is deferred and usually small. For this layer, I accept day-to-day drift and reconcile it periodically: &lt;strong&gt;eventual consistency&lt;/strong&gt; instead of the strict consistency I use for project dependencies.&lt;/p&gt;
&lt;h2&gt;Choosing an installation channel&lt;/h2&gt;
&lt;p&gt;On macOS I use three channels, in this order:&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;my-6&quot;&amp;gt;
&amp;lt;svg viewBox=&quot;0 0 600 120&quot; role=&quot;img&quot; aria-labelledby=&quot;diagram-channels-title&quot; style=&quot;width:100%;height:auto;color:inherit&quot;&amp;gt;
&amp;lt;title id=&quot;diagram-channels-title&quot;&amp;gt;Install channel priority: brew, then mas, then dmg&amp;lt;/title&amp;gt;
&amp;lt;g font-family=&quot;ui-sans-serif, system-ui, sans-serif&quot;&amp;gt;
&amp;lt;rect x=&quot;20&quot; y=&quot;35&quot; width=&quot;150&quot; height=&quot;50&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.08&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.2&quot;/&amp;gt;
&amp;lt;text x=&quot;95&quot; y=&quot;58&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--primary)&quot;&amp;gt;brew&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;95&quot; y=&quot;75&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;formula / cask&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;200&quot; y=&quot;65&quot; font-size=&quot;16&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.4&quot;&amp;gt;→&amp;lt;/text&amp;gt;
&amp;lt;rect x=&quot;225&quot; y=&quot;35&quot; width=&quot;150&quot; height=&quot;50&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.15&quot;/&amp;gt;
&amp;lt;text x=&quot;300&quot; y=&quot;58&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot;&amp;gt;mas&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;300&quot; y=&quot;75&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;App Store&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;405&quot; y=&quot;65&quot; font-size=&quot;16&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.4&quot;&amp;gt;→&amp;lt;/text&amp;gt;
&amp;lt;rect x=&quot;430&quot; y=&quot;35&quot; width=&quot;150&quot; height=&quot;50&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.04&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.12&quot;/&amp;gt;
&amp;lt;text x=&quot;505&quot; y=&quot;58&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot;&amp;gt;dmg&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;505&quot; y=&quot;75&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;last resort&amp;lt;/text&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;/svg&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;I try &lt;strong&gt;Homebrew first&lt;/strong&gt;. Formulae cover command-line tools and casks cover graphical applications. Homebrew already owns Layer 0 in this setup, and either kind of &lt;code&gt;brew&lt;/code&gt; installation is easy to record in a Brewfile.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Mac App Store comes second&lt;/strong&gt;, through &lt;code&gt;mas-cli&lt;/code&gt;. I use &lt;code&gt;mas&lt;/code&gt; when an application depends on the Apple ecosystem—iCloud sync, Family Sharing, or App Store receipts—or is available only from the store. Otherwise I prefer a cask: it updates with the same &lt;code&gt;brew&lt;/code&gt; command as everything else and does not require a signed-in App Store account.&lt;/p&gt;
&lt;p&gt;A vendor-provided &lt;strong&gt;&lt;code&gt;.dmg&lt;/code&gt; is the last resort&lt;/strong&gt;. These installations cannot be expressed in the Brewfile, so I keep a &lt;code&gt;manual-installs.md&lt;/code&gt; with the application name and its source. Without that file, the only record of what the Brewfile missed would be my memory.&lt;/p&gt;
&lt;h2&gt;One file for the intended state&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;mas-cli&lt;/code&gt; is useful because a &lt;code&gt;mas&lt;/code&gt; entry lets App Store applications live in the same Brewfile as formulae and casks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Brewfile
tap &quot;homebrew/bundle&quot;

brew &quot;ripgrep&quot;
brew &quot;mas&quot;

cask &quot;visual-studio-code&quot;
cask &quot;rectangle&quot;

# App Store apps, by their numeric ID (mas list to find them)
mas &quot;Things 3&quot;, id: 904280696
mas &quot;Xcode&quot;, id: 497799835
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that file, &lt;code&gt;brew bundle&lt;/code&gt; installs formulae, casks, and App Store applications in one pass. It is the single source of truth for what belongs on the machine, following the ownership model from the &lt;a href=&quot;/posts/manifesto/&quot;&gt;first article&lt;/a&gt;. The file may temporarily lag behind the machine, but there is still only one declared state.&lt;/p&gt;
&lt;h2&gt;How I reconcile it&lt;/h2&gt;
&lt;p&gt;Eventual consistency needs an actual convergence step. Mine starts with a read-only health check called &lt;code&gt;brewdiff&lt;/code&gt;. It shows drift in both directions: installed but undeclared, and declared but missing.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 30-functions.zsh — show drift between the Brewfile and the machine
brewdiff() {
  local brewfile=&quot;${HOMEBREW_BUNDLE_FILE:-$HOME/.config/homebrew/Brewfile}&quot;
  echo &quot;== Installed but NOT in Brewfile (undeclared) ==&quot;
  brew bundle cleanup --file=&quot;$brewfile&quot; 2&amp;gt;/dev/null \
    | grep -E &apos;^(Would uninstall|brew|cask|mas)&apos; || echo &quot;  (none)&quot;
  echo
  echo &quot;== In Brewfile but NOT installed (missing) ==&quot;
  brew bundle check --file=&quot;$brewfile&quot; --verbose 2&amp;gt;/dev/null \
    | grep -v &apos;^The Brewfile&apos; || echo &quot;  (all installed)&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function only reports; it does not change the machine. I can inspect each undeclared item and decide whether it is a tool I meant to keep or an experiment I should remove.&lt;/p&gt;
&lt;p&gt;For ordinary formulae, &lt;code&gt;brewadd&lt;/code&gt; makes the common case cheaper by installing a package and appending it to the Brewfile in the same operation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# install AND declare in one step
brewadd() {
  local brewfile=&quot;${HOMEBREW_BUNDLE_FILE:-$HOME/.config/homebrew/Brewfile}&quot;
  brew install &quot;$@&quot; || return 1
  for pkg in &quot;$@&quot;; do
    grep -q &quot;\&quot;$pkg\&quot;&quot; &quot;$brewfile&quot; || echo &quot;brew \&quot;$pkg\&quot;&quot; &amp;gt;&amp;gt; &quot;$brewfile&quot;
  done
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also need a reminder, because a reconcile command that I forget to run is not much of a system. When a shell starts, this function checks the age of a timestamp file and nudges me if more than 30 days have passed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# remind me to reconcile if it has been &amp;gt; 30 days
_brewdiff_reminder() {
  local stamp=&quot;$HOME/.cache/brewdiff-last&quot;
  if [[ ! -f &quot;$stamp&quot; ]] || \
     [[ $(find &quot;$stamp&quot; -mtime +30 2&amp;gt;/dev/null) ]]; then
    print -P &quot;%F{yellow}brewdiff:%f it&apos;s been a while — run &apos;brewdiff&apos; to reconcile&quot;
  fi
}
_brewdiff_reminder
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running &lt;code&gt;brewdiff&lt;/code&gt; would &lt;code&gt;touch&lt;/code&gt; the timestamp at the end and reset the clock. I have kept this deliberately small; any more friction would make the monthly check easy to postpone.&lt;/p&gt;
&lt;p&gt;The Brewfile itself follows the chezmoi setup from the &lt;a href=&quot;/posts/dotfiles/&quot;&gt;dotfiles article&lt;/a&gt;. A &lt;code&gt;run_onchange_&lt;/code&gt; script runs &lt;code&gt;brew bundle&lt;/code&gt; on a new machine and whenever the Brewfile changes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# run_onchange_brew-bundle.sh.tmpl
#!/bin/sh
# Brewfile hash: {{ include &quot;dot_config/homebrew/Brewfile&quot; | sha256sum }}
brew bundle --file=&quot;$HOME/.config/homebrew/Brewfile&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hash in the comment is what triggers the script. Chezmoi sees the line change when the Brewfile checksum changes, so it reruns the command after an edit.&lt;/p&gt;
&lt;h2&gt;Letting an agent audit the list&lt;/h2&gt;
&lt;p&gt;For the monthly check, I sometimes give the &lt;code&gt;brewdiff&lt;/code&gt; output to an AI agent. Its job is to explain each undeclared package and suggest keeping or dropping it, with a reason. It does not edit the Brewfile.&lt;/p&gt;
&lt;p&gt;For example, an answer such as “&lt;code&gt;pngquant&lt;/code&gt; is an image compressor and looks like a one-off; consider dropping it” saves me research time while leaving the decision visible. If the agent rewrote the file directly, a second actor would be changing the source of truth and the Brewfile would no longer represent only my intent. The useful delegation here is judgment, not authority.&lt;/p&gt;
&lt;h2&gt;The command I do not automate&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;brew bundle cleanup&lt;/code&gt;, including the &lt;code&gt;--cleanup&lt;/code&gt; flag, uninstalls everything that is not in the Brewfile. That is convenient after the declaration is complete and dangerous while half of the tools I use are still undeclared. I do not run it with &lt;code&gt;--force&lt;/code&gt; until &lt;code&gt;brewdiff&lt;/code&gt; produces a short list whose contents I understand.&lt;/p&gt;
&lt;p&gt;The monthly review is one case of the broader maintenance routine in the &lt;a href=&quot;/posts/maintenance/&quot;&gt;maintenance article&lt;/a&gt;: make drift visible, inspect it, and converge on a schedule. For Mac applications, that has been enough. I do not need every installation to update the Brewfile immediately; I need omissions to remain visible and cheap to repair.&lt;/p&gt;
</content:encoded></item><item><title>Dotfiles Without .zshrc: ZDOTDIR and chezmoi</title><link>https://www.shiinayane.com/en/posts/dotfiles/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/dotfiles/</guid><description>Move zsh config out of the home directory with ZDOTDIR, split it into modules, and manage it declaratively with chezmoi so drift becomes visible.</description><pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;For a long time I could not tell which lines in my &lt;code&gt;~/.zshrc&lt;/code&gt; I had written
and which ones came from an installer.&lt;/p&gt;
&lt;p&gt;Installers append to your shell startup file as a matter of routine. You run an install script, it prints a cheerful &quot;added initialization to &lt;code&gt;~/.zshrc&lt;/code&gt;&quot;, and that line is now load-bearing config you did not author and will not remember. Do this for two years and your &lt;code&gt;~/.zshrc&lt;/code&gt; becomes an archaeological dig: strata of half-remembered tools, some still installed, some long gone, none commented. The file works, so you never touch it, so it only grows.&lt;/p&gt;
&lt;p&gt;I now keep only &lt;code&gt;~/.zshenv&lt;/code&gt; in the home directory. The rest lives under
&lt;code&gt;~/.config/zsh/&lt;/code&gt;, split into small files and managed by chezmoi. This is the
dotfiles version of the ownership model from the &lt;a href=&quot;/posts/manifesto/&quot;&gt;first
article&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;ZDOTDIR is zsh&apos;s own escape hatch&lt;/h2&gt;
&lt;p&gt;When zsh starts, it sources files in a fixed order. For an interactive login shell on macOS it is roughly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/etc/zshenv      →  ~/.zshenv      (always, every shell)
/etc/zprofile    →  ~/.zprofile    (login shells)
/etc/zshrc       →  ~/.zshrc       (interactive shells)
/etc/zlogin      →  ~/.zlogin      (login shells)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The detail that changes everything is what &lt;code&gt;~/.zshenv&lt;/code&gt; is allowed to do. It runs first, for &lt;em&gt;every&lt;/em&gt; invocation of zsh, and it is the one place you can set an environment variable that the rest of the startup sequence will respect. One of those variables is &lt;code&gt;ZDOTDIR&lt;/code&gt;. If you set it, zsh reads &lt;code&gt;.zprofile&lt;/code&gt;, &lt;code&gt;.zshrc&lt;/code&gt;, and the rest not from &lt;code&gt;$HOME&lt;/code&gt; but from &lt;code&gt;$ZDOTDIR&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is the official mechanism, not a hack. I call the result &lt;strong&gt;ZDOTDIR mode&lt;/strong&gt;: set &lt;code&gt;ZDOTDIR=~/.config/zsh&lt;/code&gt; in &lt;code&gt;~/.zshenv&lt;/code&gt; so zsh reads all of its configuration from &lt;code&gt;~/.config/zsh/&lt;/code&gt;, leaving the home directory with only &lt;code&gt;~/.zshenv&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So the entire contents of &lt;code&gt;~/.zshenv&lt;/code&gt; are one line:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ~/.zshenv — the only zsh file allowed to live in $HOME
export ZDOTDIR=&quot;${XDG_CONFIG_HOME:-$HOME/.config}/zsh&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From here on, every other zsh file lives under &lt;code&gt;~/.config/zsh/&lt;/code&gt;. The home directory holds exactly one dotfile that zsh cares about, and its job is to point at where the real configuration lives.&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;my-6&quot;&amp;gt;
&amp;lt;svg viewBox=&quot;0 0 600 300&quot; role=&quot;img&quot; aria-labelledby=&quot;diagram-home-title&quot; style=&quot;width:100%;height:auto;color:inherit&quot;&amp;gt;
&amp;lt;title id=&quot;diagram-home-title&quot;&amp;gt;Home directory before and after ZDOTDIR mode&amp;lt;/title&amp;gt;
&amp;lt;g font-family=&quot;ui-monospace, SFMono-Regular, Menlo, monospace&quot;&amp;gt;
&amp;lt;!-- Before --&amp;gt;
&amp;lt;text x=&quot;20&quot; y=&quot;28&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; font-family=&quot;ui-sans-serif, system-ui, sans-serif&quot; fill=&quot;currentColor&quot;&amp;gt;Before&amp;lt;/text&amp;gt;
&amp;lt;rect x=&quot;20&quot; y=&quot;40&quot; width=&quot;250&quot; height=&quot;240&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.15&quot;/&amp;gt;
&amp;lt;text x=&quot;36&quot; y=&quot;66&quot; font-size=&quot;13&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.85&quot;&amp;gt;~/&amp;lt;/text&amp;gt;
&amp;lt;g font-size=&quot;12.5&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;
&amp;lt;text x=&quot;48&quot; y=&quot;90&quot;&amp;gt;.zshenv&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;48&quot; y=&quot;110&quot;&amp;gt;.zshrc&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;48&quot; y=&quot;130&quot;&amp;gt;.zprofile&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;48&quot; y=&quot;150&quot;&amp;gt;.bash_profile&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;48&quot; y=&quot;170&quot;&amp;gt;.npmrc&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;48&quot; y=&quot;190&quot;&amp;gt;.gitconfig&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;48&quot; y=&quot;210&quot;&amp;gt;.python_history&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;48&quot; y=&quot;230&quot;&amp;gt;.zsh_history&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;48&quot; y=&quot;250&quot;&amp;gt;.cargo/  .rustup/  …&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;48&quot; y=&quot;270&quot; fill-opacity=&quot;0.4&quot;&amp;gt;(it keeps growing)&amp;lt;/text&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;!-- After --&amp;gt;
&amp;lt;text x=&quot;330&quot; y=&quot;28&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; font-family=&quot;ui-sans-serif, system-ui, sans-serif&quot; fill=&quot;currentColor&quot;&amp;gt;After (ZDOTDIR mode)&amp;lt;/text&amp;gt;
&amp;lt;rect x=&quot;330&quot; y=&quot;40&quot; width=&quot;250&quot; height=&quot;70&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.15&quot;/&amp;gt;
&amp;lt;text x=&quot;346&quot; y=&quot;66&quot; font-size=&quot;13&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.85&quot;&amp;gt;~/&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;358&quot; y=&quot;90&quot; font-size=&quot;12.5&quot; fill=&quot;var(--primary)&quot;&amp;gt;.zshenv  → sets ZDOTDIR&amp;lt;/text&amp;gt;
&amp;lt;rect x=&quot;330&quot; y=&quot;125&quot; width=&quot;250&quot; height=&quot;155&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.15&quot;/&amp;gt;
&amp;lt;text x=&quot;346&quot; y=&quot;151&quot; font-size=&quot;13&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.85&quot;&amp;gt;~/.config/zsh/&amp;lt;/text&amp;gt;
&amp;lt;g font-size=&quot;12.5&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;
&amp;lt;text x=&quot;358&quot; y=&quot;173&quot;&amp;gt;.zshrc&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;358&quot; y=&quot;193&quot;&amp;gt;00-env.zsh&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;358&quot; y=&quot;213&quot;&amp;gt;20-aliases.zsh&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;358&quot; y=&quot;233&quot;&amp;gt;30-functions.zsh&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;358&quot; y=&quot;253&quot;&amp;gt;35-tools.zsh  …&amp;lt;/text&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;/svg&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;A caveat worth stating early, because it is the usual objection: this is well-supported, but not universally. The VS Code integrated terminal honors &lt;code&gt;~/.zshenv&lt;/code&gt; and therefore picks up &lt;code&gt;ZDOTDIR&lt;/code&gt; without any extra setup, which covers the editor most readers will care about. A small number of older tools assume your configuration is literally in &lt;code&gt;~/.zshrc&lt;/code&gt; and will not find it. When that happens you will know — the tool fails loudly rather than silently — and you can decide case by case whether to humor it. In two years it has come up rarely enough that I have not regretted the move.&lt;/p&gt;
&lt;h2&gt;A modular layout instead of one long file&lt;/h2&gt;
&lt;p&gt;Moving the file is half the work. The other half is not recreating the same monolith at the new address. Instead of one &lt;code&gt;~/.config/zsh/.zshrc&lt;/code&gt;, I keep a directory of numbered fragments, and the &lt;code&gt;.zshrc&lt;/code&gt; does nothing but load them in order:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/zsh/.zshrc — loads every fragment in numeric order
for _file in &quot;${ZDOTDIR}&quot;/conf.d/*.zsh(N); do
  source &quot;$_file&quot;
done
unset _file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;(N)&lt;/code&gt; is a zsh glob qualifier that makes the pattern expand to nothing if the directory is empty, instead of erroring. The fragments live in &lt;code&gt;conf.d/&lt;/code&gt; with a numeric prefix that encodes load order:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.config/zsh/conf.d/
├── 00-env.zsh          # exported env vars, PATH base
├── 10-completion.zsh   # compinit and completion styles
├── 20-aliases.zsh      # short renames of existing commands
├── 30-functions.zsh    # shell functions that do real work
├── 35-tools.zsh        # eval-hooks for external CLIs (mise, zoxide, …)
├── 40-lang.zsh         # language/runtime-specific setup
├── 50-plugins.zsh      # zsh-ecosystem plugins
└── 90-local.zsh        # machine-specific, not tracked in git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The split is not cosmetic. Each prefix corresponds to a &lt;em&gt;kind&lt;/em&gt; of thing, and the kinds have genuinely different lifecycles and ordering needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Aliases&lt;/strong&gt; (&lt;code&gt;20&lt;/code&gt;) only rename commands you already have. They are cheap and order-independent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Functions&lt;/strong&gt; (&lt;code&gt;30&lt;/code&gt;) do actual work and may depend on env being set, so they come after &lt;code&gt;00-env&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tools&lt;/strong&gt; (&lt;code&gt;35&lt;/code&gt;) are external CLIs that print a block of shell code for you to &lt;code&gt;eval&lt;/code&gt; — &lt;code&gt;mise activate&lt;/code&gt;, &lt;code&gt;zoxide init&lt;/code&gt;, and so on. These are separated because they are not your code; they are generated hooks, and isolating them makes it obvious what is yours versus what a tool injected. This is the same instinct that started the article: keep authored config distinguishable from generated config.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lang&lt;/strong&gt; (&lt;code&gt;40&lt;/code&gt;) is runtime-specific setup that needs the tool hooks from &lt;code&gt;35&lt;/code&gt; already in place.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plugins&lt;/strong&gt; (&lt;code&gt;50&lt;/code&gt;) are third-party zsh scripts, and they have the strictest ordering constraints.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two ordering rules matter enough to call out. Plugins load &lt;em&gt;after&lt;/em&gt; the language and tool setup, because a prompt plugin that shows your current runtime version needs &lt;code&gt;mise&lt;/code&gt; to have been activated first — otherwise the prompt renders before the information it wants to display exists. And syntax-highlighting, if you use it, must be sourced &lt;em&gt;last&lt;/em&gt;; it works by wrapping the line editor, and anything that touches the line editor afterward will fight it. The numeric prefixes make these rules visible in the directory listing rather than buried as a comment somewhere in a 400-line file.&lt;/p&gt;
&lt;h2&gt;Building PATH so it does not accumulate&lt;/h2&gt;
&lt;p&gt;There is one specific bug this layout invites, and it is worth fixing deliberately. &lt;code&gt;~/.zshenv&lt;/code&gt; runs on &lt;em&gt;every&lt;/em&gt; shell, including nested ones. The naive way to add a directory to &lt;code&gt;PATH&lt;/code&gt; is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export PATH=&quot;$HOME/.local/bin:$PATH&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run that in &lt;code&gt;~/.zshenv&lt;/code&gt;, open a shell, then open a shell inside that shell, and &lt;code&gt;~/.local/bin&lt;/code&gt; now appears in &lt;code&gt;PATH&lt;/code&gt; twice. Tmux, subshells, and &lt;code&gt;exec zsh&lt;/code&gt; all compound it. The list grows, lookups get slower, and reasoning about precedence gets harder.&lt;/p&gt;
&lt;p&gt;The fix is to make the prepend idempotent — adding a directory that is already present should be a no-op:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Prepend to PATH only if not already present.
path_prepend() {
  case &quot;:$PATH:&quot; in
    *&quot;:$1:&quot;*) ;;            # already there — do nothing
    *) PATH=&quot;$1:$PATH&quot; ;;
  esac
}

path_prepend &quot;$HOME/.local/bin&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;case &quot;:$PATH:&quot;&lt;/code&gt; trick wraps &lt;code&gt;PATH&lt;/code&gt; in colons on both ends so that even the first and last entries are bounded by colons, which means the &lt;code&gt;*&quot;:$1:&quot;*&lt;/code&gt; pattern matches them too. It reads as a small thing, and it is, but it is the difference between a &lt;code&gt;PATH&lt;/code&gt; that is stable across re-sourcing and one that quietly bloats. Belonging in &lt;code&gt;00-env.zsh&lt;/code&gt;, it runs before anything that depends on &lt;code&gt;PATH&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;chezmoi: the source repo is not the target&lt;/h2&gt;
&lt;p&gt;So far everything is local files. To make them portable and version-controlled without symlinking a git repo straight into &lt;code&gt;$HOME&lt;/code&gt;, I use &lt;a href=&quot;https://www.chezmoi.io/&quot;&gt;chezmoi&lt;/a&gt;, which keeps a &lt;em&gt;source&lt;/em&gt; directory under version control and renders it into your home directory. The piece worth understanding is that chezmoi encodes target-file metadata in the source &lt;em&gt;filename&lt;/em&gt; through prefixes, so the source repo deliberately does not look like your home directory.&lt;/p&gt;
&lt;p&gt;The prefixes I rely on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dot_&lt;/code&gt; — renders to a leading dot. &lt;code&gt;dot_zshenv&lt;/code&gt; becomes &lt;code&gt;~/.zshenv&lt;/code&gt;. This exists because a source repo full of literal dotfiles would have everything hidden.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;private_&lt;/code&gt; — sets &lt;code&gt;0600&lt;/code&gt; permissions on the result. Used for anything that should not be world-readable.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;executable_&lt;/code&gt; — sets the executable bit. Used for scripts in &lt;code&gt;~/.local/bin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exact_&lt;/code&gt; — on a directory, means &quot;this is the complete contents; remove anything here that I did not declare.&quot; Powerful for enforcing that a directory holds &lt;em&gt;only&lt;/em&gt; what you tracked.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;run_onchange_&lt;/code&gt; — not a file at all but a script chezmoi runs whenever its content changes. The mechanism behind &quot;re-run the Brewfile when the Brewfile changed&quot;, which the &lt;a href=&quot;/posts/apps/&quot;&gt;apps article&lt;/a&gt; leans on.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So a slice of the source repo maps to target paths like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chezmoi source repo            →  rendered into $HOME
├── dot_zshenv                 →  ~/.zshenv
├── dot_config/
│   └── zsh/
│       ├── dot_zshrc          →  ~/.config/zsh/.zshrc
│       └── conf.d/
│           ├── 00-env.zsh     →  ~/.config/zsh/conf.d/00-env.zsh
│           └── 20-aliases.zsh →  ~/.config/zsh/conf.d/20-aliases.zsh
└── dot_local/
    └── bin/
        └── executable_zhealth →  ~/.local/bin/zhealth  (chmod +x)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The prefixes are declarative metadata: the filename states the intent (hidden, private, executable, exact) and chezmoi enforces it on every apply. This is the same single-source-of-truth idea from the manifesto, applied to dotfiles — the source repo is the one place the desired state of your home directory is written down, and &lt;code&gt;chezmoi apply&lt;/code&gt; reconciles the machine to it.&lt;/p&gt;
&lt;h2&gt;The final form, and how to tell when it breaks&lt;/h2&gt;
&lt;p&gt;When this is in place, the home directory holds exactly one zsh file: &lt;code&gt;~/.zshenv&lt;/code&gt;. That fact is useful precisely because it is checkable. If a &lt;code&gt;~/.zshrc&lt;/code&gt; ever appears in &lt;code&gt;$HOME&lt;/code&gt;, it did not come from me — some installer wrote it, and it is a pollution signal worth investigating, not ignoring.&lt;/p&gt;
&lt;p&gt;Rather than remember to check, I make the check a command. This is a &lt;strong&gt;health-check function&lt;/strong&gt; — a shell function that surfaces drift — and for the home directory I call it &lt;code&gt;zhealth&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 30-functions.zsh — flag stray zsh files in $HOME
zhealth() {
  local stray=(~/.zshrc(N) ~/.zprofile(N) ~/.zlogin(N) ~/.zshrc.*(N))
  if (( ${#stray} )); then
    print -u2 &quot;zhealth: unexpected zsh files in \$HOME:&quot;
    printf &apos;  %s\n&apos; &quot;${stray[@]}&quot; &amp;gt;&amp;amp;2
    return 1
  fi
  print &quot;zhealth: \$HOME is clean — only ~/.zshenv expected&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running &lt;code&gt;zhealth&lt;/code&gt; after installing a new tool is how I catch an installer that decided to &quot;helpfully&quot; drop a &lt;code&gt;~/.zshrc&lt;/code&gt; back into my home directory. The point is not the function itself; it is that &lt;em&gt;drift becomes visible on demand&lt;/em&gt; instead of accumulating silently. The &lt;a href=&quot;/posts/maintenance/&quot;&gt;maintenance article&lt;/a&gt; is entirely about this idea — turning cleanliness from a one-time setup into something a few small functions keep honest.&lt;/p&gt;
&lt;h2&gt;What not to track&lt;/h2&gt;
&lt;p&gt;A declarative dotfiles repo invites a tempting mistake: tracking everything. Some files should stay out of version control on purpose.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.zsh_history&lt;/code&gt; is state, not configuration. It is also private, and it changes on every command, so tracking it would mean an endless stream of meaningless diffs. Leave it local.&lt;/li&gt;
&lt;li&gt;Caches and generated completion dumps (&lt;code&gt;~/.config/zsh/.zcompdump*&lt;/code&gt;) are derived. They rebuild themselves; tracking them just creates churn.&lt;/li&gt;
&lt;li&gt;Credentials, tokens, and anything secret do not belong in a plain dotfiles repo at all. chezmoi has template and secret-manager integration for the rare config file that must contain a secret, but the default answer is &quot;do not put it here&quot;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice this means a couple of ignore rules. &lt;code&gt;.gitignore&lt;/code&gt; in the source repo keeps cruft out of git, and &lt;code&gt;.chezmoiignore&lt;/code&gt; tells chezmoi not to manage specific target paths even if they exist:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# .chezmoiignore
.config/zsh/.zcompdump*
.zsh_history
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The machine-specific fragment, &lt;code&gt;90-local.zsh&lt;/code&gt;, is the designated place for anything that should differ per machine or stay off the network — a work proxy, a local-only alias. It loads last so it can override, and it is the one fragment I deliberately do not track.&lt;/p&gt;
&lt;p&gt;The result is one entry file in &lt;code&gt;$HOME&lt;/code&gt;, a directory of ordered fragments,
chezmoi as the declared state, and &lt;code&gt;zhealth&lt;/code&gt; for the next installer that writes
where it should not.&lt;/p&gt;
</content:encoded></item><item><title>I Rebuilt My Mac Around Four Layers</title><link>https://www.shiinayane.com/en/posts/manifesto/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/manifesto/</guid><description>A DFU restore forced me to decide which tool owns each part of my development environment—and how I would rebuild it.</description><pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A few weeks ago, I put a Mac mini into DFU mode (Device Firmware Update—the state where the machine has no operating system of its own and waits for another Mac to write one onto it) and erased everything.&lt;/p&gt;
&lt;p&gt;There was a stubborn problem I wanted to fix, but I also wanted the machine to feel clean again. I expected a tedious afternoon of reinstalling software. Instead, the reset forced me to work out what “clean” was supposed to mean.&lt;/p&gt;
&lt;p&gt;For a long time I treated it as a matter of installing fewer tools. That never lasted. Six months after setting up a supposedly minimal machine, I would have a &lt;code&gt;~/.zshrc&lt;/code&gt; I no longer understood, three Python interpreters I did not remember choosing, and a &lt;code&gt;pip install&lt;/code&gt; from 2023 living somewhere it should not have been. The real problem was not the number of tools. I could no longer tell which one owned what.&lt;/p&gt;
&lt;p&gt;This is the first of seven articles about the setup I rebuilt after that reset. The rest of the series goes into individual tools; this one records the four-layer model they fit into.&lt;/p&gt;
&lt;h2&gt;Reasonable installs add up&lt;/h2&gt;
&lt;p&gt;The mess usually starts with decisions that make sense on their own.&lt;/p&gt;
&lt;p&gt;I need Python, so I run &lt;code&gt;brew install python&lt;/code&gt;. A later project needs another version, so I add &lt;code&gt;pyenv&lt;/code&gt;. Then a Node project brings in &lt;code&gt;nvm&lt;/code&gt;, and Ruby brings in &lt;code&gt;rbenv&lt;/code&gt;. Each installer adds a few lines to &lt;code&gt;~/.zshrc&lt;/code&gt;, often too quietly for me to notice. A &lt;code&gt;pip install --user&lt;/code&gt; puts packages in a location that shadows what another tool expects.&lt;/p&gt;
&lt;p&gt;None of those choices is obviously broken. The problem appears later, when I have to investigate my own computer to answer a basic question: when I type &lt;code&gt;python&lt;/code&gt;, which Python runs, and why?&lt;/p&gt;
&lt;p&gt;I wanted the answer to come from the structure of the setup, not from whatever happened to win the current &lt;code&gt;PATH&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;The four layers&lt;/h2&gt;
&lt;p&gt;I rebuilt the Mac mini around four layers, starting with the system and ending with the dependencies stored in each repository.&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;my-6&quot;&amp;gt;
&amp;lt;svg viewBox=&quot;0 0 600 330&quot; role=&quot;img&quot; aria-labelledby=&quot;diagram-layers-title&quot; style=&quot;width:100%;height:auto;color:inherit&quot;&amp;gt;
&amp;lt;title id=&quot;diagram-layers-title&quot;&amp;gt;The four-layer stack: System, Runtime version, Package manager, Project dependencies&amp;lt;/title&amp;gt;
&amp;lt;g font-family=&quot;ui-sans-serif, system-ui, sans-serif&quot;&amp;gt;
&amp;lt;!-- Layer 3 --&amp;gt;
&amp;lt;rect x=&quot;10&quot; y=&quot;10&quot; width=&quot;580&quot; height=&quot;66&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.15&quot;/&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;38&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; fill=&quot;var(--primary)&quot;&amp;gt;Layer 3&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;58&quot; font-size=&quot;15&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&amp;gt;Project dependencies&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;570&quot; y=&quot;44&quot; font-size=&quot;13&quot; text-anchor=&quot;end&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;pyproject.toml + uv.lock, package.json + pnpm-lock.yaml&amp;lt;/text&amp;gt;
&amp;lt;!-- Layer 2 --&amp;gt;
&amp;lt;rect x=&quot;10&quot; y=&quot;86&quot; width=&quot;580&quot; height=&quot;66&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.15&quot;/&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;114&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; fill=&quot;var(--primary)&quot;&amp;gt;Layer 2&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;134&quot; font-size=&quot;15&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&amp;gt;Package manager&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;570&quot; y=&quot;120&quot; font-size=&quot;13&quot; text-anchor=&quot;end&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;uv, pnpm, cargo&amp;lt;/text&amp;gt;
&amp;lt;!-- Layer 1 --&amp;gt;
&amp;lt;rect x=&quot;10&quot; y=&quot;162&quot; width=&quot;580&quot; height=&quot;66&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.05&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.15&quot;/&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;190&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; fill=&quot;var(--primary)&quot;&amp;gt;Layer 1&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;210&quot; font-size=&quot;15&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&amp;gt;Runtime version&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;570&quot; y=&quot;196&quot; font-size=&quot;13&quot; text-anchor=&quot;end&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;mise (or a language&apos;s sovereign tool)&amp;lt;/text&amp;gt;
&amp;lt;!-- Layer 0 --&amp;gt;
&amp;lt;rect x=&quot;10&quot; y=&quot;238&quot; width=&quot;580&quot; height=&quot;66&quot; rx=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.08&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.2&quot;/&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;266&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; fill=&quot;var(--primary)&quot;&amp;gt;Layer 0&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;30&quot; y=&quot;286&quot; font-size=&quot;15&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&amp;gt;System&amp;lt;/text&amp;gt;
&amp;lt;text x=&quot;570&quot; y=&quot;272&quot; font-size=&quot;13&quot; text-anchor=&quot;end&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.6&quot;&amp;gt;Homebrew + Xcode Command Line Tools&amp;lt;/text&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;/svg&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 0 — System.&lt;/strong&gt; Homebrew and the Xcode Command Line Tools install command-line tools and graphical applications. They do not install language runtimes. If Homebrew owns Python, the system layer has crossed into the next layer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 1 — Runtime version.&lt;/strong&gt; This is the selected interpreter or compiler version: Python 3.14, Node 22, and so on. I usually let &lt;code&gt;mise&lt;/code&gt; own this layer. It activates a runtime according to the project directory I am in.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 2 — Package manager.&lt;/strong&gt; &lt;code&gt;uv&lt;/code&gt; installs Python packages, &lt;code&gt;pnpm&lt;/code&gt; installs Node packages, and &lt;code&gt;cargo&lt;/code&gt; installs Rust packages. These tools work inside a selected runtime; they do not decide which runtime version is active.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 3 — Project dependencies.&lt;/strong&gt; The manifest and lock files belong to the repository: &lt;code&gt;pyproject.toml&lt;/code&gt; and &lt;code&gt;uv.lock&lt;/code&gt;, or &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;. Another machine should be able to use these files to reproduce the same dependency set.&lt;/p&gt;
&lt;p&gt;The direction of dependency matters. Project dependencies rely on a package manager; the package manager relies on a runtime; the runtime sits on the system. A tool can serve its own layer without quietly taking over the layer above or below it.&lt;/p&gt;
&lt;h2&gt;One owner for each kind of resource&lt;/h2&gt;
&lt;p&gt;The practical rule is simple: each kind of resource gets one owner.&lt;/p&gt;
&lt;p&gt;Runtime versions have one owner. A project&apos;s libraries have one owner. System applications have one owner. If I ask where a Python came from, what pins a dependency, or why an application is installed, there should be one place to look.&lt;/p&gt;
&lt;p&gt;This is also why counting tools did not help me. Two tools competing to manage Python create more confusion than five tools with separate jobs. What matters is whether their ownership overlaps.&lt;/p&gt;
&lt;p&gt;The owner is not always &lt;code&gt;mise&lt;/code&gt;. Rust has &lt;code&gt;rustup&lt;/code&gt;, and Swift has Xcode. Both are strong, official toolchain managers maintained as part of their language ecosystems. When an official tool is good enough to own the runtime and toolchain, I let it do so instead of putting &lt;code&gt;mise&lt;/code&gt; in front of it. The fifth article in this series explains the test I use for that choice.&lt;/p&gt;
&lt;p&gt;“Single source of truth” therefore describes the ownership, not a favorite product. The tools can change without changing the model.&lt;/p&gt;
&lt;h2&gt;The recovery test&lt;/h2&gt;
&lt;p&gt;After the reset, recoverability became the useful test.&lt;/p&gt;
&lt;p&gt;I should be able to delete caches and build artifacts—&lt;code&gt;rm -rf&lt;/code&gt; the lot—and rebuild any project on the machine with one command. If that works, the caches really are disposable. If it does not, then something I called “derived” is carrying state that was never declared. I treat that as a layering problem and find the missing source of truth.&lt;/p&gt;
&lt;p&gt;The declared state lives in manifests, lock files, and dotfiles. Together they should be enough to recreate the working state from close to nothing. This makes the setup predictable as well: I know which tool to inspect before I start debugging &lt;code&gt;PATH&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There are limits to this setup. It is for one person on macOS who controls the whole machine. A team, a shared server, a locked-down corporate laptop, or another operating system can change the trade-offs substantially. &lt;code&gt;mise&lt;/code&gt; and &lt;code&gt;uv&lt;/code&gt; are also recent tools, and something else may replace them. Those constraints do not make the setup universal; they explain the environment in which I use it.&lt;/p&gt;
&lt;h2&gt;The next six articles&lt;/h2&gt;
&lt;p&gt;The remaining articles take one part of the setup at a time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dotfiles without &lt;code&gt;.zshrc&lt;/code&gt;&lt;/strong&gt; covers &lt;code&gt;ZDOTDIR&lt;/code&gt; and &lt;code&gt;chezmoi&lt;/code&gt;, making shell configuration declarative, exposing drift, and keeping the home directory from becoming a configuration dump.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Brewfile compromise&lt;/strong&gt; explains why eventual consistency is acceptable at the system layer and how a reconcile step keeps it honest without pretending it is strict.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python, mise, and uv&lt;/strong&gt; deals with &lt;code&gt;uv&lt;/code&gt; quietly installing its own Python behind &lt;code&gt;mise&lt;/code&gt; and the single setting that stops the conflict.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;When to let the official tool win&lt;/strong&gt; gives the test for choosing a language&apos;s own version manager instead of &lt;code&gt;mise&lt;/code&gt; at Layer 1.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One architecture, many languages&lt;/strong&gt; applies the same four layers to Node, Java, Swift, Rust, Go, and Ruby.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configuration is easy; maintenance is the work&lt;/strong&gt; covers health checks, subtraction, and the ongoing work required to keep the environment understandable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;a href=&quot;/series/sovereign-tools/&quot;&gt;series index&lt;/a&gt; has the complete reading order and current status.&lt;/p&gt;
</content:encoded></item><item><title>How I Learned to Read Swift Function Signatures</title><link>https://www.shiinayane.com/en/posts/reading-swift-function-signatures/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/reading-swift-function-signatures/</guid><pubDate>Thu, 07 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When I started learning Swift, I would open Apple’s DocC documentation, see something like this, and skip straight to the examples:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func compactMap&amp;lt;ElementOfResult&amp;gt;(
    _ transform: (Self.Element) throws -&amp;gt; ElementOfResult?
) rethrows -&amp;gt; [ElementOfResult]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My honest reaction was: “Why the hell does one function need to look this terrifying?”&lt;/p&gt;
&lt;p&gt;Python had taught me to expect APIs that looked more like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;map(func, arr)
filter(func, arr)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Swift version seemed to pack generics, &lt;code&gt;Optional&lt;/code&gt;, associated types, protocol constraints, &lt;code&gt;throws&lt;/code&gt;, and &lt;code&gt;rethrows&lt;/code&gt; into the same line. I treated all of that as syntax to get past, not information worth reading.&lt;/p&gt;
&lt;p&gt;That worked for copying an example. It did not help much when I had to understand an unfamiliar API on my own.&lt;/p&gt;
&lt;h2&gt;Reading the arrows first&lt;/h2&gt;
&lt;p&gt;The signature that made this easier was &lt;code&gt;map&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func map&amp;lt;T&amp;gt;(
    _ transform: (Element) throws -&amp;gt; T
) rethrows -&amp;gt; [T]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I no longer try to understand every symbol at once. I start with the value flowing through the function:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the method works on a collection of &lt;code&gt;Element&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;its closure accepts one &lt;code&gt;Element&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the closure produces some type &lt;code&gt;T&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;map&lt;/code&gt; collects those values into &lt;code&gt;[T]&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the closure is allowed to throw;&lt;/li&gt;
&lt;li&gt;because the method is &lt;code&gt;rethrows&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt; throws only when the closure passed to it throws.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Read that way, the type signature is already a compact description of the operation. The generic name does not tell me what &lt;code&gt;T&lt;/code&gt; is, but it tells me that the input element and output value do not have to be the same type.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;map&lt;/code&gt;, &lt;code&gt;compactMap&lt;/code&gt;, and &lt;code&gt;flatMap&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;These three methods were where signatures first became more useful to me than memorized examples.&lt;/p&gt;
&lt;p&gt;At first, I only knew the call pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;arr.map { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I compared the return types and the closure return types. &lt;code&gt;compactMap&lt;/code&gt; has an extra &lt;code&gt;?&lt;/code&gt; in one crucial place:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func compactMap&amp;lt;ElementOfResult&amp;gt;(
    _ transform: (Element) throws -&amp;gt; ElementOfResult?
) rethrows -&amp;gt; [ElementOfResult]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The transform may return either an &lt;code&gt;ElementOfResult&lt;/code&gt; or &lt;code&gt;nil&lt;/code&gt;, but the final array contains non-optional &lt;code&gt;ElementOfResult&lt;/code&gt; values. In practical terms, it transforms every input and discards the &lt;code&gt;nil&lt;/code&gt; results. That is the part I had previously remembered only as “use this when optionals are involved.”&lt;/p&gt;
&lt;p&gt;The sequence overload of &lt;code&gt;flatMap&lt;/code&gt; looks heavier:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func flatMap&amp;lt;SegmentOfResult&amp;gt;(
    _ transform: (Element) throws -&amp;gt; SegmentOfResult
) rethrows -&amp;gt; [SegmentOfResult.Element]
where SegmentOfResult : Sequence
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;where&lt;/code&gt; clause says that each transformed result must itself be a &lt;code&gt;Sequence&lt;/code&gt;. The return type then gives away the rest: the method returns an array of that inner sequence’s elements, not an array of sequences.&lt;/p&gt;
&lt;p&gt;So a nested value such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[1], [2,2], [3,3,3]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;can become:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[1,2,2,3,3,3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once I saw that relationship in the types, &lt;code&gt;flatMap&lt;/code&gt; stopped feeling like an “advanced map.” For this overload, it means transforming elements into sequences and flattening those sequences into one array.&lt;/p&gt;
&lt;h2&gt;Small signatures also carry policy&lt;/h2&gt;
&lt;p&gt;Not every useful signature is full of generics. This one tells me almost everything I need to know:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func popLast() -&amp;gt; Element?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result is optional, so an empty collection is represented by &lt;code&gt;nil&lt;/code&gt;. I do not need to guess whether absence is a valid result.&lt;/p&gt;
&lt;p&gt;That also makes the difference between these two names worth noticing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;removeLast()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;popLast()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;removeLast()&lt;/code&gt; requires the collection to be nonempty and traps otherwise. &lt;code&gt;popLast()&lt;/code&gt; handles the empty case by returning &lt;code&gt;nil&lt;/code&gt;. The names hint at the difference, while the optional return type makes it explicit.&lt;/p&gt;
&lt;h2&gt;A small vocabulary covers a lot&lt;/h2&gt;
&lt;p&gt;After a while, I found that many intimidating signatures were combinations of a few forms:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(Element) -&amp;gt; T
(Element) -&amp;gt; T?
Sequence&amp;lt;Element&amp;gt;
where T : BinaryInteger
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I read them as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a function from &lt;code&gt;Element&lt;/code&gt; to &lt;code&gt;T&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a function that may or may not produce a &lt;code&gt;T&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a sequence whose element type is &lt;code&gt;Element&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a generic &lt;code&gt;T&lt;/code&gt; constrained to conform to &lt;code&gt;BinaryInteger&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is more useful than memorizing one call site because the same relationships appear across the standard library and third-party APIs.&lt;/p&gt;
&lt;h2&gt;Where the complexity goes&lt;/h2&gt;
&lt;p&gt;Swift often asks me to accept:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;more complexity during compilation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;rather than leave:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;more uncertainty at runtime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The trade is not absolute, but the compiler and the type system can catch or represent some of that uncertainty earlier.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Optional&lt;/code&gt;, generics, protocol-oriented APIs, type constraints, and explicit error handling all contribute to the longer signatures that initially put me off. They do not make every invalid state impossible, and a signature cannot replace all documentation. But it can expose input, output, failure, and type relationships before I run the code.&lt;/p&gt;
&lt;p&gt;I still open examples when a type is unfamiliar. The difference is that I now read the signature first, then use the example to confirm the model I got from it.&lt;/p&gt;
</content:encoded></item><item><title>The Raptor Dilemma</title><link>https://www.shiinayane.com/en/posts/the-raptor-dilemma/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/the-raptor-dilemma/</guid><description>SwiftUI Dreams vs Web Reality</description><pubDate>Sat, 02 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;After comparing Astro, Saga, Toucan, Publish, Ignite, and Raptor, I wrote in &lt;a href=&quot;/posts/swift-for-static-sites/&quot;&gt;Swift for Static Sites&lt;/a&gt; that Swift seemed most useful for content modeling and generation, while HTML, CSS, and JavaScript should remain the UI layer.&lt;/p&gt;
&lt;p&gt;That was based partly on reading documentation and examples. Then I built an actual Raptor theme.&lt;/p&gt;
&lt;p&gt;Raptor was pleasant while the pages were simple. As soon as I tried to reproduce the details of a custom blog theme, however, I was thinking in Swift and debugging in CSS. That gap—not a particular bug—was the real problem.&lt;/p&gt;
&lt;h2&gt;Why Raptor felt good at first&lt;/h2&gt;
&lt;p&gt;Raptor is more than a static site generator. It provides a Swift-first model for pages, layouts, themes, styles, and components. Instead of writing this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;article class=&quot;post-card&quot;&amp;gt;
  &amp;lt;h2&amp;gt;Title&amp;lt;/h2&amp;gt;
&amp;lt;/article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I could write:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VStack {
    Text(post.title)
}
.style(PostCardStyle())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The appeal is easy to understand: type safety, composability, one implementation language, and a developer experience that resembles SwiftUI. In theory, the same ideas that made SwiftUI productive on Apple platforms can make a website pleasant to build.&lt;/p&gt;
&lt;p&gt;For simple pages, they do. These components are concise:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Text(&quot;Hello&quot;)
Button(&quot;Read more&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Landing pages, documentation sites, and basic blogs fit the model well. If most of the interface can be described as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Text + Button + Grid + Card
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Raptor is expressive and fast.&lt;/p&gt;
&lt;h2&gt;A custom theme exposes the boundary&lt;/h2&gt;
&lt;p&gt;The trouble started with ordinary theme details rather than an exotic browser feature.&lt;/p&gt;
&lt;h3&gt;A metadata row&lt;/h3&gt;
&lt;p&gt;In CSS, a row with categories on one side and a timestamp on the other can be expressed directly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.meta {
  display: flex;
  justify-content: space-between;
}
.meta time {
  float: right;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Raptor version rewrites the same intent as a component layout:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HStack {
    categories
    Spacer().axis(.horizontal)
    time
}
.style(Property.width(.percent(100)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This version is fine, and arguably cleaner. The abstraction still matches the problem.&lt;/p&gt;
&lt;h3&gt;A pseudo-element used for decoration&lt;/h3&gt;
&lt;p&gt;The next detail was a small accent bar:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.recent-info::after {
  content: &apos;&apos;;
  width: 13%;
  height: 5px;
  background: var(--accent);
  position: relative;
  bottom: -6px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Raptor, I represented it as a component:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RecentInfoAccentBar()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That works, but the meaning has changed. CSS treats the bar as a decorative layer attached to an element. The component tree treats it as structure. I was no longer only styling the existing document; I was changing the document structure to reproduce a styling technique.&lt;/p&gt;
&lt;h3&gt;A hover relationship&lt;/h3&gt;
&lt;p&gt;CSS selectors can also describe a relationship between two elements:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.card:hover .read-more {
  background-color: var(--bg-hover);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There was no equally clean expression for this in the Raptor component model. I either had to redesign the interaction or coordinate the relevant styles manually. The browser already understands the parent-hover/descendant relationship; the Swift abstraction did not make that relationship easier to state.&lt;/p&gt;
&lt;h3&gt;A negative margin&lt;/h3&gt;
&lt;p&gt;Even a familiar layout adjustment remains a CSS operation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.read-more {
  margin-top: -21px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Raptor can express it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.style(Property.marginTop(.px(-21)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But by this point I had not escaped CSS. I had translated CSS into Swift syntax.&lt;/p&gt;
&lt;p&gt;The mismatch can be summarized like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;They think in Swift,
but debug in CSS.

They write components,
but fight layout at the DOM level.

They define styles,
but still rely on raw CSS properties.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The theme ended up with two mental models&lt;/h2&gt;
&lt;p&gt;A typical card began as a useful Swift component:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PostListItem {
    PostMeta(...)
    PostTitle(...)
    PostExcerpt(...)
    PostReadMore(...)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reusable styles kept the first layer tidy:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.style(PostCardStyle())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the visual adjustments accumulated:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.style(Property.marginTop(.px(12)))
.style(Property.paddingLeft(.px(8)))
.style(Property.fontSize(.px(14)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The generated page still runs on HTML, CSS, and JavaScript, so browser behavior remains the source of truth. The working model became:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Structure → Swift
Styling → CSS concepts
Layout debugging → Browser DevTools
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The daily experience was therefore:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Half SwiftUI
Half traditional Web
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Raptor had moved the complexity, not removed it. I wrote components in Swift, inspected the resulting DOM, and fixed layout using knowledge of CSS.&lt;/p&gt;
&lt;h2&gt;Saga keeps the Web boundary visible&lt;/h2&gt;
&lt;p&gt;Saga takes a different approach. A template can still use Swift for composition while leaving the generated HTML and its classes obvious:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;article(class: &quot;mx-auto max-w-3xl px-6 py-12&quot;) {
  h1(class: &quot;text-4xl font-bold tracking-tight&quot;) {
    item.title
  }
  div(class: &quot;prose&quot;) {
    raw(item.body)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The division of responsibility is explicit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Swift → structure + composition
HTML → structure
CSS → styling
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is less translation between the code I author and the page I debug. For a visually demanding site, I find this easier to reason about than a SwiftUI-style layer over the browser.&lt;/p&gt;
&lt;h2&gt;Where I would use Raptor&lt;/h2&gt;
&lt;p&gt;Swift can express a Web UI; the question is how much distance from the rendering platform is useful.&lt;/p&gt;
&lt;p&gt;For a generic interface:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Abstraction helps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a highly customized interface:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Abstraction fights the platform
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Raptor is strongest in the first case: structured pages made from familiar components, where its type safety and composition do real work. It becomes less comfortable in the second, precisely when selectors, pseudo-elements, and small layout adjustments matter most.&lt;/p&gt;
&lt;p&gt;The broader stack still has two layers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Rendering layer → HTML / CSS / JS
Authoring layer → Swift / React / Astro
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React and Astro stay relatively close to the rendering layer. Raptor deliberately moves farther away, which gives it a distinctive SwiftUI-like experience but also creates the mismatch I hit while building the theme.&lt;/p&gt;
&lt;p&gt;My practical boundary after this project is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Swift for logic → great
Swift for UI abstraction → situational
HTML/CSS/JS → still the source of truth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I would still choose Raptor for a simple, structured Swift site. For a visual-heavy theme, I would rather use a Saga-style approach and keep the browser’s own UI model visible.&lt;/p&gt;
</content:encoded></item><item><title>Swift for Static Sites</title><link>https://www.shiinayane.com/en/posts/swift-for-static-sites/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/swift-for-static-sites/</guid><description>SwiftUI Purism vs Web-Native Reality</description><pubDate>Fri, 01 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently tried to answer a practical question: if I move a visually complex blog theme to a Swift static site generator, which one should I use?&lt;/p&gt;
&lt;p&gt;I went through &lt;a href=&quot;https://github.com/loopwerk/Saga&quot;&gt;Saga&lt;/a&gt;, &lt;a href=&quot;https://github.com/toucansites/toucan&quot;&gt;Toucan&lt;/a&gt;, &lt;a href=&quot;https://github.com/JohnSundell/Publish&quot;&gt;Publish&lt;/a&gt;, &lt;a href=&quot;https://github.com/twostraws/Ignite&quot;&gt;Ignite&lt;/a&gt;, and &lt;a href=&quot;https://github.com/raptor-build/raptor&quot;&gt;Raptor&lt;/a&gt;, then compared them with Web-native tools such as &lt;a href=&quot;https://github.com/withastro/astro&quot;&gt;Astro&lt;/a&gt;, &lt;a href=&quot;https://github.com/hexojs/hexo&quot;&gt;Hexo&lt;/a&gt;, &lt;a href=&quot;https://github.com/gohugoio/hugo&quot;&gt;Hugo&lt;/a&gt;, and &lt;a href=&quot;https://github.com/jekyll/jekyll&quot;&gt;Jekyll&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After reading their source and examples and trying them myself, I found that the choice depends less on feature count than on where Swift sits in the stack. A SwiftUI-like component API and a Swift program that generates ordinary HTML may look similar in a small demo, but they behave very differently once the design needs custom CSS.&lt;/p&gt;
&lt;h2&gt;The browser is the UI runtime&lt;/h2&gt;
&lt;p&gt;SwiftUI works on iOS because its abstractions connect to a system UI runtime:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iOS:
System UI runtime
→ SwiftUI Button / VStack / NavigationStack
→ Your Swift code
Web:
Browser runtime (DOM + CSS + JS)
→ HTML &amp;lt;button&amp;gt; / &amp;lt;div&amp;gt; / &amp;lt;article&amp;gt;
→ CSS grid / flex / selectors
→ Your HTML/CSS/JS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A SwiftUI &lt;code&gt;Button&lt;/code&gt; is connected to platform behavior including accessibility, focus, animation, and input. A button declared through Raptor or Ignite eventually becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;button class=&quot;...&quot;&amp;gt;Save&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;plus CSS. The browser does not know about the Swift component that produced it; it only receives HTML, CSS, and JavaScript.&lt;/p&gt;
&lt;p&gt;This is not an argument against declarative UI. React, Vue, and Astro are declarative too. The important difference is the object being declared and how closely it matches the browser&apos;s own model.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;What you declare&lt;/th&gt;
&lt;th&gt;Distance from the platform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SwiftUI&lt;/td&gt;
&lt;td&gt;Native UI tree&lt;/td&gt;
&lt;td&gt;Very close&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React / Vue&lt;/td&gt;
&lt;td&gt;DOM / component tree&lt;/td&gt;
&lt;td&gt;Very close&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Astro&lt;/td&gt;
&lt;td&gt;HTML + islands&lt;/td&gt;
&lt;td&gt;Very close&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Saga / Publish&lt;/td&gt;
&lt;td&gt;HTML output tree&lt;/td&gt;
&lt;td&gt;Close&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ignite&lt;/td&gt;
&lt;td&gt;Swift components (Bootstrap-like)&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raptor&lt;/td&gt;
&lt;td&gt;SwiftUI-like UI + style system&lt;/td&gt;
&lt;td&gt;Farther&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;SwiftUI maps closely to its platform. React still models the DOM, and Astro treats HTML as a first-class part of the component. Ignite and Raptor move the working model farther upward into a Swift component tree. That can be pleasant for a simple page, but the extra translation becomes visible in a heavily customized theme.&lt;/p&gt;
&lt;h2&gt;Where the component DSL stops helping&lt;/h2&gt;
&lt;p&gt;Ignite-style components are convenient for straightforward UI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Text(&quot;Hello&quot;)
Button(&quot;Read More&quot;)
Grid {
  Card { ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With Bootstrap underneath, layout, spacing, responsiveness, and basic visual hierarchy arrive quickly. That is a good fit for documentation, portfolios, basic blogs, and other sites that can stay near the framework&apos;s built-in vocabulary.&lt;/p&gt;
&lt;p&gt;A migrated visual theme is usually less cooperative. It may depend on CSS such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.card::before
.sidebar:has(.active)
grid-template-columns: minmax(0, 1fr) 18rem
position: sticky
backdrop-filter
mask-image
container queries
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the built-in components no longer express the design, the Swift code falls back to lower-level HTML wrappers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Tag(&quot;aside&quot;) { ... }
.class(&quot;layout-shell__sidebar&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CSS still has to be written. The page is now split between a SwiftUI-style vocabulary and direct HTML and CSS, so the abstraction no longer removes much work.&lt;/p&gt;
&lt;p&gt;The useful range for this approach is still real:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Simple sites
Docs
Portfolios
Basic blogs
Bootstrap-like layouts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The difficulty begins when a theme originally built in Astro or Hexo relies on precise selectors, pseudo-elements, layout rules, and browser-specific behavior. This is a boundary of the abstraction, not a defect in declarative UI or in the frameworks themselves.&lt;/p&gt;
&lt;h2&gt;Saga keeps the boundary visible&lt;/h2&gt;
&lt;p&gt;Saga takes a different route. It uses Swift for the parts where Swift is useful and leaves browser concerns in Web-native forms:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Swift:
Content model
Pipeline
Generation logic
Type safety

Web:
HTML structure
CSS styling
JavaScript behavior
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A template can look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;article(class: &quot;mx-auto max-w-3xl px-6 py-12&quot;) {
  h1(class: &quot;text-4xl font-bold tracking-tight&quot;) {
    item.title
  }
  div(class: &quot;prose prose-slate dark:prose-invert&quot;) {
    raw(item.body)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is Swift generating HTML, not an attempt to reproduce SwiftUI in a browser. The division remains easy to inspect:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Swift-native:
Types, functions, composition

Web-native:
HTML, CSS, browser semantics
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For this kind of project, that directness matters more than having a larger UI abstraction.&lt;/p&gt;
&lt;h2&gt;Tailwind makes Saga more practical&lt;/h2&gt;
&lt;p&gt;Without Tailwind, a Saga template resembles conventional HTML with named CSS classes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;article(class: &quot;post-card&quot;) {
  h2(class: &quot;post-card__title&quot;) {
    item.title
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With Tailwind, the layout and styling remain next to the generated HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;article(class: &quot;group rounded-3xl border border-slate-200 bg-white p-6 shadow-sm transition hover:-translate-y-1 hover:shadow-lg&quot;) {
  h2(class: &quot;text-2xl font-semibold tracking-tight&quot;) {
    a(href: post.url) {
      post.title
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is no need to translate each CSS concept into a Swift modifier API. Tailwind is still CSS, expressed through utility classes, so the browser-facing model stays recognizable.&lt;/p&gt;
&lt;h2&gt;Raptor and Ignite fit different jobs&lt;/h2&gt;
&lt;p&gt;Raptor is more ambitious than a conventional static site generator. It defines a broad site model:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Site
Page
PostPage
CategoryPage
Layout
Theme
Style
PostWidget
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also integrates with Vapor for server-side rendering. That makes it interesting for Swift-first content platforms, static and dynamic hybrid sites, and projects that need backend integration.&lt;/p&gt;
&lt;p&gt;The site model does not automatically make a complex front end easier to express, however. When the theme exceeds its UI and style system, the implementation returns to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Tag + Div + Class + CSS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At that point I would want the higher-level model to solve a separate problem—content architecture or server integration, for example—because it is no longer reducing the front-end work.&lt;/p&gt;
&lt;p&gt;Ignite makes a more pragmatic trade:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Swift API + Bootstrap
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is fast for a small site, portfolio, or documentation project. The trade-off is that Bootstrap&apos;s structure and visual assumptions become harder to hide when the site needs a distinctive theme.&lt;/p&gt;
&lt;h2&gt;Why I would still use Astro for the complex theme&lt;/h2&gt;
&lt;p&gt;If Swift is not itself a project requirement, Astro remains the safer choice for this particular job:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTML, CSS, and JavaScript are first-class;&lt;/li&gt;
&lt;li&gt;its components stay close to browser primitives;&lt;/li&gt;
&lt;li&gt;Tailwind integration is straightforward;&lt;/li&gt;
&lt;li&gt;Content Collections provide structure;&lt;/li&gt;
&lt;li&gt;the ecosystem is mature.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That does not make Swift incapable of building websites. It means the cost of the Swift abstraction has to buy something the project needs.&lt;/p&gt;
&lt;p&gt;The two Swift approaches I compared can be summarized as:&lt;/p&gt;
&lt;h3&gt;SwiftUI-style Web DSL (Raptor / Ignite)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Swift expresses UI
→ translated into HTML/CSS
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Swift-native generation + Web-native UI (Saga)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Swift handles logic and structure
HTML/CSS/JS express the UI
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a highly customized visual site, I prefer the second model. Swift still provides types, functions, composition, content modeling, and generation logic, while HTML, CSS, and JavaScript retain control of the UI.&lt;/p&gt;
&lt;p&gt;My practical shortlist after the comparison is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Not using Swift: Astro + Tailwind
Using Swift seriously: Saga + Tailwind
Quick Swift site: Ignite
Exploring Swift Web frameworks: Raptor
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Building a Personal Website in Swift</title><link>https://www.shiinayane.com/en/posts/building-personal-website-in-swift/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/building-personal-website-in-swift/</guid><description>I Thought It Was Easy… Until It Wasn’t</description><pubDate>Tue, 21 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;While building this site with Raptor, I needed distinct styles for post dates, navigation labels, tags, and other small pieces of text. I expected this to be a minor theme change. Instead, I ended up modifying the framework before realizing that I was working in the wrong layer.&lt;/p&gt;
&lt;p&gt;Raptor is a static site generator written in Swift. Its layouts use Swift rather than HTML templates or JSX:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VStack {
    Text(&quot;Hello, world!&quot;)
    Text(&quot;Welcome to my site&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also provides a theme system for typography and colors:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.font(.title1)
.fontSize(36, for: .title1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That felt expressive and type-safe, and using Swift for a site was genuinely fun. The trouble started when the design needed more than the built-in text roles.&lt;/p&gt;
&lt;h2&gt;The roles missing from a real site&lt;/h2&gt;
&lt;p&gt;Raptor&apos;s typography system has a fixed set of roles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;body&lt;/li&gt;
&lt;li&gt;title1 … title6&lt;/li&gt;
&lt;li&gt;codeBlock&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A real site also has post metadata, navigation labels, tags, categories, buttons, and links. In Hugo, Hexo, or a hand-written template, I would give those elements class names and move on:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;span class=&quot;post-meta&quot;&amp;gt;April 20&amp;lt;/span&amp;gt;
&amp;lt;a class=&quot;nav-label&quot;&amp;gt;Archive&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;.post-meta {
  font-size: 12px;
  color: gray;
}
.nav-label {
  font-weight: bold;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Raptor had no equivalent built-in roles for:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.postMeta
.navLabel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My first reaction was to add them.&lt;/p&gt;
&lt;h2&gt;Extending Theme&lt;/h2&gt;
&lt;p&gt;I designed an API that looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Text(&quot;April 20&quot;).textRole(.postMeta)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The theme would configure each custom role:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.fontSize(12, for: .postMeta)
.fontWeight(.medium, for: .postMeta)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Raptor would then generate:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.text-role-post-meta {
  font-size: 12px;
  font-weight: 500;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and attach the class to the rendered HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;p class=&quot;text-role-post-meta&quot;&amp;gt;April 20&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After digging through the framework, I modified the theme configuration, updated CSS generation, and patched the rendering logic. The implementation worked: custom roles went through the theme system, supported light and dark mode, generated CSS automatically, and appeared in the final HTML.&lt;/p&gt;
&lt;p&gt;The code was functional, but the model still felt wrong.&lt;/p&gt;
&lt;p&gt;First, it created two competing ways to describe text:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.font(.title1)        // built-in
.textRole(.postMeta)  // custom
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Second, the two APIs did not carry the same meaning. In Raptor:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.font(.title1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;selects an HTML tag such as &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; and applies its styling. My custom API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.textRole(.navLabel)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;only applied styling. HTML structure and visual style are separate concerns, but I had added another typography API without resolving that distinction.&lt;/p&gt;
&lt;p&gt;Eventually I was writing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.tag(.h1)
.textRole(.navLabel)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which was just a more elaborate way to express:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h1 class=&quot;nav-label&quot;&amp;gt;&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The type-safe API was no longer removing complexity. It was recreating HTML concepts with extra steps.&lt;/p&gt;
&lt;h2&gt;The abstraction was already there&lt;/h2&gt;
&lt;p&gt;I checked other static site generators to see whether they modeled this problem at the framework level. Hugo, for example, simply allows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;p class=&quot;post-meta&quot;&amp;gt;&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and leaves the rest to CSS.&lt;/p&gt;
&lt;p&gt;That comparison sent me back into Raptor&apos;s design and source code. Theme is meant for global design tokens such as typography, colors, and spacing. A semantic style like “post metadata” does not need to become another global text role.&lt;/p&gt;
&lt;p&gt;Raptor already has a separate abstraction for that: &lt;code&gt;Style&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct PostMetaStyle: Style {
    func style(content: Content, environment: EnvironmentConditions) -&amp;gt; Content {
        content
            .font(.caption)
            .foregroundStyle(.secondary)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It can be applied directly to the content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Text(&quot;April 20&quot;)
    .style(PostMetaStyle())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This serves the same purpose as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;p class=&quot;post-meta&quot;&amp;gt;April 20&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;but keeps the implementation reusable, composable, and type-safe.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Style&lt;/code&gt; also receives environment conditions, including the light or dark color scheme, active theme, contrast settings, and layout conditions. A semantic style can therefore adapt without becoming part of the theme&apos;s role system:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct PostMetaStyle: Style {
    func style(content: Content, environment: EnvironmentConditions) -&amp;gt; Content {
        if environment.colorScheme == .dark {
            content.foregroundStyle(.gray)
        } else {
            content.foregroundStyle(.secondary)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The boundary I had missed was simple: Theme defines global tokens; &lt;code&gt;Style&lt;/code&gt; packages reusable semantic styling. The framework already supported what I wanted—I had just started by changing its internals instead of using the abstraction intended for the job.&lt;/p&gt;
</content:encoded></item><item><title>Recreating Apple Music’s Disappearing Navigation Title in SwiftUI</title><link>https://www.shiinayane.com/en/posts/apple-music-style-navigation-titles/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/apple-music-style-navigation-titles/</guid><pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In Apple Music on iOS 26, scrolling can make the large navigation title and its toolbar content disappear. It does not follow the large-title behavior that iOS has used since iOS 11.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/apple-music-style-navigation-titles/navigation-title-collapse.png&quot; alt=&quot;example in Apple Music&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The usual transition is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Large Title
   ↓ scroll
Small Navigation Title
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apple Music instead looks more like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Large Title
   ↓ scroll
(no title)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is no public SwiftUI modifier dedicated to this interaction. Depending on how closely the result needs to match, I would use one of two implementations: build a custom header and control it from the scroll position, or hide only the compact title with an empty principal toolbar item.&lt;/p&gt;
&lt;h2&gt;The controllable version: own the header&lt;/h2&gt;
&lt;p&gt;A full implementation can combine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a custom top header inserted with &lt;code&gt;safeAreaInset(edge: .top)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;scroll detection through &lt;code&gt;ScrollGeometry&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;separate visibility rules for the title and other toolbar items&lt;/li&gt;
&lt;li&gt;opacity and offset animations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SwiftUI already exposes the pieces:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.onScrollGeometryChange(...)
.safeAreaInset(...)
.toolbar(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;They are building blocks rather than one navigation-bar option. A minimal version can track the vertical content offset and hide the header after a threshold:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct CollapsingHeaderView: View {

    @State private var headerHidden = false

    var body: some View {
        ScrollView {
            VStack {
                ForEach(0..&amp;lt;50) { i in
                    Text(&quot;Row \(i)&quot;)
                        .frame(maxWidth: .infinity)
                        .padding()
                }
            }
        }
        .onScrollGeometryChange(for: CGFloat.self) { geometry in
            geometry.contentOffset.y
        } action: { _, offset in
            headerHidden = offset &amp;gt; 40
        }
        .safeAreaInset(edge: .top) {
            header
                .opacity(headerHidden ? 0 : 1)
                .animation(.easeInOut, value: headerHidden)
        }
    }

    private var header: some View {
        HStack {
            Text(&quot;Library&quot;)
                .font(.largeTitle.bold())

            Spacer()

            Image(systemName: &quot;person.crop.circle&quot;)
        }
        .padding()
        .background(.ultraThinMaterial)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works with content built from &lt;code&gt;ScrollView&lt;/code&gt;, &lt;code&gt;List&lt;/code&gt;, or &lt;code&gt;LazyVStack&lt;/code&gt;, and the header can contain any layout or transition the screen needs. The hiding threshold is also under the app’s control.&lt;/p&gt;
&lt;p&gt;The tradeoff is that the app now owns the behavior, including scroll direction, toolbar layout, refresh interactions, and nested navigation-stack edge cases. That is reasonable for a screen that needs a precise result, but excessive if the only goal is to remove the compact title.&lt;/p&gt;
&lt;h2&gt;The small shortcut: empty the compact title&lt;/h2&gt;
&lt;p&gt;For the lighter version, place an empty view in the principal toolbar position:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.toolbar {
    ToolbarItem(placement: .principal) {
        Text(&quot;&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keep the normal large title:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.navigationTitle(&quot;Library&quot;)
.navigationBarTitleDisplayMode(.large)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting transition is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Large Title
   ↓ scroll
(empty)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The large title still appears at the top of the list. Once it collapses, the principal item supplies an empty compact title:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NavigationStack {
    List(items) { item in
        Text(item.title)
    }
    .navigationTitle(&quot;Library&quot;)
    .navigationBarTitleDisplayMode(.large)
    .toolbar {
        ToolbarItem(placement: .principal) {
            Text(&quot;&quot;)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is enough to make a standard &lt;code&gt;NavigationStack&lt;/code&gt; resemble Apple Music without introducing scroll state.&lt;/p&gt;
&lt;h2&gt;What the shortcut actually changes&lt;/h2&gt;
&lt;p&gt;SwiftUI normally transitions between these title states:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Large Navigation Title
        ↓
Compact Navigation Title
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The principal toolbar item replaces the content shown in the compact position. Making that item empty changes the visible result from:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Large Title
   ↓
Small Title
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Large Title
   ↓
(blank space)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The navigation bar has not been removed. Its compact title simply renders no content, which is why the effect looks convincing while requiring almost no code.&lt;/p&gt;
&lt;p&gt;That distinction also defines the limits of the trick:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the navigation bar still exists&lt;/li&gt;
&lt;li&gt;other toolbar items may continue to occupy space&lt;/li&gt;
&lt;li&gt;the result depends on current SwiftUI rendering behavior and may change in a future release&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the whole header and its layout must disappear, the custom scroll-driven version is the appropriate one. If an empty compact title is visually sufficient, the toolbar shortcut is much cheaper.&lt;/p&gt;
&lt;h2&gt;An official API may eventually replace both&lt;/h2&gt;
&lt;p&gt;Apple has introduced UI behavior in system apps before exposing related public APIs. &lt;code&gt;.searchable&lt;/code&gt;, large navigation titles, and the tab-bar minimization behavior shown in Apple Music followed that general pattern.&lt;/p&gt;
&lt;p&gt;A future SwiftUI API could conceivably look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.navigationBarCollapseBehavior(.onScroll)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.toolbarScrollVisibility(.hidden)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those modifiers are only illustrative; SwiftUI does not currently provide them.&lt;/p&gt;
</content:encoded></item><item><title>Use Docker to Deploy a Minecraft server</title><link>https://www.shiinayane.com/en/posts/deploy-minecraft-server-with-docker/</link><guid isPermaLink="true">https://www.shiinayane.com/en/posts/deploy-minecraft-server-with-docker/</guid><pubDate>Thu, 15 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Recently, I wanted to start a Minecraft server on my Mac. It’s quite simple to run &lt;code&gt;./run.bash&lt;/code&gt; in the server directory, but obviously not elegant enough since you have to keep the terminal and Java process running in the background. Screen can hide the terminal, but it has no idea about the Java service itself.&lt;/p&gt;
&lt;p&gt;Docker is a powerful tool for deploying complicated services on almost any device. So I wondered if I could use Docker for the Minecraft server too.&lt;/p&gt;
&lt;p&gt;And luckily, we have &lt;code&gt;itzg/minecraft-server&lt;/code&gt;!&lt;/p&gt;
&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;The GitHub link:&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;itzg/docker-minecraft-server&quot;}&lt;/p&gt;
&lt;p&gt;Here is a simple introduction from its &lt;a href=&quot;https://docker-minecraft-server.readthedocs.io/en/latest/&quot;&gt;documentation&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This docker image provides a Minecraft Server that will automatically download the latest stable version at startup. You can also run/upgrade to any specific version or the latest snapshot. See the Versions section below for more information.&lt;/p&gt;
&lt;p&gt;To simply use the latest stable version, run&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;docker run -d -it -p 25565:25565 -e EULA=TRUE itzg/minecraft-server
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;where, in this case, the standard server port 25565 will be exposed on your host machine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Docker Compose&lt;/h2&gt;
&lt;p&gt;I do not recommend running the image directly with the command above. Docker Compose is a better method, just like for other usual projects.&lt;/p&gt;
&lt;p&gt;Here are the official instructions.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Create a new directory&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Put the contents of the file below in a file called &lt;code&gt;compose.yaml&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run &lt;code&gt;docker compose up -d&lt;/code&gt; in that directory&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Done! Point your client at your host&apos;s name/IP address and port 25565.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;# docker.yaml
services:
  mc:
    image: itzg/minecraft-server:latest
    pull_policy: daily
    tty: true
    stdin_open: true
    ports:
      - &quot;25565:25565&quot;
    environment:
      EULA: &quot;TRUE&quot;
    volumes:
      # attach the relative directory &apos;data&apos; to the container&apos;s /data path
      - ./data:/data
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;To apply changes made to the compose file, just run &lt;code&gt;docker compose up -d&lt;/code&gt; again.&lt;/p&gt;
&lt;p&gt;Follow the logs of the container using &lt;code&gt;docker compose logs -f&lt;/code&gt;, check on the status with &lt;code&gt;docker compose ps&lt;/code&gt;, and stop the container using &lt;code&gt;docker compose stop&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you are new to Docker, volumes may be a little confusing. Well, they are actually quite simple. One possible directory structure looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Your-MC-Server-Folder
├── data
│   ├── config
│   ├── eula.txt
│   ├── kubejs
│   ├── mods
│   ├── server.properties
│   └── world
└── docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;data&lt;/code&gt; folder inside &lt;code&gt;Your-MC-Server-Folder&lt;/code&gt; is where you put all the mods and world data. Of course, you can also run Docker Compose first and let the container generate the data by itself, or just move your existing data into &lt;code&gt;data&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Loader and mods&lt;/h2&gt;
&lt;p&gt;Starting a modded server is also possible by changing the variables in &lt;code&gt;compose.yaml&lt;/code&gt;. Here is my own configuration. You can use &lt;a href=&quot;https://setupmc.com/java-server/&quot;&gt;SetupMC&lt;/a&gt; to help generate yours.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  mc:
    image: itzg/minecraft-server:latest
    tty: true
    stdin_open: true
    ports:
      - &quot;25565:25565&quot;
      - &quot;24454:24454/udp&quot;
    environment:
      EULA: &quot;TRUE&quot;
      TYPE: &quot;NEOFORGE&quot;
      VERSION: &quot;1.21.1&quot;
      INIT_MEMORY: &quot;4G&quot;
      MAX_MEMORY: &quot;12G&quot;
      MOTD: &quot;A Minecraft Server&quot;
      TZ: &quot;Asia/Tokyo&quot;
      DIFFICULTY: &quot;hard&quot;
    volumes:
      - &quot;./data:/data&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Btw, port &lt;code&gt;24454/udp&lt;/code&gt; is open because of the mod “Simple Voice Chat.” In this situation, you also need to change the IP to &lt;code&gt;0.0.0.0&lt;/code&gt; in the Simple Voice Chat config.&lt;/p&gt;
&lt;h2&gt;And…&lt;/h2&gt;
&lt;p&gt;Have fun!&lt;/p&gt;
</content:encoded></item></channel></rss>