<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>~iany/ X</title><link>https://blog.iany.me/tags/x/</link><description>Recent content in X «~iany/»</description><language>en-US</language><managingEditor>me@iany.me (Ian Yang)</managingEditor><webMaster>me@iany.me (Ian Yang)</webMaster><copyright>CC-BY-SA 4.0</copyright><lastBuildDate>Tue, 08 Sep 2026 09:00:00 +0800</lastBuildDate><atom:link href="https://blog.iany.me/tags/x/index.xml" rel="self" type="application/rss+xml"/><item><title>Forwarding Desktop Notifications to Pushover</title><link>https://blog.iany.me/2026/09/forwarding-desktop-notifications-to-pushover/</link><pubDate>Tue, 08 Sep 2026 09:00:00 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/09/forwarding-desktop-notifications-to-pushover/</guid><description>&lt;p&gt;I already forwarded &lt;strong&gt;mako&lt;/strong&gt; notifications to Pushover on Linux. The hook is tiny: mako runs a command with the notification id, the script reads &lt;code&gt;makoctl list -j&lt;/code&gt;, skips loops and Pushover itself, and POSTs to the Pushover API.&lt;/p&gt;
&lt;p&gt;Windows has no equivalent of &lt;code&gt;on-notify=exec&lt;/code&gt;. I wanted one binary, &lt;code&gt;dn-pushover&lt;/code&gt;, that does the same job on both desktops: titles become &lt;code&gt;[DN][hostname] …&lt;/code&gt; so a notification that bounces back is easy to drop.&lt;/p&gt;
&lt;p&gt;Linux stayed simple. Windows did not.&lt;/p&gt;
&lt;h2 id="how-it-works"&gt;How it works&lt;/h2&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Notification Forwarding Setup Paths"
loading="lazy"
src="https://blog.iany.me/2026/09/forwarding-desktop-notifications-to-pushover/Notification%20forwarding%20setup%20paths.svg" /&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Linux.&lt;/strong&gt; &lt;code&gt;dn-pushover setup&lt;/code&gt; writes a managed block into &lt;code&gt;~/.config/mako/config&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;on-notify=exec dn-pushover &amp;quot;$id&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then it reloads mako. If &lt;code&gt;makoctl&lt;/code&gt; is missing, setup fails instead of pretending to work. An old &lt;code&gt;mako-pushover&lt;/code&gt; block is migrated so deleting the old binary cannot leave a dead hook behind.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows.&lt;/strong&gt; There is no desktop-wide exec hook. The supported API is &lt;code&gt;UserNotificationListener&lt;/code&gt;: after the user grants access, an app can read other apps&amp;rsquo; toast notifications. &lt;code&gt;setup&lt;/code&gt; therefore:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Checks Pushover credentials.&lt;/li&gt;
&lt;li&gt;Builds a small C# WinForms helper (first time only; later setups reuse the exe).&lt;/li&gt;
&lt;li&gt;Packs a &lt;strong&gt;sparse identity MSIX&lt;/strong&gt; (manifest + logos; the exe lives next to it via &lt;code&gt;-ExternalLocation&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Signs it and registers it.&lt;/li&gt;
&lt;li&gt;Launches the helper. You click &lt;strong&gt;Allow Access&lt;/strong&gt; and accept the Windows prompt.&lt;/li&gt;
&lt;li&gt;The helper baselines whatever is already in the notification center, then polls for new toasts and runs &lt;code&gt;dn-pushover --forward&lt;/code&gt; with JSON on stdin.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;teardown&lt;/code&gt; stops the helper. &lt;code&gt;teardown --cleanup&lt;/code&gt; also unregisters the package and deletes &lt;code&gt;%LOCALAPPDATA%\dn-pushover&lt;/code&gt;. There is no autostart: after logout, run &lt;code&gt;setup&lt;/code&gt; again.&lt;/p&gt;
&lt;p&gt;The JS side is shared: prefix, truncation, skip &lt;code&gt;[DN]&lt;/code&gt; / &lt;code&gt;[mako]&lt;/code&gt; / recognizable Pushover traffic, then &lt;code&gt;send()&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="gotchas"&gt;Gotchas&lt;/h2&gt;
&lt;h3 id="linux-is-a-hook-windows-is-an-identity"&gt;Linux is a hook. Windows is an identity.&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;UserNotificationListener&lt;/code&gt; is not &amp;ldquo;read the notification database.&amp;rdquo; It is a capability on a &lt;strong&gt;packaged&lt;/strong&gt; app. An unpackaged Win32 process can call &lt;code&gt;RequestAccessAsync&lt;/code&gt; and even get &lt;code&gt;Allowed&lt;/code&gt; while &lt;code&gt;GetNotificationsAsync&lt;/code&gt; returns an empty list and Settings never shows the app.&lt;/p&gt;
&lt;p&gt;A loose &lt;code&gt;Add-AppxPackage -Register&lt;/code&gt; development package (&lt;code&gt;SignatureKind=None&lt;/code&gt;) has the same problem. Windows will not list it under &lt;strong&gt;Privacy &amp;amp; security → Notifications&lt;/strong&gt; (the &lt;em&gt;read&lt;/em&gt; list, not &lt;strong&gt;System → Notifications&lt;/strong&gt;, which is the &lt;em&gt;send&lt;/em&gt; list).&lt;/p&gt;
&lt;p&gt;What worked was a &lt;strong&gt;signed sparse MSIX&lt;/strong&gt; whose publisher matches the exe&amp;rsquo;s &lt;code&gt;app.manifest&lt;/code&gt; &lt;code&gt;msix&lt;/code&gt; element, with &lt;code&gt;uap3:Capability Name=&amp;quot;userNotificationListener&amp;quot;&lt;/code&gt; and &lt;code&gt;AppListEntry&lt;/code&gt; &lt;strong&gt;not&lt;/strong&gt; set to &lt;code&gt;none&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="signing-is-not-optional-and-per-user-trust-is-not-enough"&gt;Signing is not optional, and per-user trust is not enough&lt;/h3&gt;
&lt;p&gt;Self-signed is fine for a personal machine. The private key stays in &lt;code&gt;CurrentUser\My&lt;/code&gt;. Importing the &lt;em&gt;public&lt;/em&gt; cert into &lt;code&gt;CurrentUser\TrustedPeople&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; make you a public CA; it only means this user trusts this publisher.&lt;/p&gt;
&lt;p&gt;MSIX deployment still failed with &lt;code&gt;0x800B0109&lt;/code&gt; (untrusted root) until the public cert was also in &lt;strong&gt;&lt;code&gt;LocalMachine\TrustedPeople&lt;/code&gt;&lt;/strong&gt;. That needs a one-time UAC. Later setups skip the import if the thumbprint is already there.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;powershell.exe -NoProfile&lt;/code&gt; has no &lt;code&gt;Cert:&lt;/code&gt; drive. &lt;code&gt;Import-Module PKI&lt;/code&gt; under &lt;code&gt;$ErrorActionPreference = 'Stop'&lt;/code&gt; blew up on duplicate type data. Certificate create/import had to use &lt;code&gt;X509Store&lt;/code&gt; / &lt;code&gt;CertificateRequest&lt;/code&gt; instead of &lt;code&gt;New-SelfSignedCertificate&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="the-sdk-is-not-the-runtime"&gt;The SDK is not the runtime&lt;/h3&gt;
&lt;p&gt;A .NET 10 &lt;strong&gt;runtime&lt;/strong&gt; cannot &lt;code&gt;dotnet publish&lt;/code&gt;. First Windows setup needs a &lt;strong&gt;.NET 10 SDK&lt;/strong&gt; (Scoop: &lt;code&gt;versions/dotnet-sdk-lts&lt;/code&gt;) plus Windows SDK tools &lt;code&gt;makeappx&lt;/code&gt; and &lt;code&gt;signtool&lt;/code&gt;. Setup runs PowerShell with &lt;code&gt;-NoProfile&lt;/code&gt;, so Scoop&amp;rsquo;s shims are not on &lt;code&gt;PATH&lt;/code&gt;; it has to look in &lt;code&gt;~/scoop/apps/dotnet-sdk-lts/current/&lt;/code&gt; itself.&lt;/p&gt;
&lt;p&gt;Setup also &lt;strong&gt;skips rebuilding&lt;/strong&gt; if the helper exe already exists, and used to &lt;strong&gt;skip re-registering&lt;/strong&gt; if the package was already installed. That is how a stale unsigned package and an old helper survived &amp;ldquo;I already ran setup.&amp;rdquo; &lt;code&gt;teardown --cleanup&lt;/code&gt; is the rebuild switch. Signed packages under &lt;code&gt;WindowsApps&lt;/code&gt; must actually be removed; if cleanup no-ops, &lt;code&gt;Get-AppxPackage&lt;/code&gt; will still show them.&lt;/p&gt;
&lt;h3 id="the-compiled-bun-binary-is-a-different-program"&gt;The compiled Bun binary is a different program&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;import.meta.main&lt;/code&gt; is false inside a Bun-compiled &lt;code&gt;.exe&lt;/code&gt;. Guarding &lt;code&gt;main()&lt;/code&gt; with only that flag made &lt;code&gt;dn-pushover setup&lt;/code&gt; exit 0 with &lt;strong&gt;no output&lt;/strong&gt;. Detect the virtual &lt;code&gt;B:/~BUN/&lt;/code&gt; / &lt;code&gt;$bunfs&lt;/code&gt; entrypoint as well.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Bun.stdin.text()&lt;/code&gt; in that same compiled Windows exe did not read the helper&amp;rsquo;s piped JSON. &lt;code&gt;--forward&lt;/code&gt; exited 0, sent nothing. &lt;code&gt;readStdin()&lt;/code&gt; from Node&amp;rsquo;s &lt;code&gt;process.stdin&lt;/code&gt; did. A WinForms &lt;strong&gt;AcceptButton&lt;/strong&gt; also treated the Enter key from the terminal as &lt;strong&gt;Allow Access&lt;/strong&gt;, so setup &amp;ldquo;returned immediately&amp;rdquo; after the first successful grant.&lt;/p&gt;
&lt;p&gt;Substring &lt;code&gt;/pushover/i&lt;/code&gt; on the app id matched &lt;code&gt;DnPushover.NotificationListener&lt;/code&gt; and dropped every toast. Skip the Pushover &lt;em&gt;product&lt;/em&gt;, not any name that contains those letters.&lt;/p&gt;
&lt;h2 id="debug-process"&gt;Debug process&lt;/h2&gt;
&lt;p&gt;The failure mode was always the same from the outside: a Windows toast appeared, Pushover stayed quiet. The pipeline has three stages; each can succeed while the next never runs.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Notification Forwarding Failure Stages"
loading="lazy"
src="https://blog.iany.me/2026/09/forwarding-desktop-notifications-to-pushover/Notification%20forwarding%20failure%20stages.svg" /&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Is the listener alive?&lt;/strong&gt; Process + &lt;code&gt;%LOCALAPPDATA%\dn-pushover\listener.log&lt;/code&gt;. Empty log with a running process meant polls were not seeing new toasts (or the form was hidden and the timer never ticked). &lt;code&gt;Listener ready; N existing toast(s)&lt;/code&gt; means those N will never be forwarded — send a &lt;strong&gt;new&lt;/strong&gt; toast after ready.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does JS send?&lt;/strong&gt; Pipe JSON into &lt;code&gt;dn-pushover --forward&lt;/code&gt; and watch the Pushover API. A 200 body &lt;code&gt;{&amp;quot;status&amp;quot;:1,&amp;quot;request&amp;quot;:&amp;quot;…&amp;quot;}&lt;/code&gt; means credentials and the API are fine. If that works and the listener still does not, the helper is not invoking the CLI you think it is (&lt;code&gt;config.json&lt;/code&gt; points at &lt;code&gt;dist\dn-pushover.exe&lt;/code&gt;), or the CLI is skipping the payload.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What did &lt;code&gt;--forward&lt;/code&gt; actually do?&lt;/strong&gt; Temporary logging of argv, stdin length, skip vs send, and the API status. That is how &lt;code&gt;Bun.stdin.text()&lt;/code&gt; was caught: argv logged, stdin never read, process exit 0, no HTTP. After the stdin fix, the same toast showed &lt;code&gt;apiStatus: 200&lt;/code&gt;. Edge then showed the Pushover copy with a &lt;code&gt;[DN]&lt;/code&gt; title; that one is supposed to be skipped so you do not loop.&lt;/p&gt;
&lt;p&gt;Direct &lt;code&gt;fetch&lt;/code&gt; to &lt;code&gt;https://api.pushover.net/1/messages.json&lt;/code&gt; (user key + desktop-notification token, no secrets in logs) is the ground truth. If that returns &lt;code&gt;status: 1&lt;/code&gt; and the device still shows nothing, you are looking at the wrong Pushover app or device, not at this script.&lt;/p&gt;
&lt;h2 id="practical-checklist"&gt;Practical checklist&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Linux: &lt;code&gt;makoctl&lt;/code&gt; on mako&amp;rsquo;s PATH; &lt;code&gt;dn-pushover setup&lt;/code&gt;; section-specific &lt;code&gt;on-notify&lt;/code&gt; rules win over the global hook.&lt;/li&gt;
&lt;li&gt;Windows: .NET 10 SDK + Windows SDK; one UAC to trust &lt;code&gt;CN=dn-pushover&lt;/code&gt; in &lt;code&gt;LocalMachine\TrustedPeople&lt;/code&gt;; &lt;code&gt;dn-pushover setup&lt;/code&gt;; click Allow Access; Privacy list is &lt;em&gt;access&lt;/em&gt;, not senders.&lt;/li&gt;
&lt;li&gt;After changing helper C# or the Bun CLI: &lt;code&gt;teardown --cleanup&lt;/code&gt; then &lt;code&gt;setup&lt;/code&gt;, and use the rebuilt &lt;code&gt;dist&lt;/code&gt; binary.&lt;/li&gt;
&lt;li&gt;Test with a toast created &lt;strong&gt;after&lt;/strong&gt; the listener is ready. Expect &lt;code&gt;[DN][hostname]&lt;/code&gt; on Pushover.&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/bun/">Bun</category><category domain="https://blog.iany.me/tags/desktop/">Desktop</category><category domain="https://blog.iany.me/tags/linux/">Linux</category><category domain="https://blog.iany.me/tags/script/">Script</category><category domain="https://blog.iany.me/tags/windows/">Windows</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>My AI-Assisted Development Workflow</title><link>https://blog.iany.me/2026/08/my-ai-assisted-development-workflow/</link><pubDate>Mon, 10 Aug 2026 12:45:09 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/08/my-ai-assisted-development-workflow/</guid><description>&lt;p&gt;AI agents write code fastest when the work is sliced small and a human stays on judgment. My workflow runs a four-step loop: plan the feature into units, hand the branch stack to the agent, review each PR myself, then let the agent address my comments.&lt;/p&gt;
&lt;p&gt;I wrote a separate guide on the branch-management mechanics: &lt;a href="https://blog.iany.me/2026/07/a-practical-guide-to-gh-stack-for-ai-assisted-development/"&gt;♯ A Practical Guide to gh-stack for AI-Assisted Development&lt;/a&gt;. That post covers worktrees, rebase habits, and JSON sync. This post is the loop around that tool.&lt;/p&gt;
&lt;h2 id="1-plan-and-split-into-units"&gt;1. Plan and split into units&lt;/h2&gt;
&lt;p&gt;Start with a written plan, not &amp;ldquo;build the feature.&amp;rdquo; The plan names the goal, records the product decisions already made, and breaks work into Implementation Units: U1, U2, U3, and so on.&lt;/p&gt;
&lt;p&gt;Each unit is small enough to fit one agent session and one human review. Specific, not vague. &amp;ldquo;Design the data model for board columns&amp;rdquo; is a unit. &amp;ldquo;Do the backend&amp;rdquo; is not. Dependencies between units are explicit – U3 depends on U2 because it queries the schema U2 defines.&lt;/p&gt;
&lt;p&gt;Before any code, I do a quick plan review across three axes: product (is this the right thing?), feasibility (can we build it this way?), and scope (are the units the right size?). Fixing a bad unit boundary in a markdown file takes a minute. Fixing it across five stacked PRs takes an afternoon.&lt;/p&gt;
&lt;p&gt;When the plan turns out to be wrong mid-implementation, I stop and revise it before writing more code. The plan is the source of truth, not the branches.&lt;/p&gt;
&lt;h2 id="2-let-ai-manage-branches-with-gh-stack"&gt;2. Let AI manage branches with gh-stack&lt;/h2&gt;
&lt;p&gt;Once the units are stable, I give the agent one prompt to build the stack:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Implement U1 and U2 from the plan. Use gh-stack to add a branch per unit. Include the Jira ticket number in branch names.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The agent creates branches, writes scoped commits, opens stacked PRs with each PR based on the unit below, and runs &lt;code&gt;gh stack rebase --upstack&lt;/code&gt; when a lower layer changes. I do not hand-manage the branch chain day to day.&lt;/p&gt;
&lt;p&gt;One worktree per stack, one branch per unit. The &lt;a href="https://blog.iany.me/2026/07/a-practical-guide-to-gh-stack-for-ai-assisted-development/"&gt;gh-stack guide&lt;/a&gt; has the full setup for worktrees, branch naming, and how to keep the JSON in sync.&lt;/p&gt;
&lt;h2 id="3-review-the-pr-and-leave-comments"&gt;3. Review the PR and leave comments&lt;/h2&gt;
&lt;p&gt;This step belongs to me.&lt;/p&gt;
&lt;p&gt;I read each stacked PR as a focused slice. Small units mean the diff is narrow enough to actually read. I leave inline comments or a review summary – whatever makes the problem clear. I do not fix code during the review pass.&lt;/p&gt;
&lt;p&gt;Good review comments say what is wrong, what I want instead, and which layer owns the change. Ambiguous comments waste the next agent session. The review is the quality gate; I treat it like one.&lt;/p&gt;
&lt;h2 id="4-let-ai-address-the-review-comments"&gt;4. Let AI address the review comments&lt;/h2&gt;
&lt;p&gt;I hand the PR back with a prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Address review comments on PR #42. Stay on the matching stack branch. Add new commits; don&amp;rsquo;t amend unless I ask.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The agent reads the threads, fixes issues on the correct layer, replies to comments, and resolves what it can. Anything that requires a product call comes back to me. After fixes on a lower layer, it rebases upstack.&lt;/p&gt;
&lt;p&gt;Then I re-review or approve and merge bottom-up. One loop per unit. Repeat.&lt;/p&gt;
&lt;h2 id="why-this-shape-works"&gt;Why this shape works&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plan and units&lt;/td&gt;
&lt;td&gt;Human + agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branches and PRs&lt;/td&gt;
&lt;td&gt;Agent + gh-stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review&lt;/td&gt;
&lt;td&gt;Human&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Address comments&lt;/td&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Agents do well with bounded units and explicit comment lists. They do worse at deciding where one unit ends and another begins, or at choosing between two product directions. The plan and the review are where those decisions happen – both belong to the human.&lt;/p&gt;
&lt;p&gt;The loop: plan → stack → review → fix.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/git/">Git</category><category domain="https://blog.iany.me/tags/productivity/">Productivity</category><category domain="https://blog.iany.me/tags/programming/">Programming</category><category domain="https://blog.iany.me/tags/tool/">Tool</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2026-08-08</title><link>https://blog.iany.me/journal/2026/08/what-i-touched-this-week-2026-08-08/</link><pubDate>Sat, 08 Aug 2026 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2026/08/what-i-touched-this-week-2026-08-08/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="designing-with-ai-make-a-jig"&gt;Designing With AI: Make a Jig&lt;/h2&gt;
&lt;p&gt;Jack Cheng&amp;rsquo;s &lt;a href="https://every.to/p/designing-with-ai-make-a-jig"&gt;Designing With AI? Make a Jig&lt;/a&gt; names a pattern already appearing in practice: custom-built AI design controls—jigs—that constrain the model so the right move is the easy one.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A jig isn&amp;rsquo;t a full app; it&amp;rsquo;s a fixture that holds the work in place while you cut. The same idea applies to AI: fewer free-form prompts, more knobs wired to real values.&lt;/li&gt;
&lt;li&gt;Nearby bookmarks this week: &lt;a href="https://joshpuckett.me/dialkit"&gt;DialKit&lt;/a&gt; (floating typed controls—sliders, toggles, springs—bound to UI state) and &lt;a href="https://toolcraft.sh/"&gt;Toolcraft&lt;/a&gt; (open-source starter kit for AI design apps).&lt;/li&gt;
&lt;li&gt;Also noted: &lt;a href="https://ht-ml.app/"&gt;ht-ml.app&lt;/a&gt;—POST a single HTML document, get a public URL—hosting shaped for agent workflows rather than humans clicking through a dashboard.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On verification, &lt;a href="https://corentings.dev/blog/tdd-age-of-ai/"&gt;TDD in the Age of AI&lt;/a&gt; is the counterweight to &amp;ldquo;just let the model cover it.&amp;rdquo; When the same model writes both tests and implementation, you get the &lt;em&gt;appearance&lt;/em&gt; of verification without an independent check. AI-generated tests often become &lt;strong&gt;implementation-aware&lt;/strong&gt;—they assert &lt;em&gt;how&lt;/em&gt; the code works rather than &lt;em&gt;what&lt;/em&gt; it should do—so keep a human (or a separate process) owning behavior, invariants, and failure cases.&lt;/p&gt;
&lt;h2 id="domain-driven-design-quickly"&gt;Domain-Driven Design Quickly&lt;/h2&gt;
&lt;p&gt;I worked through Avram &amp;amp; Marinescu&amp;rsquo;s &lt;em&gt;Domain-Driven Design Quickly&lt;/em&gt;—a short summary of Evans—and captured notes in &lt;a href="https://kb.iany.me/para/lets/s/Software&amp;#43;Engineering/DOYR&amp;#43;on&amp;#43;Domain-Driven&amp;#43;Design"&gt;DOYR on Domain-Driven Design&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ubiquitous language&lt;/strong&gt;: build one vocabulary shared by domain experts and developers. Share the model with small diagrams that spotlight one area, then describe that area in the same language—not a separate &amp;ldquo;tech dialect.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layered architecture&lt;/strong&gt;: keep the domain independent. UI, infrastructure, and application depend on the domain (same dependency direction as Clean Architecture), not the other way around.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Building blocks&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Entities&lt;/strong&gt; — mutable objects with identity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Value objects&lt;/strong&gt; — immutable; defined by attributes, not id&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Aggregates&lt;/strong&gt; — entity root plus its relations; the consistency boundary&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Services&lt;/strong&gt; — operations that span multiple objects/aggregates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Factories&lt;/strong&gt; create aggregates; &lt;strong&gt;Repositories&lt;/strong&gt; persist and restore them&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Related bookmarks: &lt;a href="https://github.com/axelhamil/aggregate-kit"&gt;aggregate-kit&lt;/a&gt; (TypeScript DDD primitives) and &lt;a href="https://khalilstemmler.com/articles/tags/ddd/"&gt;Khalil Stemmler&amp;rsquo;s DDD articles&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="learning-is-an-output-process"&gt;Learning Is an Output Process&lt;/h2&gt;
&lt;p&gt;Dan Koe&amp;rsquo;s thread &lt;a href="https://x.com/thedankoe/status/2081415714636996844"&gt;How to Remember Everything You Read&lt;/a&gt; is less about memory tricks and more about what&amp;rsquo;s worth learning in the first place.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you need a system to remember something, it probably wasn&amp;rsquo;t important; if it matters, you&amp;rsquo;ll remember it because you used it.&lt;/li&gt;
&lt;li&gt;Sounding smart comes from &lt;em&gt;understanding&lt;/em&gt;, not memorization. Most learning fails for lack of a negative feedback loop.&lt;/li&gt;
&lt;li&gt;Learning is an output process: output demands input. Start from a self-generated goal and take the first step toward it; self-education is learning what that goal requires—not hoarding general knowledge to &lt;em&gt;feel&lt;/em&gt; productive.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Jack&amp;#43;Cheng&amp;#43;-&amp;#43;Designing&amp;#43;With&amp;#43;AI&amp;#43;Make&amp;#43;a&amp;#43;Jig.&amp;#43;%28Highlights%29"&gt;Jack Cheng - Designing With AI Make a Jig. (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Corentin&amp;#43;Giaufer&amp;#43;Saubert&amp;#43;-&amp;#43;TDD&amp;#43;in&amp;#43;the&amp;#43;Age&amp;#43;of&amp;#43;AI&amp;#43;Who&amp;#43;Tests&amp;#43;the&amp;#43;Tests&amp;#43;%28Highlights%29"&gt;Corentin Giaufer Saubert - TDD in the Age of AI Who Tests the Tests (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Tweets/Dan&amp;#43;Koe&amp;#43;-&amp;#43;How&amp;#43;to&amp;#43;Remember&amp;#43;Everything&amp;#43;You&amp;#43;Read&amp;#43;%28Highlights%29"&gt;Dan Koe - How to Remember Everything You Read (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/s/Software&amp;#43;Engineering/DOYR&amp;#43;on&amp;#43;Domain-Driven&amp;#43;Design"&gt;DOYR on Domain-Driven Design&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2026-08-01</title><link>https://blog.iany.me/journal/2026/08/what-i-touched-this-week-2026-08-01/</link><pubDate>Sat, 01 Aug 2026 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2026/08/what-i-touched-this-week-2026-08-01/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="what-stays-valuable-when-copies-are-free"&gt;What Stays Valuable When Copies Are Free&lt;/h2&gt;
&lt;p&gt;Two readings this week circled the same cultural shift: abundant free copies (and now free AI-generated ones) make the scarce, uncopyable layer matter more—and video-first media weakens the habits that used to build that layer.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kevin Kelly&amp;rsquo;s &lt;a href="https://tim.blog/2026/07/17/better-than-free/"&gt;Better Than Free&lt;/a&gt; frames eight &lt;em&gt;generative&lt;/em&gt; values you cannot pirate: trust, immediacy, personalization, interpretation, authenticity, accessibility, embodiment, and patronage. Trust is the purest case—you can&amp;rsquo;t buy or duplicate it; it has to be earned over time. Immediacy and personalization are the practical ones: a free copy eventually appears, but a copy in your inbox at release (or tuned to your room, your stack, your workflow) is still worth paying for.&lt;/li&gt;
&lt;li&gt;Rose Horowitch&amp;rsquo;s &lt;a href="https://www.theatlantic.com/magazine/2026/08/reading-crisis-postliterate-age/687618/"&gt;The End of Reading Is Here&lt;/a&gt; is the demand-side mirror. Video packs more raw information than text, yet it does not push deeper thinking. Spoken argument is harder to edit, easier to contradict without notice, and tied to the speaker&amp;rsquo;s identity rather than to a claim you can hold still and check. Culture also moves more horizontally now—peer to peer—so platform personalities become de facto guardians of taste.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The joint lesson: when the copy is free (or the feed is video), differentiate on what cannot be copied, and keep reading as a tool for claims you can revise.&lt;/p&gt;
&lt;h2 id="how-discord-stores-trillions-of-messages"&gt;How Discord Stores Trillions of Messages&lt;/h2&gt;
&lt;p&gt;On the systems side I went back to &lt;a href="https://discord.com/blog/how-discord-stores-trillions-of-messages"&gt;How Discord Stores Trillions of Messages&lt;/a&gt;—the follow-up to their earlier Cassandra write-up.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They moved toward &lt;a href="https://www.scylladb.com/"&gt;ScyllaDB&lt;/a&gt;: Cassandra-compatible, C++, shard-per-core isolation, no JVM GC pauses, faster repairs. The pitch is workload isolation and predictable latency once message volume leaves the &amp;ldquo;billions&amp;rdquo; regime.&lt;/li&gt;
&lt;li&gt;Between the API and the clusters sit thin &lt;em&gt;data services&lt;/em&gt;: roughly one gRPC endpoint per query, no business logic. The feature that matters is &lt;strong&gt;request coalescing&lt;/strong&gt;—if many users ask for the same row at once, hit the database once and fan out the result.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Same theme as last week&amp;rsquo;s performance reading: hide duplicate work, keep the hot path boring, and pick storage that fails in ways you can reason about under load.&lt;/p&gt;
&lt;h2 id="stacked-prs-with-gh-stack-and-ai-agents"&gt;Stacked PRs with gh-stack and AI Agents&lt;/h2&gt;
&lt;p&gt;I published &lt;a href="https://blog.iany.me/2026/07/a-practical-guide-to-gh-stack-for-ai-assisted-development/"&gt;A Practical Guide to gh-stack for AI-Assisted Development&lt;/a&gt;—the workflow I actually use when a feature is too big for one PR.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One Git worktree per stack so &amp;ldquo;which stack am I in?&amp;rdquo; is just the working directory.&lt;/li&gt;
&lt;li&gt;Let an agent break the feature into focused layers (schema → service → API → UI) and open one branch per layer; stacked review only works if each slice is coherent.&lt;/li&gt;
&lt;li&gt;Day to day: &lt;code&gt;gh stack rebase --upstack&lt;/code&gt;. Avoid &lt;code&gt;git commit --amend&lt;/code&gt; (gh-stack loses the SHA). For manual reshuffles, rebase from the &lt;em&gt;top&lt;/em&gt; with &lt;code&gt;rebase.updateRefs = true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;After squash/amend, &lt;code&gt;.git/gh-stack&lt;/code&gt; goes stale—hand an agent the repo and ask it to rewrite each recorded HEAD to match reality.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Related bookmarks this week: &lt;a href="https://github.com/gvergnaud/ts-pattern"&gt;ts-pattern&lt;/a&gt; for exhaustive matching, and a short note on TypeScript &lt;strong&gt;branded types&lt;/strong&gt; (&lt;code&gt;string &amp;amp; { readonly __brand: unique symbol }&lt;/code&gt;) as zero-cost domain IDs.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Kevin&amp;#43;Kelly&amp;#43;-&amp;#43;Better&amp;#43;Than&amp;#43;Free&amp;#43;How&amp;#43;to&amp;#43;Differentiate&amp;#43;in&amp;#43;the&amp;#43;Age&amp;#43;of&amp;#43;AI&amp;#43;%28Highlights%29"&gt;Kevin Kelly - Better Than Free How to Differentiate in the Age of AI (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Rose&amp;#43;Horowitch&amp;#43;-&amp;#43;The&amp;#43;End&amp;#43;of&amp;#43;Reading&amp;#43;Is&amp;#43;Here&amp;#43;%28Highlights%29"&gt;Rose Horowitch - The End of Reading Is Here (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Bo&amp;#43;Ingram&amp;#43;-&amp;#43;How&amp;#43;Discord&amp;#43;Stores&amp;#43;Trillions&amp;#43;of&amp;#43;Messages&amp;#43;%28Highlights%29"&gt;Bo Ingram - How Discord Stores Trillions of Messages (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.iany.me/2026/07/a-practical-guide-to-gh-stack-for-ai-assisted-development/"&gt;♯ A Practical Guide to gh-stack for AI-Assisted Development&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>A Practical Guide to gh-stack for AI-Assisted Development</title><link>https://blog.iany.me/2026/07/a-practical-guide-to-gh-stack-for-ai-assisted-development/</link><pubDate>Thu, 30 Jul 2026 07:19:53 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/07/a-practical-guide-to-gh-stack-for-ai-assisted-development/</guid><description>&lt;p&gt;Stacked pull requests let you review a feature in logical slices rather than one enormous diff. The challenge is managing the branches, synchronizing PRs, and surviving rebases without breaking the chain. &lt;a href="https://github.github.com/gh-stack/"&gt;gh-stack&lt;/a&gt; handles the scaffolding. It tracks your branch stack in a local JSON file, generates PRs that base each one to its parent, and works on any GitHub repo regardless of whether your organization has enabled native stacked PR support.&lt;/p&gt;
&lt;p&gt;This is the workflow I use, including how an AI coding agent fits into it.&lt;/p&gt;
&lt;h2 id="start-with-a-worktree"&gt;Start with a Worktree&lt;/h2&gt;
&lt;p&gt;Before creating any branches, set up a dedicated Git worktree for the stack.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;git worktree add ../project-login-stack login-stack-base
cd ../project-login-stack
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&amp;rsquo;s not required but my personal preference. Stacks and worktrees map one-to-one. This matters for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can switch between stacks by switching directories, not branches.&lt;/li&gt;
&lt;li&gt;Your shell&amp;rsquo;s working directory tells you exactly which stack you&amp;rsquo;re in.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="using-an-agent-to-build-the-stack"&gt;Using an Agent to Build the Stack&lt;/h2&gt;
&lt;p&gt;Stacked PRs only work well if the individual branches are focused. Broad, unfocused branches defeat the point. This is where AI agents help.&lt;/p&gt;
&lt;p&gt;When starting a feature, describe it to the agent and ask it to divide the work into discrete units. A typical breakdown might look like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Database layer&lt;/strong&gt; – schema and migrations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service layer&lt;/strong&gt; – business logic and data access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API layer&lt;/strong&gt; – endpoints and serializers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI layer&lt;/strong&gt; – frontend components&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The agent creates a branch for each, makes commits scoped to that layer, and stacks them in order. You end up with four focused PRs instead of one that mixes migrations, API code, and React components in the same diff.&lt;/p&gt;
&lt;h2 id="rebasing-without-breaking-the-stack"&gt;Rebasing Without Breaking the Stack&lt;/h2&gt;
&lt;p&gt;For day-to-day stack updates, use &lt;code&gt;gh stack rebase --upstack&lt;/code&gt;. Switch to any branch in the stack using &lt;code&gt;gh stack up/down/switch&lt;/code&gt;, add new commits, and &lt;code&gt;gh stack rebase --upstack&lt;/code&gt; to reorganize the branch chain. Avoid &lt;code&gt;git commit --amend&lt;/code&gt; in this workflow – gh-stack cannot track amended commits.&lt;/p&gt;
&lt;p&gt;When you need to manually rebase the stack such as moving commits between branches in the stack, check out the topmost branch and rebase from there. With &lt;code&gt;rebase.updateRefs&lt;/code&gt; enabled, Git updates every intermediate branch ref as it replays commits down the chain. Starting from a middle branch leaves the branches above it with stale refs.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-gitconfig"&gt;# ~/.gitconfig or repo .gitconfig
[rebase]
updateRefs = true
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;gh stack top # top of the stack
git rebase -i develop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prefer &lt;code&gt;gh stack rebase --upstack&lt;/code&gt; for the common case. Reserve manual &lt;code&gt;git rebase&lt;/code&gt; from the top for when you need to move commits between branches in the stack.&lt;/p&gt;
&lt;h2 id="the-json-sync-problem"&gt;The JSON Sync Problem&lt;/h2&gt;
&lt;p&gt;gh-stack records branch metadata in &lt;code&gt;.git/gh-stack&lt;/code&gt;. When you squash or amend commits, or rebase from a middle layer, the SHA that gh-stack recorded no longer matches the branch’s actual HEAD. The JSON goes stale and breaks most features of &lt;code&gt;gh-stack&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The fix is to sync the JSON with the current state of your branches. Doing this manually means running &lt;code&gt;git log --oneline&lt;/code&gt; for each branch, reading the current SHAs, and editing the JSON to match. Not difficult, but tedious – especially across a deep stack.&lt;/p&gt;
&lt;p&gt;An AI assistant handles this well. Give it access to the repository and ask:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Read the current HEAD of each branch in my gh-stack and update &lt;code&gt;.git/gh-stack&lt;/code&gt; so each entry&amp;rsquo;s recorded SHA matches.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="putting-it-together"&gt;Putting It Together&lt;/h2&gt;
&lt;p&gt;Stacked PRs reduce review friction. gh-stack makes stacking practical without requiring GitHub-side configuration. The four habits that make it work:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;One worktree per stack. Isolation by directory, not branch.&lt;/li&gt;
&lt;li&gt;Agent-planned stacks. Focused units make PRs reviewable.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gh stack rebase --upstack&lt;/code&gt; for routine updates. Manual &lt;code&gt;git rebase&lt;/code&gt; from the top when moving commits between branches in the stack.&lt;/li&gt;
&lt;li&gt;AI-assisted JSON sync. Fast recovery after a squash or amend.&lt;/li&gt;
&lt;/ol&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/git/">Git</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/tool/">Tool</category><category domain="https://blog.iany.me/tags/programming/">Programming</category><category domain="https://blog.iany.me/tags/productivity/">Productivity</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>Build an MCP with OAuth on Cloudflare Workers</title><link>https://blog.iany.me/2026/07/build-an-mcp-with-oauth-on-cloudflare-workers/</link><pubDate>Fri, 24 Jul 2026 19:48:12 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/07/build-an-mcp-with-oauth-on-cloudflare-workers/</guid><description>&lt;p&gt;I wanted MCP clients to create Google Tasks for me. The Google Tasks API requires a Workspace-style setup I don&amp;rsquo;t have. IFTTT already bridges webhooks to Google Tasks—but a Maker webhook URL is itself the credential. Storing that key in local MCP config is a bad idea.&lt;/p&gt;
&lt;p&gt;The fix is a thin Cloudflare Worker plus &lt;strong&gt;Cloudflare Access Managed OAuth&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Worker holds the IFTTT key as a secret and speaks MCP over HTTP&lt;/li&gt;
&lt;li&gt;Access sits in front, handles OAuth login with your IdP&lt;/li&gt;
&lt;li&gt;Worker verifies the Access JWT (&lt;code&gt;Cf-Access-Jwt-Assertion&lt;/code&gt;) before running tools&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The full example: &lt;a href="https://github.com/doitian/google-task-ifttt-webhook-mcp"&gt;doitian/google-task-ifttt-webhook-mcp&lt;/a&gt;. Most of it was scaffolded by AI; the interesting part is the structure, not the TypeScript.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The problem&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Tasks API&lt;/td&gt;
&lt;td&gt;Needs a Workspace-style setup I don&amp;rsquo;t want to pay for&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IFTTT Maker webhook&lt;/td&gt;
&lt;td&gt;Works, but the URL &lt;em&gt;is&lt;/em&gt; the credential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP clients&lt;/td&gt;
&lt;td&gt;Expect remote HTTP tools with OAuth&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;MCP Client
→ Cloudflare Access (OAuth)
→ Worker (JWT)
→ IFTTT
→ Google Tasks
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clients never see &lt;code&gt;IFTTT_MAKER_TASK_KEY&lt;/code&gt;. They only hold a short-lived Access token after login.&lt;/p&gt;
&lt;h2 id="ifttt-side"&gt;IFTTT side&lt;/h2&gt;
&lt;p&gt;One applet:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;If&lt;/strong&gt;: Maker Webhooks → Receive a web request, event name &lt;code&gt;task&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Then&lt;/strong&gt;: Google Tasks → Create a task (title / notes / due from the payload)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Worker posts to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://maker.ifttt.com/trigger/task/json/with/key/{KEY}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;with &lt;code&gt;{ &amp;quot;title&amp;quot;, &amp;quot;notes?&amp;quot;, &amp;quot;due?&amp;quot; }&lt;/code&gt;. The key never leaves Cloudflare.&lt;/p&gt;
&lt;h2 id="three-secrets"&gt;Three secrets&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-pwsh"&gt;npx wrangler secret put IFTTT_MAKER_TASK_KEY
npx wrangler secret put CF_ACCESS_TEAM_DOMAIN
npx wrangler secret put CF_ACCESS_AUD
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IFTTT_MAKER_TASK_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maker webhook key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CF_ACCESS_TEAM_DOMAIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zero Trust team domain (e.g. &lt;code&gt;myteam.cloudflareaccess.com&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CF_ACCESS_AUD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Access application audience tag (validates JWT &lt;code&gt;aud&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;No custom authorize page, no signing keys you mint yourself. Access is the authorization server.&lt;/p&gt;
&lt;h2 id="cloudflare-access"&gt;Cloudflare Access&lt;/h2&gt;
&lt;p&gt;In the &lt;a href="https://one.dash.cloudflare.com/"&gt;Zero Trust dashboard&lt;/a&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Access&lt;/strong&gt; → &lt;strong&gt;Applications&lt;/strong&gt; → &lt;strong&gt;Add&lt;/strong&gt; → &lt;strong&gt;Self-hosted&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Point the app at your Worker hostname (&lt;code&gt;*.workers.dev&lt;/code&gt; or a custom domain)&lt;/li&gt;
&lt;li&gt;Pick an IdP (Google, GitHub, email OTP, …) and an allow policy for yourself&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Managed OAuth (Beta)&lt;/strong&gt;—this is what MCP clients need for the OAuth dance&lt;/li&gt;
&lt;li&gt;Copy the team domain and Application Audience (AUD)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Access issues JWTs; MCP clients send them as &lt;code&gt;Cf-Access-Jwt-Assertion&lt;/code&gt;. The Worker verifies with the team&amp;rsquo;s JWKS:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ts"&gt;const JWKS = createRemoteJWKSet(
new URL(`${issuer}/cdn-cgi/access/certs`),
);
await jwtVerify(token, JWKS, {
issuer,
...(env.CF_ACCESS_AUD ? { audience: env.CF_ACCESS_AUD } : {}),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;jose&lt;/code&gt; on Workers with &lt;code&gt;nodejs_compat&lt;/code&gt;.)&lt;/p&gt;
&lt;h2 id="worker-just-mcp--verify"&gt;Worker: just MCP + verify&lt;/h2&gt;
&lt;p&gt;The business logic is small:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POST /&lt;/code&gt; or &lt;code&gt;POST /mcp&lt;/code&gt;—JSON-RPC (&lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;tools/call&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;One tool: &lt;code&gt;create_google_task&lt;/code&gt; (&lt;code&gt;title&lt;/code&gt; required; &lt;code&gt;notes&lt;/code&gt;, &lt;code&gt;due&lt;/code&gt; optional)&lt;/li&gt;
&lt;li&gt;Reject anything without a valid Access JWT&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No OAuth endpoints in the Worker. Access owns login; the Worker only checks the assertion header.&lt;/p&gt;
&lt;h2 id="client-config"&gt;Client config&lt;/h2&gt;
&lt;p&gt;Point the client at the Worker URL and enable OAuth. Example structure (keys vary by client):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
&amp;quot;mcp&amp;quot;: {
&amp;quot;google-tasks&amp;quot;: {
&amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
&amp;quot;url&amp;quot;: &amp;quot;https://google-task-mcp.yourdomain.com&amp;quot;
}
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The client discovers OAuth via Access, opens the browser for your IdP, then attaches the Access JWT on MCP calls.&lt;/p&gt;
&lt;h2 id="why-this-structure-works-with-ai"&gt;Why this structure works with AI&lt;/h2&gt;
&lt;p&gt;The task decomposes cleanly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;MCP handler&lt;/strong&gt;—fixed JSON-RPC + one tool schema&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IFTTT &lt;code&gt;fetch&lt;/code&gt;&lt;/strong&gt;—secret from &lt;code&gt;env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access JWT verify&lt;/strong&gt;—JWKS + &lt;code&gt;aud&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wrangler secrets&lt;/strong&gt;—nothing in git&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Describe the constraints (&amp;ldquo;don&amp;rsquo;t expose the IFTTT URL&amp;rdquo;, &amp;ldquo;Cloudflare Access OAuth&amp;rdquo;, &amp;ldquo;Worker MCP&amp;rdquo;) and get a deployable skeleton. Review JWT verification and secret handling; treat the tool body as glue.&lt;/p&gt;
&lt;h2 id="takeaways"&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Skip the official Tasks API if IFTTT already has the integration.&lt;/li&gt;
&lt;li&gt;Keep Maker keys in Worker secrets, not client config.&lt;/li&gt;
&lt;li&gt;Cloudflare Access Managed OAuth turns IdP login into MCP-friendly OAuth without requiring custom authorize/token endpoints.&lt;/li&gt;
&lt;li&gt;The Worker only needs to verify &lt;code&gt;Cf-Access-Jwt-Assertion&lt;/code&gt; and call IFTTT.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Source: &lt;a href="https://github.com/doitian/google-task-ifttt-webhook-mcp"&gt;github.com/doitian/google-task-ifttt-webhook-mcp&lt;/a&gt;.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/api/">API</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/javascript/">JavaScript</category><category domain="https://blog.iany.me/tags/security/">Security</category><category domain="https://blog.iany.me/tags/tool/">Tool</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2026-07-18</title><link>https://blog.iany.me/journal/2026/07/what-i-touched-this-week-2026-07-18/</link><pubDate>Sat, 18 Jul 2026 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2026/07/what-i-touched-this-week-2026-07-18/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="a-week-of-performance-and-scaling-reading"&gt;A Week of Performance and Scaling Reading&lt;/h2&gt;
&lt;p&gt;I kept a pace of three performance articles a day, and a clear map of the field emerged: nearly every technique is a variation on &lt;em&gt;reserve headroom, spread load evenly, and hide latency you can&amp;rsquo;t remove&lt;/em&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rate limiting&lt;/strong&gt; starts with the request rate limiter as the most important defense against abuse, built on the &lt;a href="https://stripe.com/blog/rate-limiters"&gt;token bucket algorithm&lt;/a&gt;: take a token per request, drip tokens back in, reject when empty. Stripe also runs a &lt;em&gt;concurrency&lt;/em&gt; limiter (&amp;ldquo;only 20 requests in progress at once&amp;rdquo; rather than &amp;ldquo;1000 per second&amp;rdquo;), always reserves capacity for critical requests, and sheds low-priority traffic (test mode first) when workers get busy. The recurring lesson: build a kill switch so you can turn any limiter off.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Load balancing&lt;/strong&gt; has a subtle trap. &lt;a href="https://netflixtechblog.com/netflix-edge-load-balancing-695308b5548c"&gt;Netflix&amp;rsquo;s edge rework&lt;/a&gt; notes that join-the-shortest-queue works for a single balancer but causes &lt;em&gt;herding&lt;/em&gt; across a cluster—every balancer piles onto the same idle server. The fix is choice-of-two plus combining data sources: the client is the best judge of latency, the server the best judge of its own utilization, and stats should decay to zero over time so stale readings don&amp;rsquo;t linger.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Caching and perceived speed&lt;/strong&gt; cover the other half. &lt;a href="https://engineering.fb.com/2022/06/08/core-infra/cache-made-consistent/"&gt;Facebook&amp;rsquo;s Cache Made Consistent&lt;/a&gt; shows why a version field alone can&amp;rsquo;t fix cache races—the version that resolves a conflict can itself be evicted before the stale write arrives. On the client side, &lt;a href="https://medium.com/@jaivalsuthar/prefetching-at-scale-why-instagram-works-without-internet-a-deep-dive-into-predictive-caching-b58fc8332c4e"&gt;Instagram&amp;rsquo;s predictive prefetching&lt;/a&gt; and &lt;a href="https://simonhearne.com/2021/optimistic-ui-patterns/"&gt;optimistic UI patterns&lt;/a&gt; share one principle: when latency is unacceptable but access patterns are predictable, switch from pull to prediction—prefetch on hover, slide in the cart before the API responds, and accept some wasted work in exchange for eliminating latency on the hits.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This reading also fed straight into work: the ApiHistory backfill script I wrote this week used &lt;a href="https://github.com/vercel/async-sema"&gt;async-sema&lt;/a&gt; as its concurrency limiter, and I bookmarked &lt;a href="https://github.com/graphql/dataloader"&gt;DataLoader&lt;/a&gt; after reading &lt;a href="https://shopify.engineering/solving-the-n-1-problem-for-graphql-through-batching"&gt;Shopify on solving GraphQL&amp;rsquo;s N+1 problem through batching&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="learning-by-writing"&gt;Learning by Writing&lt;/h2&gt;
&lt;p&gt;A second thread ran through the week: how to actually think, rather than just accumulate information. Holden Karnofsky and Henrik Karlsson converge on the same counterintuitive advice—commit to a claim &lt;em&gt;early&lt;/em&gt;, on purpose, so you have something to break.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In &lt;a href="https://www.cold-takes.com/learning-by-writing/"&gt;Learning by Writing&lt;/a&gt;, Karnofsky forms a hypothesis &amp;ldquo;long before I&amp;rsquo;m qualified to have an opinion,&amp;rdquo; then reads to change his mind rather than to gather facts. He rarely reads a document start to finish; he skips energetically to the parts most likely to overturn his current view. Writing early forces you to pin down the &lt;em&gt;version of the claim you care most about&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;His companion piece, &lt;a href="https://www.cold-takes.com/minimal-trust-investigations/"&gt;Minimal-Trust Investigations&lt;/a&gt;, is the discipline of occasionally suspending trust in others&amp;rsquo; judgment and digging into a question until you can keep pace with experts on that narrow slice.&lt;/li&gt;
&lt;li&gt;Karlsson&amp;rsquo;s &lt;a href="https://substack.com/home/post/p-143987982"&gt;How to Think in Writing&lt;/a&gt; supplies the mechanism: push yourself to make &lt;em&gt;definite positive claims&lt;/em&gt;, because ambiguity lets ideas stay fluid and dodge scrutiny. Unfolding a claim into an explanation spreads it on a &amp;ldquo;wider front&amp;rdquo; so criticism has more targets. Seeing your ideas crumble is the point—&amp;ldquo;it is in the cracks the light shines in.&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Paul&amp;#43;Tarjan&amp;#43;-&amp;#43;Scaling&amp;#43;Your&amp;#43;API&amp;#43;With&amp;#43;Rate&amp;#43;Limiters&amp;#43;%28Highlights%29"&gt;Paul Tarjan - Scaling Your API With Rate Limiters (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Mike&amp;#43;Smith&amp;#43;-&amp;#43;Rethinking&amp;#43;Netflix%E2%80%99s&amp;#43;Edge&amp;#43;Load&amp;#43;Balancing&amp;#43;%28Highlights%29"&gt;Mike Smith - Rethinking Netflix’s Edge Load Balancing (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Lu&amp;#43;Pan&amp;#43;-&amp;#43;Cache&amp;#43;Made&amp;#43;Consistent&amp;#43;%28Highlights%29"&gt;Lu Pan - Cache Made Consistent (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Jaival&amp;#43;Suthar&amp;#43;-&amp;#43;Prefetching&amp;#43;at&amp;#43;Scale&amp;#43;Why&amp;#43;Instagram&amp;#43;Works&amp;#43;Without&amp;#43;Internet&amp;#43;%E2%80%94&amp;#43;A&amp;#43;Deep&amp;#43;Dive&amp;#43;Into&amp;#43;Predictive&amp;#43;Caching&amp;#43;%28Highlights%29"&gt;Jaival Suthar - Prefetching at Scale Why Instagram Works Without Internet — A Deep Dive Into Predictive Caching (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Simon&amp;#43;Hearne&amp;#43;-&amp;#43;Optimistic&amp;#43;UI&amp;#43;Patterns&amp;#43;for&amp;#43;Improved&amp;#43;Perceived&amp;#43;Performance&amp;#43;%28Highlights%29"&gt;Simon Hearne - Optimistic UI Patterns for Improved Perceived Performance (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Leanne&amp;#43;Shapton&amp;#43;-&amp;#43;Solving&amp;#43;the&amp;#43;N%2B1&amp;#43;Problem&amp;#43;for&amp;#43;GraphQL&amp;#43;Through&amp;#43;Batching&amp;#43;%28Highlights%29"&gt;Leanne Shapton - Solving the N+1 Problem for GraphQL Through Batching (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Holden&amp;#43;Karnofsky&amp;#43;-&amp;#43;Learning&amp;#43;by&amp;#43;Writing&amp;#43;%28Highlights%29"&gt;Holden Karnofsky - Learning by Writing (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Holden&amp;#43;Karnofsky&amp;#43;-&amp;#43;Minimal-Trust&amp;#43;Investigations&amp;#43;%28Highlights%29"&gt;Holden Karnofsky - Minimal-Trust Investigations (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Henrik&amp;#43;Karlsson&amp;#43;-&amp;#43;How&amp;#43;to&amp;#43;Think&amp;#43;in&amp;#43;Writing&amp;#43;%28Highlights%29"&gt;Henrik Karlsson - How to Think in Writing (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2026-07-11</title><link>https://blog.iany.me/journal/2026/07/what-i-touched-this-week-2026-07-11/</link><pubDate>Sat, 11 Jul 2026 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2026/07/what-i-touched-this-week-2026-07-11/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="two-ways-to-kill-serverless-cold-starts"&gt;Two Ways to Kill Serverless Cold Starts&lt;/h2&gt;
&lt;p&gt;Two platform teams attacked the same problem—the latency spike when a serverless function boots from zero—from opposite ends of the architecture.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vercel&amp;rsquo;s &lt;a href="https://vercel.com/blog/scale-to-one-how-fluid-solves-cold-starts"&gt;Scale to One: How Fluid Solves Cold Starts&lt;/a&gt; rejects scale-to-zero outright: Fluid always keeps at least one function instance running, so the platform scales to one instead of zero.&lt;/li&gt;
&lt;li&gt;Fluid also rethinks the one-instance-per-request model: each function instance serves many concurrent requests, so bursts of traffic no longer translate into bursts of cold boots.&lt;/li&gt;
&lt;li&gt;Cloudflare&amp;rsquo;s &lt;a href="https://blog.cloudflare.com/eliminating-cold-starts-2-shard-and-conquer/"&gt;Eliminating Cold Starts 2: Shard and Conquer&lt;/a&gt; builds on their earlier trick of pre-warming a Worker during the TLS handshake of its first request—the handshake round-trips buy enough time to load the isolate before the actual HTTP request arrives.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="speed-over-consistency-data-stores-at-netflix-and-discord"&gt;Speed Over Consistency: Data Stores at Netflix and Discord&lt;/h2&gt;
&lt;p&gt;Two classic production write-ups showed how large services deliberately trade strict consistency for speed and availability, then engineer around the sharp edges.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Netflix&amp;rsquo;s &lt;a href="https://netflixtechblog.com/ephemeral-volatile-caching-in-the-cloud-8eba7b124589"&gt;Ephemeral Volatile Caching in the Cloud&lt;/a&gt; describes EVCache as a best-effort cache: the architecture chooses speed over consistency, and every dependent application must tolerate stale data.&lt;/li&gt;
&lt;li&gt;The EVCache client is AWS zone-aware: writes and deletes replicate to the EVCache servers in every zone, while reads stay in the client&amp;rsquo;s own zone for latency. The Java client, built on &lt;a href="https://code.google.com/archive/p/spymemcached/"&gt;spymemcached&lt;/a&gt;, handles discovery and cluster membership changes automatically.&lt;/li&gt;
&lt;li&gt;Discord&amp;rsquo;s &lt;a href="https://discord.com/blog/how-discord-stores-billions-of-messages"&gt;How Discord Stores Billions of Messages&lt;/a&gt; picked Cassandra because nodes can be added linearly and lost without impact—and captures a culture principle worth stealing: build quickly to prove out a feature, but always with a path to a more robust solution.&lt;/li&gt;
&lt;li&gt;The best mental model for Cassandra is a KKV store: the partition key locates the node and disk position, the clustering key sorts rows within the partition—a partition is an ordered dictionary.&lt;/li&gt;
&lt;li&gt;Two production gotchas stood out: writing null to a column generates a tombstone just like a delete (their 16-column schema averaged 4 set values, so most writes created 12 pointless tombstones—fixed by writing only non-null values), and queries over empty time buckets had to be tracked and skipped to avoid full scans on quiet channels.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="grit-optimism-parenting-and-the-effortreward-connection"&gt;Grit: Optimism, Parenting, and the Effort–Reward Connection&lt;/h2&gt;
&lt;p&gt;Continued through the later chapters of Angela Duckworth&amp;rsquo;s &lt;a href="https://readwise.io/bookreview/61388063"&gt;Grit&lt;/a&gt;, which move from the psychology of hope to how grit is grown in others.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The lesson on setbacks: don&amp;rsquo;t overreact—step back, analyze, and learn from them, but stay optimistic. Explaining adversity as permanent and pervasive turns minor complications into catastrophes; the seminal learned-helplessness experiments showed it isn&amp;rsquo;t suffering that leads to hopelessness, but suffering you think you can&amp;rsquo;t control.&lt;/li&gt;
&lt;li&gt;What didn&amp;rsquo;t kill the young rats—when they could control what was happening by their own efforts—made them stronger for life. Direct experience of the connection between effort and reward matters: without it, animals and people alike default to laziness.&lt;/li&gt;
&lt;li&gt;On parenting for grit: wise parents are child-centered without believing children are always the best judge of how hard to work or when to quit, and their authority rests on knowledge and wisdom rather than power.&lt;/li&gt;
&lt;li&gt;The uncomfortable mirror: if you want a gritty child, first ask how much passion and perseverance you have for your own goals, and whether your parenting invites the child to emulate you.&lt;/li&gt;
&lt;li&gt;The best coaching engineers situations slightly outside the comfort zone—far enough to stretch, close enough to reach.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Malte&amp;#43;Ubl&amp;#43;et&amp;#43;al.&amp;#43;-&amp;#43;Scale&amp;#43;to&amp;#43;One&amp;#43;How&amp;#43;Fluid&amp;#43;Solves&amp;#43;Cold&amp;#43;Starts&amp;#43;%28Highlights%29"&gt;Malte Ubl et al. - Scale to One How Fluid Solves Cold Starts (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Harris&amp;#43;Hancock&amp;#43;-&amp;#43;Eliminating&amp;#43;Cold&amp;#43;Starts&amp;#43;2&amp;#43;Shard&amp;#43;and&amp;#43;Conquer&amp;#43;%28Highlights%29"&gt;Harris Hancock - Eliminating Cold Starts 2 Shard and Conquer (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Shashi&amp;#43;Madappa&amp;#43;-&amp;#43;Ephemeral&amp;#43;Volatile&amp;#43;Caching&amp;#43;in&amp;#43;the&amp;#43;Cloud&amp;#43;%28Highlights%29"&gt;Shashi Madappa - Ephemeral Volatile Caching in the Cloud (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Stanislav&amp;#43;Vishnevskiy&amp;#43;-&amp;#43;How&amp;#43;Discord&amp;#43;Stores&amp;#43;Billions&amp;#43;of&amp;#43;Messages&amp;#43;%28Highlights%29"&gt;Stanislav Vishnevskiy - How Discord Stores Billions of Messages (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Books/Angela&amp;#43;Duckworth&amp;#43;-&amp;#43;Grit&amp;#43;%28Highlights%29"&gt;Angela Duckworth - Grit (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2026-07-04</title><link>https://blog.iany.me/journal/2026/07/what-i-touched-this-week-2026-07-04/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2026/07/what-i-touched-this-week-2026-07-04/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="dimensional-modeling-meets-the-open-lakehouse"&gt;Dimensional Modeling Meets the Open Lakehouse&lt;/h2&gt;
&lt;p&gt;The bulk of this week&amp;rsquo;s reading was data engineering: Kimball&amp;rsquo;s &lt;a href="https://readwise.io/bookreview/61511627"&gt;The Data Warehouse Toolkit&lt;/a&gt; delivered over a hundred highlights on dimensional modeling, while &lt;a href="https://livebook.manning.com/book/architecting-an-apache-iceberg-lakehouse"&gt;Architecting an Apache Iceberg Lakehouse&lt;/a&gt; covered how the same presentation-layer discipline lands on open table formats.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kimball&amp;rsquo;s bedrock principle: one measurement event in the physical world maps to exactly one row in the fact table. The four design decisions follow in order—select the business process, declare the grain, identify the dimensions, identify the facts—and the most frequent design error is skipping the grain declaration.&lt;/li&gt;
&lt;li&gt;Dimension tables trade space for simplicity: denormalize aggressively, replace cryptic codes with verbose textual attributes, and substitute default rows (Unknown, Not Applicable) for null keys. Normalized 3NF models are great for operational writes but &amp;ldquo;resemble a map of the Los Angeles freeway system&amp;rdquo; to BI users.&lt;/li&gt;
&lt;li&gt;Percentages and ratios are non-additive—store the numerator and denominator in the fact table instead. Store derived facts physically so the ETL computes them once and consistently.&lt;/li&gt;
&lt;li&gt;Conformed dimensions are the enterprise glue: separate teams can build separate star schemas at different times, and they plug together as long as shared dimensions mean the same thing everywhere. That&amp;rsquo;s 95 percent of the data architecture effort.&lt;/li&gt;
&lt;li&gt;Iceberg modernizes the storage side: layered immutable metadata gives atomic commits, time travel, and optimistic concurrency, with the catalog as the arbiter of each table&amp;rsquo;s current state. The REST Catalog specification decouples clients from catalog implementations; Nessie adds git-like branching and merging so pipelines can write in isolation and merge atomically.&lt;/li&gt;
&lt;li&gt;Adjacent bookmarks rounded out the architecture view: Zhamak Dehghani&amp;rsquo;s &lt;a href="https://martinfowler.com/articles/data-monolith-to-mesh.html"&gt;data mesh&lt;/a&gt; argument for decomposing monolithic lakes, and the &lt;a href="https://bitol-io.github.io/open-data-contract-standard/latest/"&gt;Open Data Contract Standard&lt;/a&gt; for formalizing the producer–consumer interface.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="fractional-indexing-for-realtime-ordered-sequences"&gt;Fractional Indexing for Realtime Ordered Sequences&lt;/h2&gt;
&lt;p&gt;A pair of articles explained how collaborative tools keep ordered lists consistent under concurrent edits without the complexity of Operational Transformation.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Figma&amp;rsquo;s &lt;a href="https://www.figma.com/blog/realtime-editing-of-ordered-sequences/"&gt;Realtime Editing of Ordered Sequences&lt;/a&gt; (Evan Wallace) explains why OT was overkill for a design tool: OT&amp;rsquo;s benefits target enormous text sequences and interleaving avoidance, at a high implementation cost—the first OT paper had subtle bugs that went undiscovered for years.&lt;/li&gt;
&lt;li&gt;Figma&amp;rsquo;s alternative: every object gets a fraction between 0 and 1 (exclusive) as its index; ordering is just sorting. Inserting between two objects averages their indices, and the exclusive bounds guarantee you can always generate a key before or after any existing one.&lt;/li&gt;
&lt;li&gt;Indices are stored as strings and averaged with string manipulation, so precision never runs out. When two clients insert at the same position simultaneously, the server simply assigns a fresh unique position to the second insert.&lt;/li&gt;
&lt;li&gt;David Greenspan&amp;rsquo;s &lt;a href="https://observablehq.com/@dgreensp/implementing-fractional-indexing"&gt;Implementing Fractional Indexing&lt;/a&gt; works out the details in lexicographic string space: the midpoint algorithm must not needlessly grow key length, and edge cases like trailing zeros (&amp;ldquo;3&amp;rdquo; vs &amp;ldquo;30&amp;rdquo; have nothing between them) need careful handling.&lt;/li&gt;
&lt;li&gt;His refinement uses variable-length integer parts with a length prefix (A0–A9, B00–B99, …) so appending grows keys logarithmically instead of one digit per insert, and base 95 packs 65 bits of integer precision into ten characters.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="war-stories-of-scaling-relational-databases"&gt;War Stories of Scaling Relational Databases&lt;/h2&gt;
&lt;p&gt;Two production write-ups showed different paths to the same destination: keeping a relational database alive under massive growth.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenAI&amp;rsquo;s &lt;a href="https://openai.com/index/scaling-postgresql/"&gt;Scaling PostgreSQL to power 800 million ChatGPT users&lt;/a&gt; keeps a single-writer Postgres and offloads pressure instead of sharding it: shardable write-heavy workloads migrate to Cosmos DB, backfills are strictly rate-limited, and complex joins move to the application layer.&lt;/li&gt;
&lt;li&gt;The operational playbook: isolate low-priority and high-priority requests onto dedicated instances to kill the noisy-neighbor problem, pool connections with PgBouncer in transaction mode, use cache locking so a cache-miss storm sends only one reader per key to the database, rate-limit at every layer, and enforce a 5-second timeout on schema changes.&lt;/li&gt;
&lt;li&gt;Slack took the sharding path with &lt;a href="https://slack.engineering/scaling-datastores-at-slack-with-vitess/"&gt;Vitess&lt;/a&gt;: the original architecture had three MySQL clusters with all data-access and shard-lookup logic living in the webapp itself, which became the bottleneck to evolve—motivating a datastore layer that owns routing instead. The &lt;a href="https://vitess.io/"&gt;Vitess project&lt;/a&gt; itself landed in my bookmarks as the reusable version of that lesson.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Books/Ralph&amp;#43;Kimball&amp;#43;-&amp;#43;The&amp;#43;Data&amp;#43;Warehouse&amp;#43;Toolkit&amp;#43;The&amp;#43;Definitive&amp;#43;Guide&amp;#43;to&amp;#43;Dimensional&amp;#43;Modeling&amp;#43;%28Highlights%29"&gt;Ralph Kimball - The Data Warehouse Toolkit The Definitive Guide to Dimensional Modeling (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Books/Alex&amp;#43;Merced&amp;#43;-&amp;#43;Architecting&amp;#43;an&amp;#43;Apache&amp;#43;Iceberg&amp;#43;Lakehouse&amp;#43;%28Highlights%29"&gt;Alex Merced - Architecting an Apache Iceberg Lakehouse (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Evan&amp;#43;Wallace&amp;#43;-&amp;#43;Realtime&amp;#43;Editing&amp;#43;of&amp;#43;Ordered&amp;#43;Sequences&amp;#43;%28Highlights%29"&gt;Evan Wallace - Realtime Editing of Ordered Sequences (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/David&amp;#43;Greenspan&amp;#43;-&amp;#43;Implementing&amp;#43;Fractional&amp;#43;Indexing&amp;#43;%28Highlights%29"&gt;David Greenspan - Implementing Fractional Indexing (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Bohan&amp;#43;Zhang&amp;#43;-&amp;#43;Scaling&amp;#43;PostgreSQL&amp;#43;to&amp;#43;power&amp;#43;800&amp;#43;million&amp;#43;ChatGPT&amp;#43;users&amp;#43;%28Highlights%29"&gt;Bohan Zhang - Scaling PostgreSQL to power 800 million ChatGPT users (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Arka&amp;#43;Ganguli&amp;#43;et&amp;#43;al.&amp;#43;-&amp;#43;Scaling&amp;#43;Datastores&amp;#43;at&amp;#43;Slack&amp;#43;with&amp;#43;Vitess&amp;#43;%28Highlights%29"&gt;Arka Ganguli et al. - Scaling Datastores at Slack with Vitess (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>Fix Claude Desktop PATH with CLAUDE_ENV_FILE</title><link>https://blog.iany.me/2026/07/fix-claude-desktop-path-with-claude-env-file/</link><pubDate>Wed, 01 Jul 2026 21:27:36 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/07/fix-claude-desktop-path-with-claude-env-file/</guid><description>&lt;p&gt;Claude Code runs tools and shell commands on your behalf. Launched from a terminal, the CLI inherits your shell environment and everything just works. But under &lt;strong&gt;Claude Desktop&lt;/strong&gt;, Claude Code is spawned from a GUI/desktop session that never sourced your interactive shell config—so if you rely on version managers like mise, asdf, or nix, the tools they install may not be on Claude&amp;rsquo;s &lt;code&gt;PATH&lt;/code&gt;. The fix is a single setting: &lt;code&gt;CLAUDE_ENV_FILE&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="how-it-works"&gt;How it works&lt;/h2&gt;
&lt;p&gt;Set &lt;code&gt;CLAUDE_ENV_FILE&lt;/code&gt; in &lt;code&gt;.claude/settings.local.json&lt;/code&gt; to point at a shell file. Claude sources it before running any command.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
&amp;quot;env&amp;quot;: {
&amp;quot;CLAUDE_ENV_FILE&amp;quot;: &amp;quot;.claude/mise.env&amp;quot;
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That file is a plain shell snippet—&lt;code&gt;export&lt;/code&gt; statements, one per line:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;export PATH=&amp;quot;/home/ian/.local/share/mise/shims:/home/ian/project/node_modules/.bin:$PATH&amp;quot;
export DATABASE_URL=&amp;quot;postgres://localhost/db&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The path can be relative—it&amp;rsquo;s resolved from the project root—so &lt;code&gt;.claude/mise.env&lt;/code&gt; works on any machine and can be committed alongside the project. Write both, and the next Claude session picks up everything.&lt;/p&gt;
&lt;h2 id="why-not-just-put-path-in-settingsjson"&gt;Why not just put PATH in settings.json?&lt;/h2&gt;
&lt;p&gt;You can set &lt;code&gt;env.PATH&lt;/code&gt; directly in &lt;code&gt;settings.local.json&lt;/code&gt;, but there&amp;rsquo;s a catch: Claude does not support variable substitution like &lt;code&gt;${env:PATH}&lt;/code&gt;. You&amp;rsquo;d have to hardcode the full &lt;code&gt;PATH&lt;/code&gt;, which breaks the moment dependencies change or you move between machines.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;CLAUDE_ENV_FILE&lt;/code&gt; you get full shell semantics—&lt;code&gt;$PATH&lt;/code&gt; expands at runtime, conditionals work, and subshells are fair game.&lt;/p&gt;
&lt;h2 id="why-shell-init-doesnt-help-either-the-snapshot"&gt;Why shell init doesn&amp;rsquo;t help either: the snapshot&lt;/h2&gt;
&lt;p&gt;There&amp;rsquo;s a deeper reason the obvious fixes fail. At session start, Claude Code freezes a &lt;strong&gt;shell snapshot&lt;/strong&gt; to &lt;code&gt;~/.claude/shell-snapshots/snapshot-zsh-*.sh&lt;/code&gt;, and the Bash tool sources that file before every command it runs.&lt;/p&gt;
&lt;p&gt;The snapshot&amp;rsquo;s &lt;em&gt;final&lt;/em&gt; line is &lt;code&gt;export PATH=&amp;lt;frozen value&amp;gt;&lt;/code&gt;, and that value is Claude Code&amp;rsquo;s own process &lt;code&gt;PATH&lt;/code&gt; captured at startup—not re-derived from your shell config. (You can spot it: it contains directories no rc file adds, like Claude&amp;rsquo;s own plugin and session &lt;code&gt;bin&lt;/code&gt; paths.) Because this &lt;code&gt;export PATH&lt;/code&gt; is the last thing sourced before each command, it clobbers anything your shell init did:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mise activate&lt;/code&gt; in &lt;code&gt;.zshenv&lt;/code&gt;/&lt;code&gt;.zshrc&lt;/code&gt; runs, prepends the shims—then the snapshot&amp;rsquo;s trailing &lt;code&gt;export PATH&lt;/code&gt; overwrites it, burying the shims back where they started.&lt;/li&gt;
&lt;li&gt;Even setting &lt;code&gt;env.PATH&lt;/code&gt; in &lt;code&gt;settings.json&lt;/code&gt; as a plain subprocess variable loses, because the snapshot is sourced &lt;em&gt;after&lt;/em&gt; the process environment is set up.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;CLAUDE_ENV_FILE&lt;/code&gt; wins because it&amp;rsquo;s sourced &lt;strong&gt;after&lt;/strong&gt; the snapshot. Its &lt;code&gt;export PATH=&amp;quot;…/shims:$PATH&amp;quot;&lt;/code&gt; is the last word, so the shims land first for every Bash tool call. That&amp;rsquo;s the whole trick: it&amp;rsquo;s not just convenient shell semantics—it&amp;rsquo;s the one hook that runs late enough to survive the snapshot.&lt;/p&gt;
&lt;h2 id="generating-the-env-file-automatically"&gt;Generating the env file automatically&lt;/h2&gt;
&lt;p&gt;Writing the env file by hand gets tedious. If you use mise, you can generate both files with a task—I vibe-coded &lt;a href="https://github.com/doitian/dotfiles-public/blob/master/src/mise-tasks/preset-claude.js"&gt;&lt;code&gt;preset-claude.js&lt;/code&gt;&lt;/a&gt; for exactly this (it&amp;rsquo;s a custom mise task, not a built-in). It:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reads &lt;code&gt;env._.path&lt;/code&gt; from &lt;code&gt;mise.toml&lt;/code&gt; (so per-project &lt;code&gt;node_modules/.bin&lt;/code&gt; directories land on &lt;code&gt;PATH&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Expands &lt;code&gt;{{config_root}}&lt;/code&gt; to absolute paths.&lt;/li&gt;
&lt;li&gt;Writes &lt;code&gt;.claude/mise.env&lt;/code&gt; with shims, extra paths, and all mise env vars.&lt;/li&gt;
&lt;li&gt;Sets &lt;code&gt;CLAUDE_ENV_FILE&lt;/code&gt; in &lt;code&gt;.claude/settings.local.json&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;mise run preset:claude
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One command, and Claude sees the exact same tools you do.&lt;/p&gt;
&lt;h2 id="what-about-other-version-managers"&gt;What about other version managers?&lt;/h2&gt;
&lt;p&gt;The pattern works for anything that can write a shell env file:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;asdf&lt;/strong&gt;: &lt;code&gt;asdf direnv envrc &amp;gt; .claude/asdf.env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nix&lt;/strong&gt;: &lt;code&gt;nix print-dev-env | grep ^export &amp;gt; .claude/nix.env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;direnv&lt;/strong&gt;: &lt;code&gt;direnv export json | jq -r 'to_entries[] | &amp;quot;export \(.key)=\&amp;quot;\(.value)\&amp;quot;&amp;quot;' &amp;gt; .claude/direnv.env&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then point &lt;code&gt;CLAUDE_ENV_FILE&lt;/code&gt; at it and you&amp;rsquo;re set.&lt;/p&gt;
&lt;h2 id="caveats"&gt;Caveats&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The file is sourced, not executed—shebang lines are ignored.&lt;/li&gt;
&lt;li&gt;Changes to &lt;code&gt;settings.local.json&lt;/code&gt; take effect on the next Claude session.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;CLAUDE_ENV_FILE&lt;/code&gt; is a relative path, it&amp;rsquo;s resolved from the project root.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A small config file saves you from fighting invisible &lt;code&gt;PATH&lt;/code&gt; issues. Set it once, and Claude runs the same tools you do.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/dev-environment/">Dev Environment</category><category domain="https://blog.iany.me/tags/environment-variables/">Environment Variables</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/tool/">Tool</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2026-06-20</title><link>https://blog.iany.me/journal/2026/06/what-i-touched-this-week-2026-06-20/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2026/06/what-i-touched-this-week-2026-06-20/</guid><description>&lt;p&gt;&lt;strong&gt;Focus&lt;/strong&gt;: &lt;a href="https://kb.iany.me/para/lets/p/Painting/Proko&amp;#43;-&amp;#43;Figure&amp;#43;Drawing&amp;#43;Fundamentals"&gt;Proko - Figure Drawing Fundamentals&lt;/a&gt;&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="fault-injection-and-distributed-systems-testing"&gt;Fault Injection and Distributed Systems Testing&lt;/h2&gt;
&lt;p&gt;This week&amp;rsquo;s reading and hands-on work converged on how to test distributed systems under failure. I finished the &lt;a href="https://github.com/jepsen-io/jepsen/blob/main/doc/tutorial/04-checker.md"&gt;Jepsen etcd demo tutorial&lt;/a&gt; while the Readwise sync delivered a stack of papers and posts on chaos engineering, lineage-based failure testing, and deterministic simulation.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pingcap.co.jp/blog/chaos-practice-in-tidb/"&gt;From Chaos to Order — TiDB Testing&lt;/a&gt; catalogs a practical fault-injection toolkit: SIGSTOP/SIGCONT for process hangs, &lt;code&gt;renice&lt;/code&gt;/&lt;code&gt;setpriority&lt;/code&gt; for CPU contention, &lt;code&gt;iptables&lt;/code&gt;/&lt;code&gt;tc&lt;/code&gt; for network drops and reordering, libfuse/libfiu for I/O faults, and gofail/fail-rs for compile-time injection points. The workflow is hypothesis → inject → analyze.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://readwise.io/bookreview/61307343"&gt;Automating Failure Testing at Internet Scale&lt;/a&gt; (Netflix) inverts the search: start from a successful outcome, build a lineage graph of contributing computations, then find minimal fault combinations that would prevent it. Netflix traces requests through services to locate injection points and treats missing error metrics as failures.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sled.rs/simulation.html"&gt;Sled Simulation Guide&lt;/a&gt; offers a lightweight pattern: model nodes as state machines that receive messages and emit outgoing messages per tick. When an invariant breaks, shrink the initial request set to a minimal reproducer.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.polarsignals.com/blog/posts/2024/05/28/mostly-dst-in-go"&gt;Mostly Deterministic Simulation Testing in Go&lt;/a&gt; shows how to make Go tests reproducible by compiling to WASM, seeding the runtime RNG via environment variable, and advancing virtual time only when all goroutines block.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.infoq.com/articles/cap-twelve-years-later-how-the-rules-have-changed/"&gt;CAP Twelve Years Later&lt;/a&gt; reframes partitions as time bounds on communication: detect partition, enter partition mode, then recover by merging divergent histories with version vectors.&lt;/li&gt;
&lt;li&gt;I also started learning &lt;a href="https://github.com/apple/foundationdb/tree/main/flow#using-flow"&gt;FoundationDB Flow&lt;/a&gt;—an actor-style concurrency model where &lt;code&gt;Promise&lt;/code&gt;/&lt;code&gt;Future&lt;/code&gt; pairs and &lt;code&gt;wait()&lt;/code&gt; in ACTOR functions underpin the database&amp;rsquo;s fault-tolerant architecture.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="reviewing-agent-pull-requests"&gt;Reviewing Agent Pull Requests&lt;/h2&gt;
&lt;p&gt;As AI agents produce more code, the review workflow shifts from line-by-line reading to judgment calls only a human can make. Two guides from this week&amp;rsquo;s sync define a practical checklist.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.blog/ai-and-ml/generative-ai/agent-pull-requests-are-everywhere-heres-how-to-review-them/"&gt;Agent Pull Requests Are Everywhere&lt;/a&gt; (Andrea Griffiths): annotate the diff with context before tagging reviewers; self-review is mandatory respect for reviewer time; search for duplicate helpers and block merge until consolidated; trace the critical path end-to-end checking boundary conditions and permission checks on every branch; require a test that fails on pre-change behavior.&lt;/li&gt;
&lt;li&gt;Red flags to send back: more than five unrelated files touched, purpose not describable in one sentence, empty PR body or missing implementation plan. Ask the author to break it into scoped units or add a structural summary before investing review time.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://readwise.io/"&gt;Agentic Code Review&lt;/a&gt; (Addy Osmani): separate analysis from execution with a human approval gate for production changes; let automated review run first as a prerequisite, not a replacement; never evaluate model output without human validation.&lt;/li&gt;
&lt;li&gt;The through-line: agents handle volume, humans supply intent verification, deduplication discipline, and end-to-end correctness tracing.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="grit-and-figure-drawing-practice"&gt;Grit and Figure Drawing Practice&lt;/h2&gt;
&lt;p&gt;Personal development reading intersected with deliberate skill-building this week. I synced &lt;a href="https://readwise.io/bookreview/61388063"&gt;Grit&lt;/a&gt; by Angela Duckworth and continued the &lt;a href="https://www.proko.com/course/figure-drawing-fundamentals"&gt;Proko Figure Drawing Fundamentals&lt;/a&gt; course—measuring techniques, idealistic proportions, and shading light and form.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Duckworth&amp;rsquo;s core formula: achievement = talent × effort². Effort builds skill &lt;em&gt;and&lt;/em&gt; makes skill productive; consistency over time beats bursts of intensity.&lt;/li&gt;
&lt;li&gt;The &amp;ldquo;naturalness bias&amp;rdquo; hides how much work excellence requires—we prefer to attribute success to innate talent rather than sustained practice.&lt;/li&gt;
&lt;li&gt;Goal hierarchy matters: grit is holding the same top-level goal for a long time while being willing to abandon lower-level goals that no longer serve it. The hard part is deciding what &lt;em&gt;not&lt;/em&gt; to do.&lt;/li&gt;
&lt;li&gt;The Proko lessons this week applied the same principle at the craft level—measuring with head units, constructing idealistic figure proportions, then rendering form with light and shadow. Each lesson is a small, repeatable drill that compounds.&lt;/li&gt;
&lt;li&gt;Weekly plan alignment: close the figure drawing course by copying the demo drawing—treating the final piece as integration of many individual, doable feats.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Siddon&amp;#43;Tang&amp;#43;-&amp;#43;From&amp;#43;Chaos&amp;#43;to&amp;#43;Order&amp;#43;%E2%80%94&amp;#43;Tools&amp;#43;and&amp;#43;Techniques&amp;#43;for&amp;#43;Testing&amp;#43;TiDB%2C&amp;#43;a&amp;#43;Distributed&amp;#43;NewSQL&amp;#43;Database&amp;#43;%28Highlights%29"&gt;Siddon Tang - From Chaos to Order — Tools and Techniques for Testing TiDB, a Distributed NewSQL Database (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Peter&amp;#43;Alvaro&amp;#43;et&amp;#43;al.&amp;#43;-&amp;#43;Automating&amp;#43;Failure&amp;#43;Testing&amp;#43;Research&amp;#43;at&amp;#43;Internet&amp;#43;Scale&amp;#43;%28Highlights%29"&gt;Peter Alvaro et al. - Automating Failure Testing Research at Internet Scale (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Alfonso&amp;#43;Subiotto&amp;#43;Marqu%C3%A9s&amp;#43;-&amp;#43;%28Mostly%29&amp;#43;Deterministic&amp;#43;Simulation&amp;#43;Testing&amp;#43;in&amp;#43;Go&amp;#43;%28Highlights%29"&gt;Alfonso Subiotto Marqués - (Mostly) Deterministic Simulation Testing in Go (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Sled&amp;#43;Authors&amp;#43;-&amp;#43;Sled&amp;#43;Simulation&amp;#43;Guide&amp;#43;%28Highlights%29"&gt;Sled Authors - Sled Simulation Guide (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Eric&amp;#43;Brewer&amp;#43;-&amp;#43;CAP&amp;#43;Twelve&amp;#43;Years&amp;#43;Later&amp;#43;How&amp;#43;the&amp;#43;Rules&amp;#43;Have&amp;#43;Changed&amp;#43;%28Highlights%29"&gt;Eric Brewer - CAP Twelve Years Later How the Rules Have Changed (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/FoundationDB&amp;#43;Authors&amp;#43;-&amp;#43;FoundationDB&amp;#43;Flow&amp;#43;%28Highlights%29"&gt;FoundationDB Authors - FoundationDB Flow (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Andrea&amp;#43;Griffiths&amp;#43;-&amp;#43;Agent&amp;#43;Pull&amp;#43;Requests&amp;#43;Are&amp;#43;Everywhere.&amp;#43;Here%E2%80%99s&amp;#43;How&amp;#43;to&amp;#43;Review&amp;#43;Them.&amp;#43;%28Highlights%29"&gt;Andrea Griffiths - Agent Pull Requests Are Everywhere. Here’s How to Review Them. (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Tweets/Addy&amp;#43;Osmani&amp;#43;-&amp;#43;Agentic&amp;#43;Code&amp;#43;Review&amp;#43;%28Highlights%29"&gt;Addy Osmani - Agentic Code Review (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Books/Angela&amp;#43;Duckworth&amp;#43;-&amp;#43;Grit&amp;#43;%28Highlights%29"&gt;Angela Duckworth - Grit (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/p/Painting/Proko&amp;#43;-&amp;#43;Figure&amp;#43;Drawing&amp;#43;Fundamentals"&gt;Proko - Figure Drawing Fundamentals&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2026-06-13</title><link>https://blog.iany.me/journal/2026/06/what-i-touched-this-week-2026-06-13/</link><pubDate>Sat, 13 Jun 2026 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2026/06/what-i-touched-this-week-2026-06-13/</guid><description>&lt;p&gt;&lt;strong&gt;Focus&lt;/strong&gt;: &lt;a href="https://kb.iany.me/para/lets/k/Kleppmann&amp;#43;-&amp;#43;Designing&amp;#43;data-intensive&amp;#43;applications/%E2%99%AF&amp;#43;Kleppmann&amp;#43;-&amp;#43;Designing&amp;#43;data-intensive&amp;#43;applications"&gt;♯ Kleppmann - Designing data-intensive applications&lt;/a&gt;&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="systems-correctness-and-randomized-testing"&gt;Systems Correctness and Randomized Testing&lt;/h2&gt;
&lt;p&gt;This week&amp;rsquo;s Readwise sync centered on the DDIA second-edition thread about formal methods and randomized testing. The papers form a practical stack: classify what breaks in production, explain why random partition tests work, and describe how AWS validates correctness at scale.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://spawn-queue.acm.org/doi/pdf/10.1145/3712057"&gt;Systems Correctness Practices At AWS&lt;/a&gt; combines lightweight formal approaches—property-based testing, fuzzing, runtime monitoring—with tools like P for state-machine modeling and PObserve for post-hoc log validation against specifications. Open-source &lt;a href="https://github.com/awslabs/shuttle"&gt;shuttle&lt;/a&gt; and &lt;a href="https://github.com/tokio-rs/turmoil"&gt;turmoil&lt;/a&gt; extend build-time testing of thread ordering and failure injection.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://read.readwise.io/read/01ktwhbafgb2jtkmyemg0pqmqz"&gt;Why Is Random Testing Effective for Partition Tolerance Bugs?&lt;/a&gt; formalizes the Jepsen pattern: a short setup sequence, a carefully chosen partition, operations in each block, heal, repeat. Separating coverage and minority isolation explain why random tests find most partition bugs with surprisingly small schedules.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://readwise.io/bookreview/61162320"&gt;What Bugs Live in the Cloud?&lt;/a&gt; catalogs 3,000+ cloud issues: data consistency, scalability, and topology bugs are distinct from classic distributed failures. Operational protocols (bootstrap, cloning, fsck) carry data inconsistency bugs because they run on cold paths. Failover-on-failover is brittle; systems must distinguish hardware failure from software logic bugs.&lt;/li&gt;
&lt;li&gt;I also started exploring &lt;a href="https://fizzbee.io/"&gt;FizzBee&lt;/a&gt; as a Python-like formal modeling tool with built-in fault injection and MBT support—promising ergonomics for the same problem space.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="from-data-lakes-to-iceberg-lakehouses"&gt;From Data Lakes to Iceberg Lakehouses&lt;/h2&gt;
&lt;p&gt;A second cluster of highlights mapped the evolution from schema-on-write warehouses to schema-on-read lakes, and now to lakehouses that unify cheap object storage with warehouse-style analytics.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://readwise.io/bookreview/61259460"&gt;Data Lakes: A Survey of Functions and Systems&lt;/a&gt; frames the core trade-off: lakes store raw data in original formats with a common access interface, but metadata and governance determine whether ad-hoc analytics is feasible. The survey covers ingestion tiers, metadata modeling (data vault, enterprise knowledge graphs), dataset discovery (Aurum, JOSIE), and the lakehouse convergence that avoids maintaining separate lake and warehouse systems.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://iceberglakehouse.com/posts/2024-12-2025-guide-architecting-an-iceberg-lakehouse/"&gt;2025 Guide to Architecting an Iceberg Lakehouse&lt;/a&gt; turns theory into an architecture checklist: self-audit current data placement and SLAs first, then standardize on Parquet plus Iceberg metadata, pick catalog (Nessie, Polaris, Glue), ingestion (Spark, Flink, Kafka Connect), and query engines (Dremio, Trino). Dremio&amp;rsquo;s ability to federate non-Iceberg sources alongside Iceberg tables lowers migration risk.&lt;/li&gt;
&lt;li&gt;The lakehouse pattern aligns with a broader shift toward analytical systems and data engineering—an area I&amp;rsquo;m exploring as part of a career transition.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="local-first-data-ownership"&gt;Local-First Data Ownership&lt;/h2&gt;
&lt;p&gt;The local-first essay rounds out the week&amp;rsquo;s data-systems reading with a user-centric counterpoint to cloud defaults: you should own your data even when services run in the cloud.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.inkandswitch.com/essay/local-first/"&gt;You own your data, in spite of the cloud&lt;/a&gt; argues that Git/GitHub is the closest existing model for local-first collaboration—data lives on your machine, sync is explicit, and the remote is optional infrastructure rather than the source of truth.&lt;/li&gt;
&lt;li&gt;CRDTs and tools like &lt;a href="https://github.com/automerge/automerge"&gt;Automerge&lt;/a&gt; offer a technical foundation for convergence without a central coordinator, which maps directly to DDIA 2nd edition&amp;rsquo;s new section on sync engines and local-first software.&lt;/li&gt;
&lt;li&gt;The essay&amp;rsquo;s further-reading list (Baboulevitch&amp;rsquo;s &lt;em&gt;Data Laced with History&lt;/em&gt;, Kleppmann&amp;rsquo;s &lt;em&gt;Convergence vs Consensus&lt;/em&gt;, formal CRDT verification) is a good bridge from informal local-first ideals to the formal-methods thread above.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/k/Kleppmann&amp;#43;-&amp;#43;Designing&amp;#43;data-intensive&amp;#43;applications/%E2%99%AF&amp;#43;Kleppmann&amp;#43;-&amp;#43;Designing&amp;#43;data-intensive&amp;#43;applications"&gt;♯ Kleppmann - Designing data-intensive applications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/f/Formal&amp;#43;Method/FizzBee"&gt;FizzBee&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Marc&amp;#43;Brooker&amp;#43;et&amp;#43;al.&amp;#43;-&amp;#43;Systems&amp;#43;Correctness&amp;#43;Practices&amp;#43;At&amp;#43;Aws&amp;#43;%28Highlights%29"&gt;Marc Brooker et al. - Systems Correctness Practices At Aws (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Rupak&amp;#43;Majumdar&amp;#43;et&amp;#43;al.&amp;#43;-&amp;#43;Why&amp;#43;Is&amp;#43;Random&amp;#43;Testing&amp;#43;Effective&amp;#43;for&amp;#43;Partition&amp;#43;Tolerance&amp;#43;Bugs&amp;#43;%28Highlights%29"&gt;Rupak Majumdar et al. - Why Is Random Testing Effective for Partition Tolerance Bugs (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Haryadi&amp;#43;S.&amp;#43;Gunawi&amp;#43;et&amp;#43;al.&amp;#43;-&amp;#43;What&amp;#43;Bugs&amp;#43;Live&amp;#43;in&amp;#43;the&amp;#43;Cloud&amp;#43;A&amp;#43;Study&amp;#43;of&amp;#43;3000%2B&amp;#43;Issues&amp;#43;in&amp;#43;Cloud&amp;#43;Systems&amp;#43;%28Highlights%29"&gt;Haryadi S. Gunawi et al. - What Bugs Live in the Cloud A Study of 3000+ Issues in Cloud Systems (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Rihan&amp;#43;Hai&amp;#43;et&amp;#43;al.&amp;#43;-&amp;#43;Data&amp;#43;Lakes&amp;#43;A&amp;#43;Survey&amp;#43;of&amp;#43;Functions&amp;#43;and&amp;#43;Systems&amp;#43;%28Highlights%29"&gt;Rihan Hai et al. - Data Lakes A Survey of Functions and Systems (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Alex&amp;#43;Merced&amp;#43;-&amp;#43;2025&amp;#43;Guide&amp;#43;to&amp;#43;Architecting&amp;#43;an&amp;#43;Iceberg&amp;#43;Lakehouse&amp;#43;%28Highlights%29"&gt;Alex Merced - 2025 Guide to Architecting an Iceberg Lakehouse (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Martin&amp;#43;Kleppmann&amp;#43;et&amp;#43;al.&amp;#43;-&amp;#43;You&amp;#43;own&amp;#43;your&amp;#43;data%2C&amp;#43;in&amp;#43;spite&amp;#43;of&amp;#43;the&amp;#43;cloud&amp;#43;%28Highlights%29"&gt;Martin Kleppmann et al. - You own your data, in spite of the cloud (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2026-03-30</title><link>https://blog.iany.me/journal/2026/03/what-i-touched-this-week-2026-03-30/</link><pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2026/03/what-i-touched-this-week-2026-03-30/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="convincing-vs-persuading-in-technical-work"&gt;Convincing vs Persuading in Technical Work&lt;/h2&gt;
&lt;p&gt;This week sharpened a useful distinction: being correct is only the starting point, while moving a system or a team requires persuasion tuned to real context. That frame maps directly to architecture work, migration plans, and cross-team decisions.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.alaindichiappari.dev/p/convincing-is-not-persuading"&gt;Convincing Is Not Persuading&lt;/a&gt; highlights that proof and benchmarks convince, but adoption depends on trust, timing, and incentives.&lt;/li&gt;
&lt;li&gt;The practical takeaway is to pair correctness with audience-aware framing: what this change protects, what risk it removes, and what people can safely do next.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="scale-first-ai-thinking-and-mental-models"&gt;Scale-First AI Thinking and Mental Models&lt;/h2&gt;
&lt;p&gt;The Readwise set this week converged on a coherent AI posture: favor general methods that scale with compute, then use explicit mental models to reason about systems and constraints.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.cs.utexas.edu/~eunsol/courses/data/bitter_lesson.pdf"&gt;The Bitter Lesson&lt;/a&gt; reinforces a long-run strategy: approaches that scale with computation and learning tend to dominate handcrafted knowledge-heavy systems.&lt;/li&gt;
&lt;li&gt;&lt;a href="http://muratbuffalo.blogspot.com/2026/03/tla-mental-models.html"&gt;TLA+ Mental Models&lt;/a&gt; contributed a systems lens (state, logs, replication) that is useful for evaluating reliability claims, not just feature velocity.&lt;/li&gt;
&lt;li&gt;This combination was a good backbone for AI reflection this week: keep abstractions general, but reason rigorously about behavior under change.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="boring-tech-better-practices-and-tooling-leverage"&gt;Boring Tech, Better Practices, and Tooling Leverage&lt;/h2&gt;
&lt;p&gt;A recurring theme across highlights and bookmarks was pragmatic leverage: pick stable tools, then innovate in process, automation, and integration points.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://buttondown.com/hillelwayne/archive/choose-boring-technology-and-innovative-practices/"&gt;Choose Boring Technology and Innovative Practices&lt;/a&gt; captures the principle well: conservatism in stack choice enables boldness in operational practice.&lt;/li&gt;
&lt;li&gt;Recent bookmarks aligned with this approach, including &lt;a href="https://github.com/doitian/RustTraining"&gt;RustTraining&lt;/a&gt;, &lt;a href="https://github.com/saber-notes/saber"&gt;Saber&lt;/a&gt;, and monitor-switch automation via &lt;a href="https://github.com/doitian/dotfiles-public/blob/master/src/monctl.js"&gt;monctl.js&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Weekly hygiene (bookmark triage, highlights cleanup, sync routines) made this strategy actionable rather than theoretical.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Alain&amp;#43;Di&amp;#43;Chiappari&amp;#43;-&amp;#43;Convincing&amp;#43;Is&amp;#43;Not&amp;#43;Persuading&amp;#43;%28Highlights%29"&gt;Alain Di Chiappari - Convincing Is Not Persuading (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Rich&amp;#43;Sutton&amp;#43;-&amp;#43;The&amp;#43;Bitter&amp;#43;Lesson&amp;#43;%28Highlights%29"&gt;Rich Sutton - The Bitter Lesson (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Computer&amp;#43;Things&amp;#43;-&amp;#43;Choose&amp;#43;Boring&amp;#43;Technology&amp;#43;and&amp;#43;Innovative&amp;#43;Practices&amp;#43;%28Highlights%29"&gt;Computer Things - Choose Boring Technology and Innovative Practices (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Murat&amp;#43;Demirbas&amp;#43;-&amp;#43;TLA%2B&amp;#43;Mental&amp;#43;Models&amp;#43;%28Highlights%29"&gt;Murat Demirbas - TLA+ Mental Models (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2026-03-21</title><link>https://blog.iany.me/journal/2026/03/what-i-touched-this-week-2026-03-21/</link><pubDate>Sat, 21 Mar 2026 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2026/03/what-i-touched-this-week-2026-03-21/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="software-maintenance-and-the-specification-problem"&gt;Software Maintenance and the Specification Problem&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://nearzero.software/p/warranty-void-if-regenerated"&gt;Scott Werner&amp;rsquo;s &amp;ldquo;Warranty Void if Regenerated&amp;rdquo;&lt;/a&gt; explores why software needs continuous tending, not just initial development. The core insight: specifications describe static relationships, but real-world inputs are alive—feeds from other systems that are constantly being updated, recalibrated, and regenerated.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The maintenance paradox&lt;/strong&gt;: Paying for maintenance means admitting vulnerability, while paying for repair means responding to an emergency. Humans find emergencies more motivating than vulnerabilities.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Domain knowledge is irreplaceable&lt;/strong&gt;: People who understood the domain and could diagnose specification problems were the most valuable. This knowledge is often embodied, contextual, and inarticulable—like knowing the clay underneath a greenhouse or how prevailing winds affect crop rows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Software Choreographer concept&lt;/strong&gt;: A role that maps your entire tool ecosystem, specifies interfaces between them, and builds a conformance layer so that when any tool regenerates, interfaces are verified before the new version goes live. Like shipping containers: the container was cheap; organizing the logistics was where all the value was.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Physical controls matter&lt;/strong&gt;: Always offer a physical control—a button, switch, or lever. Something the client can touch. This builds trust and allows graceful degradation when systems fail.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="ai-agents-and-the-harness"&gt;AI Agents and the Harness&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://stratechery.com/2026/agents-over-bubbles/"&gt;Ben Thompson&amp;rsquo;s &amp;ldquo;Agents Over Bubbles&amp;rdquo;&lt;/a&gt; argues that the critical component of making agentic workloads work is the &amp;ldquo;harness&amp;rdquo;—the software that actually controls the model, not the model itself.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Abstraction layer&lt;/strong&gt;: Claude Code and OpenAI&amp;rsquo;s Codex abstract the user away from the model. You give instructions to an agent, which directs the model; the agent can also use deterministic tools to verify results.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Models lack goals&lt;/strong&gt;: Large language models are intelligent, but they do not have goals or values or drive. The harness provides the structure and verification.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The opportunity&lt;/strong&gt;: How many apps or services haven&amp;rsquo;t been built, not because one person can&amp;rsquo;t imagine them, but because they haven&amp;rsquo;t had the resources, team, or coordination capabilities to ship them? Agents could change this dynamic.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="simplifying-life-through-lifestyle-design"&gt;Simplifying Life Through Lifestyle Design&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://tim.blog/2026/03/11/how-to-simplify-your-life-2-transcript/"&gt;Tim Ferriss Show episode on simplifying life&lt;/a&gt; brought together insights from Maria Popova, Morgan Housel, Cal Newport, Craig Mod, and Debbie Millman on what it means to simplify.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Read history, not forecasts&lt;/strong&gt;: A good heuristic for your relationship with information is to read more history and fewer forecasts. As Kelly Hayes says, &amp;ldquo;When you haven&amp;rsquo;t engaged with history, everything feels unprecedented.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lifestyle design over triage&lt;/strong&gt;: Cal Newport argues that triage rules don&amp;rsquo;t work—too many things satisfy &amp;ldquo;good enough&amp;rdquo; criteria. Instead, think about lifestyle design: know what conditions of your day-to-day existence are best for you, and design a lifestyle that matches that.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Courage over confidence&lt;/strong&gt;: Debbie Millman distinguishes courage from confidence. Courage means taking risks and steps toward what you want despite how you feel. Confidence comes through repeated success. You don&amp;rsquo;t wait for confidence to arrive; you act from courage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Freedom over control&lt;/strong&gt;: What Millman wanted wasn&amp;rsquo;t more control, but more freedom. That freedom has allowed her to build a very different kind of life.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simplicity as coherence&lt;/strong&gt;: Simplicity isn&amp;rsquo;t only about minimalism. It&amp;rsquo;s also about coherence—things fitting together in a way that makes sense.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nearzero.software/p/warranty-void-if-regenerated"&gt;Warranty Void if Regenerated&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stratechery.com/2026/agents-over-bubbles/"&gt;Agents Over Bubbles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tim.blog/2026/03/11/how-to-simplify-your-life-2-transcript/"&gt;How to Simplify Your Life in 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://collabfund.com/blog/engaging-with-history/"&gt;Engaging With History&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.creativeboom.com/inspiration/debbie-millman/"&gt;Debbie Millman on Courage Over Confidence&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Scott&amp;#43;Werner&amp;#43;-&amp;#43;Warranty&amp;#43;Void&amp;#43;if&amp;#43;Regenerated&amp;#43;%28Highlights%29"&gt;Scott Werner - Warranty Void if Regenerated (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Ben&amp;#43;Thompson&amp;#43;-&amp;#43;Agents&amp;#43;Over&amp;#43;Bubbles&amp;#43;%28Highlights%29"&gt;Ben Thompson - Agents Over Bubbles (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Tim&amp;#43;Ferriss&amp;#43;-&amp;#43;The&amp;#43;Tim&amp;#43;Ferriss&amp;#43;Show&amp;#43;857&amp;#43;How&amp;#43;to&amp;#43;Simplify&amp;#43;Your&amp;#43;Life&amp;#43;in&amp;#43;2026&amp;#43;%28Highlights%29"&gt;Tim Ferriss - The Tim Ferriss Show 857 How to Simplify Your Life in 2026 (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Podcasts/The&amp;#43;Tim&amp;#43;Ferriss&amp;#43;Show&amp;#43;-&amp;#43;857&amp;#43;How&amp;#43;to&amp;#43;Simplify&amp;#43;Your&amp;#43;Life&amp;#43;in&amp;#43;2026&amp;#43;%E2%80%94&amp;#43;New&amp;#43;Tips&amp;#43;From&amp;#43;Maria&amp;#43;Popova%2C&amp;#43;Morgan&amp;#43;Housel%2C&amp;#43;Cal&amp;#43;Newport%2C&amp;#43;Craig&amp;#43;Mod%2C&amp;#43;and&amp;#43;Debbie&amp;#43;Millman&amp;#43;%28Highlights%29"&gt;The Tim Ferriss Show - 857 How to Simplify Your Life in 2026 — New Tips From Maria Popova, Morgan Housel, Cal Newport, Craig Mod, and Debbie Millman (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Morgan&amp;#43;Housel&amp;#43;-&amp;#43;Engaging&amp;#43;With&amp;#43;History&amp;#43;%28Highlights%29"&gt;Morgan Housel - Engaging With History (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Katy&amp;#43;Cowan&amp;#43;-&amp;#43;Debbie&amp;#43;Millman&amp;#43;on&amp;#43;the&amp;#43;Power&amp;#43;of&amp;#43;Courage&amp;#43;Over&amp;#43;Confidence%2C&amp;#43;Embracing&amp;#43;Criticism&amp;#43;and&amp;#43;Overcoming&amp;#43;Fear&amp;#43;%28Highlights%29"&gt;Katy Cowan - Debbie Millman on the Power of Courage Over Confidence, Embracing Criticism and Overcoming Fear (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>Power of Monoid, Beauty of Simplicity</title><link>https://blog.iany.me/2026/02/power-of-monoid-beauty-of-simplicity/</link><pubDate>Fri, 20 Feb 2026 00:00:00 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/02/power-of-monoid-beauty-of-simplicity/</guid><description>&lt;p&gt;A monoid is one of the smallest useful abstractions in algebra: a set closed under an associative binary operation, with an identity element. That simplicity is exactly why it shows up everywhere—from summing numbers and concatenating strings to powering divide-and-conquer algorithms and elegant data structures like finger trees. This post walks through what monoids are, why they give you &amp;ldquo;compute power&amp;rdquo; for free when you can phrase a problem in terms of them, and how to think about choosing the right monoid and predicate when you do.&lt;/p&gt;
&lt;h2 id="what-is-a-monoid"&gt;What is a monoid?&lt;/h2&gt;
&lt;p&gt;A monoid is a set &lt;code&gt;$S$&lt;/code&gt; equipped with a binary operator &lt;code&gt;$\bullet$&lt;/code&gt; and an identity element &lt;code&gt;$e$&lt;/code&gt; (&lt;a href="https://en.wikipedia.org/wiki/Monoid"&gt;Wikipedia&lt;/a&gt;).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The operator is closed on &lt;code&gt;$S$&lt;/code&gt;. For all &lt;code&gt;$a, b \in S$&lt;/code&gt;, the result &lt;code&gt;$a \bullet b$&lt;/code&gt; is also in &lt;code&gt;$S$&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The operator is associative: For all &lt;code&gt;$a,b,c \in S$&lt;/code&gt;, &lt;code&gt;$(a \bullet b) \bullet c = a \bullet (b \bullet c)$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The identity element &lt;code&gt;$e$&lt;/code&gt; satisfies &lt;code&gt;$e \bullet a = a \bullet e = a$&lt;/code&gt; for all &lt;code&gt;$a \in S$&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, The integer numbers with the operator addition (&lt;code&gt;+&lt;/code&gt;) is a monoid, where the identity element is &lt;code&gt;0&lt;/code&gt;. The integer numbers with the operator multiplication (&lt;code&gt;x&lt;/code&gt;) is also a monoid with the identity element &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The set of finite lists with the operator concatenation is a monoid since:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The operator is closed because concatenation of two finite lists is also a finite list.&lt;/li&gt;
&lt;li&gt;The operator is associative, because both &lt;code&gt;$(a \bullet b) \bullet c$&lt;/code&gt; and &lt;code&gt;$a \bullet (b \bullet c)$&lt;/code&gt; result in a new list by placing elements of &lt;code&gt;$a, b, c$&lt;/code&gt; consecutively.&lt;/li&gt;
&lt;li&gt;The identity element is the empty list.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The integer numbers with the operator &lt;code&gt;max&lt;/code&gt; is a counterexample. The operator is closed and associative, but there is no identity element. Given any integer &lt;code&gt;$e$&lt;/code&gt;, there&amp;rsquo;s always a smaller integer &lt;code&gt;$a$&lt;/code&gt; such that &lt;code&gt;$e \bullet a = e \ne a$&lt;/code&gt;. However, &lt;code&gt;max&lt;/code&gt; on the integer set with a lower bound is a monoid, such as the non-negative integers where the identity element is the lower bound &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="divide-and-conquer-why-associativity-matters"&gt;Divide and conquer: why associativity matters&lt;/h2&gt;
&lt;p&gt;At first glance, associativity may seem too trivial to be useful in programming. However, associativity is what enables powerful divide-and-conquer strategies, where problems can be split into parts, solved independently, and then safely recombined.&lt;/p&gt;
&lt;h3 id="exponentiation-by-squaring"&gt;Exponentiation by Squaring&lt;/h3&gt;
&lt;p&gt;Let’s begin with a simple application: repeatedly applying the binary operator to the same element.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\underbrace{a \bullet a \bullet \cdots \bullet a}_{a \text{ appears } n \text{ times}}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of applying the binary operator &lt;code&gt;$n-1$&lt;/code&gt; times sequentially, we exploit associativity to group every two instances of &lt;code&gt;$a$&lt;/code&gt; together recursively. This gives us a smaller problem when n is even:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\underbrace{(a \bullet a) \bullet \cdots \bullet (a \bullet a)}_{(a \bullet a) \text{ appears } \frac{n}{2} \text{ times}}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When n is odd:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
a \bullet \underbrace{(a \bullet a) \bullet \cdots \bullet (a \bullet a)}_{(a \bullet a) \text{ appears } \frac{n-1}{2} \text{ times}}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We only need to compute &lt;code&gt;$a \bullet a$&lt;/code&gt; once to turn the problem of size &lt;code&gt;$n$&lt;/code&gt; to size &lt;code&gt;$n/2$&lt;/code&gt;. Repeating the process on &lt;code&gt;$(a \bullet a)$&lt;/code&gt; gives the &lt;a href="https://en.wikipedia.org/wiki/Exponentiation_by_squaring"&gt;Exponentiation by Squaring&lt;/a&gt; algorithm, which requires at most &lt;code&gt;$\displaystyle 2 \lfloor \log _{2}n\rfloor$&lt;/code&gt; computations that is more efficient than &lt;code&gt;$n-1$&lt;/code&gt; when &lt;code&gt;$n$&lt;/code&gt; is greater than 4.&lt;/p&gt;
&lt;p&gt;For integers or real numbers under multiplication (&lt;code&gt;×&lt;/code&gt;), exponentiation by squaring is an efficient algorithm to compute positive integer powers. Since the set of elliptic curve points under point addition forms a monoid, this same method can also be used to compute &lt;a href="https://kb.iany.me/para/lets/c/Cryptography/Elliptic&amp;#43;Curve&amp;#43;Scalar&amp;#43;Multiplication"&gt;Elliptic Curve Scalar Multiplication&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="general-divide-and-conquer-search-algorithm"&gt;General Divide-and-Conquer Search Algorithm&lt;/h3&gt;
&lt;p&gt;We can generalize the divide-and-conquer method to search an element in a sequence based solely on associativity.&lt;/p&gt;
&lt;p&gt;The result of applying the monoid operator to a sequence from left to right serves as a summary of that sequence. If a predicate can determine whether a given element is in the sequence based solely on this summary, we can devise a general divide-and-conquer search algorithm.&lt;/p&gt;
&lt;p&gt;Let’s say we want to search for a target element in the sequence &lt;code&gt;$t_1, \ldots, t_n$&lt;/code&gt;, where each &lt;code&gt;$t_i$&lt;/code&gt; belongs to a monoid &lt;code&gt;$(S, \bullet, e)$&lt;/code&gt;. Here, &lt;code&gt;$S$&lt;/code&gt; is the underlying set, &lt;code&gt;$\bullet$&lt;/code&gt; is the binary operation, and &lt;code&gt;$e$&lt;/code&gt; is the identity element.&lt;/p&gt;
&lt;p&gt;We don&amp;rsquo;t know which kind of predicates works for the search algorithm. Let&amp;rsquo;s give a best guess that the predicate &lt;code&gt;$p$&lt;/code&gt; is a function of the monoid &amp;ldquo;summary&amp;rdquo; that &lt;code&gt;$p(t_1 \bullet \cdots \bullet t_n)$&lt;/code&gt; is true if and only if &lt;code&gt;$t$&lt;/code&gt; is in the sequence &lt;code&gt;$t_1, \dots, t_n$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Assume that &lt;code&gt;$p(t_1 \bullet \cdots \bullet t_n)$&lt;/code&gt; is true, thus the target element &lt;code&gt;$t$&lt;/code&gt; is in the sequence. We divide the sequence into two halves: &lt;code&gt;$t_1,\ldots,t_k$&lt;/code&gt; and &lt;code&gt;$t_{k+1},\ldots,t_n$&lt;/code&gt;, where &lt;code&gt;$1 \le k \le n$&lt;/code&gt;. We then evaluate &lt;code&gt;$p(t_1 \bullet \cdots \bullet t_k)$&lt;/code&gt; to determine whether the target element lies in the first or second half, and continue the search.&lt;/p&gt;
&lt;p&gt;Based on this observation, we can deduce the following property of the predicate: there exists an index &lt;code&gt;$x$&lt;/code&gt; such that&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
p(t_1 \bullet \cdots \bullet t_k) := \begin{cases}
\text{false} &amp;amp; \text{if } k &amp;lt; x, \\
\text{true} &amp;amp; \text{if } k \ge x.
\end{cases}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;$t_x$&lt;/code&gt; is the target element if such &lt;code&gt;$x$&lt;/code&gt; exists; otherwise, the target element does not exist in the sequence.&lt;/p&gt;
&lt;p&gt;Intuitively, the target element is the turning point at which the predicate on the running summary changes from false to true.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Monoid Search Turning Point.excalidraw"
loading="lazy"
src="https://blog.iany.me/2026/02/power-of-monoid-beauty-of-simplicity/Monoid%20search%20turning%20point.excalidraw.svg" /&gt;
&lt;/figure&gt;
&lt;p&gt;Note that &lt;code&gt;$p$&lt;/code&gt; makes sense only on the summary of any prefix of the sequence. If we need to continue the search in the second half, we must remember the summary of the scanned prefix.&lt;/p&gt;
&lt;p&gt;Now we can define the search algorithm &lt;code&gt;$\mathrm{Search}(p, s, \{t_i,\ldots,t_j\})$&lt;/code&gt; where&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$s$&lt;/code&gt; is the summary of scanned prefix &lt;code&gt;$t_1 \bullet \cdots \bullet t_{i-1}$&lt;/code&gt; when &lt;code&gt;$i &amp;gt; 1$&lt;/code&gt; or the identity element &lt;code&gt;$e$&lt;/code&gt; otherwise.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$t_i, \ldots, t_j$&lt;/code&gt; is the sub-range to search next.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$p$&lt;/code&gt; is the predicate as defined above&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The algorithm proceeds as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;$p(s \bullet t_i \bullet \cdots \bullet t_j$&lt;/code&gt;) is false, the target element does not exist. The algorithm aborts with an error.&lt;/li&gt;
&lt;li&gt;Otherwise, if there&amp;rsquo;s only one element (&lt;code&gt;$i = j$&lt;/code&gt;), &lt;code&gt;$t_i$&lt;/code&gt; is the target element. The algorithm aborts with the found result.&lt;/li&gt;
&lt;li&gt;Otherwise, choose a pivot index &lt;code&gt;$i \le m \lt j$&lt;/code&gt; to split the sequence into two nonempty halves: &lt;code&gt;$t_i, \ldots, t_m$&lt;/code&gt; and &lt;code&gt;$t_{m+1},\ldots,t_j$&lt;/code&gt;. Test &lt;code&gt;$p(s \bullet t_i \bullet \cdots \bullet t_m)$&lt;/code&gt; that
&lt;ul&gt;
&lt;li&gt;If it is true, continue the search in the first half: &lt;code&gt;$\mathrm{Search}(p, s, \{t_i,\ldots,t_m\})$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Otherwise, continue the search in the second half: &lt;code&gt;$\mathrm{Search}(p, s \bullet (t_i \bullet \cdots \bullet t_m),\{t_{m+1},\ldots,t_j\})$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The algorithm starts with &lt;code&gt;$\mathrm{Search}(p, e, \{t_1, \ldots, t_k\})$&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="application-random-access-sequence"&gt;Application: Random-Access Sequence&lt;/h3&gt;
&lt;p&gt;An application of the search algorithm is accessing the nth element in the sequence.&lt;/p&gt;
&lt;p&gt;We initialize the sequence to all 1s and use the monoid of non-negative integers with addition &lt;code&gt;$(\mathbb{N},+,0)$&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\underbrace{1, \ldots, 1}_{n \text{ times}}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The predicate to find the i-th (starting from 0) element is:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
p_i(s) := s &amp;gt; i
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It may seem silly to search for the i-th 1 in a sequence of 1s, but we can store any data in the sequence and attach the monoid values as annotations to guide the search algorithm.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Sequence Monoid Annotations.excalidraw"
loading="lazy"
src="https://blog.iany.me/2026/02/power-of-monoid-beauty-of-simplicity/Sequence%20Monoid%20Annotations.excalidraw.svg" /&gt;
&lt;/figure&gt;
&lt;h3 id="application-max-priority-queue"&gt;Application: Max-Priority Queue&lt;/h3&gt;
&lt;p&gt;Another application is finding the element with the max priority.&lt;/p&gt;
&lt;p&gt;We use the monoid of non-negative integers with operator &lt;code&gt;max&lt;/code&gt; &lt;code&gt;$(\mathbb{N},\mathrm{max},0)$&lt;/code&gt; and assume that the maximum value has the maximum priority.&lt;/p&gt;
&lt;p&gt;The predicate to find the element with the max priority is&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
p(s) := s = m
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where &lt;code&gt;$m$&lt;/code&gt; is the monoid summary of the entire sequence—that is, the maximum value in the sequence. The predicate checks whether the summary equals to &lt;code&gt;$m$&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="annotated-search-tree"&gt;Annotated Search Tree&lt;/h3&gt;
&lt;p&gt;A natural way to support the divide-and-conquer search is an &lt;em&gt;annotated binary tree&lt;/em&gt;. Store the sequence elements at the leaves, and at each node store the monoid summary of the subtree—e.g. the sum of lengths or the maximum priority in that subtree. The predicate can then be evaluated on the left subtree’s annotation to decide whether to descend left or right, and the prefix summary is updated when going right by combining it with the left subtree’s summary.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Annotated Binary Tree.excalidraw"
loading="lazy"
src="https://blog.iany.me/2026/02/power-of-monoid-beauty-of-simplicity/Annotated%20binary%20tree.excalidraw.svg" /&gt;
&lt;/figure&gt;
&lt;p&gt;A plain binary tree can degenerate to a list in the worst case, so operations may become linear. A more advanced structure, the &lt;em&gt;finger tree&lt;/em&gt;&lt;sup id="fnref:1"&gt;&lt;span class="footnote-ref" role="doc-noteref"&gt;1&lt;/span&gt;&lt;/sup&gt;, keeps the tree balanced and supports efficient access at both ends and in the middle; each node carries a monoidal “measure” of its subtree, and the same search strategy applies. In Haskell, &lt;a href="https://hackage-content.haskell.org/package/containers-0.8/docs/Data-Sequence.html"&gt;Data.Sequence&lt;/a&gt; from the &lt;code&gt;containers&lt;/code&gt; library implements sequences as finger trees with size (length) as the measure, giving &lt;code&gt;$O(\log n)$&lt;/code&gt; indexing, splitting, and concatenation.&lt;/p&gt;
&lt;h3 id="utility-of-the-identity-element"&gt;Utility of the Identity Element&lt;/h3&gt;
&lt;p&gt;The general divide-and-conquer algorithm does not require a monoid—only a semigroup. A semigroup is a fancy word for a set equipped with a closed, associative binary operator but lacking an identity element. The presence of an identity element makes monoids convenient to work with.&lt;/p&gt;
&lt;p&gt;The identity element serves as a natural default value or starting point for algorithms. For instance, in the search algorithm, the summary of the scanned prefix is initialized to &lt;code&gt;$e$&lt;/code&gt;. Without an identity element, we need an additional flag to indicate whether any prefix has been scanned, and the algorithm would have to branch conditionally based on that flag.&lt;/p&gt;
&lt;h2 id="the-art-of-choosing-monoid-and-predicate"&gt;The art of choosing monoid and predicate&lt;/h2&gt;
&lt;p&gt;In the random-access example we used &lt;code&gt;$(\mathbb{N}, +, 0)$&lt;/code&gt; and annotated each position with &lt;code&gt;$1$&lt;/code&gt;—the summary of a segment is its length, and the predicate &lt;code&gt;$s &amp;gt; i$&lt;/code&gt; tells us whether the &lt;code&gt;$i$&lt;/code&gt;-th element lies in the prefix we have so far. In the max-priority queue we used &lt;code&gt;$(\mathbb{N}, \max, 0)$&lt;/code&gt; (or a bounded variant): the summary is the maximum value in the segment, and the predicate &lt;code&gt;$s = m$&lt;/code&gt; identifies the segment that contains the global maximum. In both cases, the monoid was chosen so that the &lt;em&gt;combined&lt;/em&gt; summary over a range is exactly what the predicate needs to decide where to go next.&lt;/p&gt;
&lt;p&gt;The flip side is that finding both the right monoid and the right predicate can be tricky. At each step the search has access only to the monoid summary of the prefix (or segment) seen so far, so the predicate must be decided from that summary alone. The monoid must be rich enough to supply the information the predicate needs. Sometimes the natural summary (e.g. sum or max) suggests the predicate (e.g. &lt;code&gt;$s &amp;gt; i$&lt;/code&gt; or &lt;code&gt;$s = m$&lt;/code&gt;). Sometimes you must try a different carrier or operation, or encode extra information into the monoid (e.g. pairs or custom types), so that the predicate can be expressed. There is no universal recipe—it is a matter of design and experimentation. Reframe the problem as: “What do I need to know about a segment to decide the next step?” Then choose a monoid that can represent that knowledge and a predicate that uses it.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Hinze, R., &amp;amp; Paterson, R. (2006). Finger trees: A simple general-purpose data structure. &lt;em&gt;Journal of Functional Programming, 16&lt;/em&gt;(2), 197–217. Cambridge University Press. &lt;a href="https://www.cs.ox.ac.uk/ralf.hinze/publications/FingerTrees.pdf"&gt;https://www.cs.ox.ac.uk/ralf.hinze/publications/FingerTrees.pdf&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/algorithm/">Algorithm</category><category domain="https://blog.iany.me/tags/math/">Math</category><category domain="https://blog.iany.me/tags/programming/">Programming</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2026-02-07</title><link>https://blog.iany.me/journal/2026/02/what-i-touched-this-week-2026-02-07/</link><pubDate>Sat, 07 Feb 2026 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2026/02/what-i-touched-this-week-2026-02-07/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="haskell-typeclassopedia-io-and-documentation"&gt;Haskell: Typeclassopedia, IO, and Documentation&lt;/h2&gt;
&lt;p&gt;This week was heavily Haskell-focused. I worked through the &lt;a href="https://wiki.haskell.org/Typeclassopedia"&gt;Typeclassopedia&lt;/a&gt;, &lt;a href="https://learnyouahaskell.github.io/input-and-output.html"&gt;Learn You a Haskell Chapter 9 (Input and Output)&lt;/a&gt;, and &lt;a href="https://book.realworldhaskell.org/read/io.html"&gt;Real World Haskell Chapter 7 (IO)&lt;/a&gt;, plus the &lt;a href="https://haskell-haddock.readthedocs.io/latest/markup.html"&gt;Haddock markup guide&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Monad vs Applicative&lt;/strong&gt;: The structure of an Applicative computation is fixed; a Monad’s structure can depend on intermediate results. That’s why &lt;code&gt;(&amp;gt;&amp;gt;=)&lt;/code&gt; (bind) is strictly more powerful than &lt;code&gt;(&amp;lt;*&amp;gt;)&lt;/code&gt;. Prefer Applicative when you don’t need that—it can allow parallelization where monadic code cannot.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reader, Writer, State&lt;/strong&gt;: The &lt;code&gt;(-&amp;gt;) e&lt;/code&gt; reader monad gives a read-only environment; &lt;code&gt;Writer&lt;/code&gt; accumulates a monoidal log; &lt;code&gt;State s a&lt;/code&gt; is &lt;code&gt;s -&amp;gt; (a,s)&lt;/code&gt; for stateful computations. All live in &lt;code&gt;Control.Monad&lt;/code&gt; and the mtl library.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IO in practice&lt;/strong&gt;: &lt;code&gt;getContents&lt;/code&gt; does lazy I/O; &lt;code&gt;interact&lt;/code&gt; captures the pattern of “read, transform, output.” Use &lt;code&gt;withFile&lt;/code&gt; for handle-based file I/O. Prefer &lt;code&gt;Maybe&lt;/code&gt;/&lt;code&gt;Either&lt;/code&gt; in pure code and reserve exceptions for the IO part; catch with &lt;code&gt;System.IO.Error.catch&lt;/code&gt; and predicates like &lt;code&gt;isDoesNotExistError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Haddock&lt;/strong&gt;: Use &lt;code&gt;-- |&lt;/code&gt; before or &lt;code&gt;-- ^&lt;/code&gt; after declarations; document arguments inline. Headings with &lt;code&gt;-- *&lt;/code&gt;, &lt;code&gt;-- **&lt;/code&gt;; link identifiers with single quotes/backticks and modules with double quotes; code blocks with &lt;code&gt;@...@&lt;/code&gt; or bird tracks &lt;code&gt;&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="use-bun-for-shell-scripts"&gt;Use Bun for Shell Scripts&lt;/h2&gt;
&lt;p&gt;I published a post on &lt;a href="https://blog.iany.me/2026/02/use-bun-for-shell-scripts/"&gt;using Bun for cross-platform shell scripts&lt;/a&gt; on Windows. Bun avoids shebang and WSL/Git-bash setup while giving a single runtime for ad-hoc commands and full scripts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single binary&lt;/strong&gt;: &lt;code&gt;bun build ./script.ts --compile --outfile script&lt;/code&gt; produces a native executable, so no shebang or interpreter is needed on Windows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bun Shell&lt;/strong&gt;: &lt;code&gt;Bun.$&lt;/code&gt; builds shell commands from template literals (similar to zx). On Windows, Bun ships with MSYS for a consistent bash environment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extras&lt;/strong&gt;: &lt;code&gt;Bun.secrets&lt;/code&gt; for the OS keystore, built-in globs, YAML, ANSI. Fast startup (~5ms vs Node’s ~25ms for simple scripts) helps for small, frequent scripts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.iany.me/2026/02/use-bun-for-shell-scripts/"&gt;♯ Use Bun for Shell Scripts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/d/Development&amp;#43;Environment/JavaScript&amp;#43;Shell&amp;#43;Scripting"&gt;JavaScript Shell Scripting&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="how-i-estimate-work"&gt;How I Estimate Work&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.seangoedecke.com/how-i-estimate-work/"&gt;Sean Goedecke’s post&lt;/a&gt; reframes estimation as a political and constraint-satisfaction problem rather than a pure “how long will this take?” question.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Context first&lt;/strong&gt;: Gather political context before looking at code—pressure on the project, what kind of estimate the chain wants, whether this is a casual ask or a must-have.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inversion&lt;/strong&gt;: The job isn’t “break down work to get a duration.” It’s “find the set of software approaches that fit the timeline they already have in mind.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If you don’t estimate&lt;/strong&gt;: Someone less technical will estimate for you. Engaging with the process lets you steer which approaches are in scope.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Readwise&amp;#43;Syncs/Readwise&amp;#43;Sync&amp;#43;2026-02-07"&gt;Readwise Sync 2026-02-07&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Brent&amp;#43;Yorgey&amp;#43;-&amp;#43;Typeclassopedia&amp;#43;%28Highlights%29"&gt;Brent Yorgey - Typeclassopedia (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Miran&amp;#43;Lipova%C4%8Da&amp;#43;-&amp;#43;Learn&amp;#43;You&amp;#43;a&amp;#43;Haskell&amp;#43;for&amp;#43;Great&amp;#43;Good&amp;#43;Chapter&amp;#43;9.&amp;#43;Input&amp;#43;and&amp;#43;Output&amp;#43;%28Highlights%29"&gt;Miran Lipovača - Learn You a Haskell for Great Good Chapter 9. Input and Output (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Bryan&amp;#43;O%27Sullivan&amp;#43;et&amp;#43;al.&amp;#43;-&amp;#43;Real&amp;#43;World&amp;#43;Haskell&amp;#43;Chapter&amp;#43;7.&amp;#43;IO&amp;#43;%28Highlights%29"&gt;Bryan O&amp;rsquo;Sullivan et al. - Real World Haskell Chapter 7. IO (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Haddock&amp;#43;Authors&amp;#43;-&amp;#43;Documentation&amp;#43;and&amp;#43;Markup&amp;#43;%28Highlights%29"&gt;Haddock Authors - Documentation and Markup (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Sean&amp;#43;Goedecke&amp;#43;-&amp;#43;How&amp;#43;I&amp;#43;estimate&amp;#43;work&amp;#43;%28Highlights%29"&gt;Sean Goedecke - How I estimate work (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/d/Development&amp;#43;Environment/JavaScript&amp;#43;Shell&amp;#43;Scripting"&gt;JavaScript Shell Scripting&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>Use Bun for Shell Scripts</title><link>https://blog.iany.me/2026/02/use-bun-for-shell-scripts/</link><pubDate>Sat, 07 Feb 2026 00:00:00 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/02/use-bun-for-shell-scripts/</guid><description>&lt;p&gt;I&amp;rsquo;ve moved most of my small scripts to Bun. It sidesteps Windows&amp;rsquo; lack of shebang support and the Git-for-Windows/WSL bash dance, and gives you one runtime for both ad-hoc shell-style commands and full scripts.&lt;/p&gt;
&lt;h2 id="problems-bun-solves"&gt;Problems Bun solves&lt;/h2&gt;
&lt;p&gt;Windows doesn&amp;rsquo;t execute &lt;code&gt;#!/usr/bin/env python3&lt;/code&gt; or &lt;code&gt;#!/bin/bash&lt;/code&gt;, so scripts written for Linux or macOS usually need a separate PowerShell version. Bun compiles JavaScript to native executables, so a single script can run cross-platform without shebangs.&lt;/p&gt;
&lt;p&gt;Mise &lt;a href="https://mise.jdx.dev/tasks/file-tasks.html"&gt;file tasks&lt;/a&gt; have started to support shebangs on Windows in the latest release, but they rely on system &lt;code&gt;bash.exe&lt;/code&gt;, which often points at WSL. Bun does not. You run &lt;code&gt;bun ./script.js&lt;/code&gt; and embed shell commands via &lt;code&gt;Bun.$&lt;/code&gt;; the script uses Bun&amp;rsquo;s bundled bash environment.&lt;/p&gt;
&lt;h2 id="why-bun-for-shell-scripts"&gt;Why Bun for shell scripts&lt;/h2&gt;
&lt;p&gt;The command &lt;code&gt;bun build ./script.ts --compile --outfile script&lt;/code&gt; &lt;a href="https://bun.com/docs/bundler/executables"&gt;produces a single binary&lt;/a&gt;, so no shebang or interpreter is needed on Windows. The &lt;code&gt;Bun.build&lt;/code&gt; API is available too; I use a &lt;code&gt;build.js&lt;/code&gt; script to walk a directory and compile all executables. Pre-built binaries start quickly—Bun&amp;rsquo;s docs cite ~5ms vs Node&amp;rsquo;s ~25ms for a simple script, which helps for small, frequently run scripts.&lt;/p&gt;
&lt;p&gt;Bun has strong support for shell-style scripting. &lt;a href="https://bun.com/docs/runtime/shell"&gt;Bun Shell&lt;/a&gt; runs bash commands; the main entry point is &lt;code&gt;Bun.$&lt;/code&gt;, which constructs shell commands from template literals for safe interpolation, similar to &lt;a href="https://github.com/google/zx"&gt;google/zx&lt;/a&gt;. On Windows, Bun ships with MSYS, so you get a consistent bash environment and common Linux CLI tools without extra setup.&lt;/p&gt;
&lt;p&gt;Other useful features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Bun.secrets&lt;/code&gt;—Securely store and retrieve secrets using the OS keystore (Windows Credential Manager, macOS Keychain, Linux libsecret). &lt;a href="https://bun.com/docs/api/secrets"&gt;Secrets - Bun&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Built-in support for glob patterns, YAML parsing, ANSI colors, and more. &lt;a href="https://bun.com/docs/runtime"&gt;Bun Runtime&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="short-example"&gt;Short example&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;import { $ } from &amp;quot;bun&amp;quot;;
const out = await $`echo hello`.text();
console.log(out); // hello
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it with &lt;code&gt;bun run script.js&lt;/code&gt; or &lt;code&gt;bun ./script.js&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="obsidian-notes"&gt;Obsidian Notes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/d/Development&amp;#43;Environment/JavaScript&amp;#43;Shell&amp;#43;Scripting"&gt;JavaScript Shell Scripting&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/bun/">Bun</category><category domain="https://blog.iany.me/tags/javascript/">JavaScript</category><category domain="https://blog.iany.me/tags/script/">Script</category><category domain="https://blog.iany.me/tags/windows/">Windows</category><category domain="https://blog.iany.me/tags/dev-environment/">Dev Environment</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2026-02-01</title><link>https://blog.iany.me/journal/2026/02/what-i-touched-this-week-2026-02-01/</link><pubDate>Sun, 01 Feb 2026 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2026/02/what-i-touched-this-week-2026-02-01/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="haskell-type-classes-functors-applicatives-and-monoids"&gt;Haskell Type Classes: Functors, Applicatives, and Monoids&lt;/h2&gt;
&lt;p&gt;This week I dove deep into Haskell&amp;rsquo;s type class hierarchy, working through Learn You a Haskell and the CIS 194 course materials. The key insight is understanding how these abstractions compose to enable powerful, generic programming.- &lt;strong&gt;Functors&lt;/strong&gt; are types that can be mapped over. The &lt;code&gt;fmap&lt;/code&gt; function can be viewed as &amp;ldquo;lifting&amp;rdquo; a function: &lt;code&gt;fmap :: (a -&amp;gt; b) -&amp;gt; (f a -&amp;gt; f b)&lt;/code&gt; transforms an ordinary function into one that works inside a functor context&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;functor laws&lt;/strong&gt; ensure predictable behavior: &lt;code&gt;fmap id = id&lt;/code&gt; (identity preservation) and &lt;code&gt;fmap (f . g) = fmap f . fmap g&lt;/code&gt; (composition preservation)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Applicatives&lt;/strong&gt; extend functors by allowing wrapped functions to be applied to wrapped values using &lt;code&gt;&amp;lt;*&amp;gt;&lt;/code&gt;. Lists as applicatives represent non-deterministic computations where &lt;code&gt;[(+3),(*2)] &amp;lt;*&amp;gt; [1,2]&lt;/code&gt; produces all combinations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ZipList&lt;/strong&gt; provides an alternative applicative instance for lists using pairwise application instead of all combinations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monoids&lt;/strong&gt; provide an identity element (&lt;code&gt;mempty&lt;/code&gt;) and an associative binary operation (&lt;code&gt;mappend&lt;/code&gt;). The &lt;code&gt;foldMap&lt;/code&gt; function reveals why monoids matter: the identity handles empty structures and the associative operation combines values reliably&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;foldl'&lt;/code&gt; from &lt;code&gt;Data.List&lt;/code&gt; instead of &lt;code&gt;foldl&lt;/code&gt; to avoid building huge thunks; &lt;code&gt;foldr&lt;/code&gt; is better when the folding function can short-circuit&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learnyouahaskell.github.io/functors-applicative-functors-and-monoids.html"&gt;Learn You a Haskell: Functors, Applicative Functors and Monoids&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.seas.upenn.edu/~cis1940/spring13/lectures/04-higher-order.html"&gt;CIS 194: Higher-order programming and type inference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wiki.haskell.org/Foldr_Foldl_Foldl%27"&gt;Haskell Wiki: Foldr Foldl Foldl&amp;rsquo;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="using-tmux-for-powershell-in-windows-terminal"&gt;Using tmux for PowerShell in Windows Terminal&lt;/h2&gt;
&lt;p&gt;I published a blog post on getting tmux session persistence in Windows Terminal while keeping PowerShell as the default shell. The solution runs tmux through WSL but configures it to spawn &lt;code&gt;pwsh.exe&lt;/code&gt; for new panes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create thin wrapper scripts (&lt;code&gt;tmux.ps1&lt;/code&gt;, &lt;code&gt;tmux.cmd&lt;/code&gt;) so &lt;code&gt;tmux&lt;/code&gt; from Windows means &lt;code&gt;wsl tmux&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;A helper script &lt;code&gt;tmux-pwsh.ps1&lt;/code&gt; creates or attaches to named sessions with &lt;code&gt;pwsh.exe -nologo&lt;/code&gt; as the default command&lt;/li&gt;
&lt;li&gt;The key trick: send &lt;code&gt;set-option default-command&lt;/code&gt; via &lt;code&gt;tmux -C attach&lt;/code&gt; because running &lt;code&gt;wsl tmux set-option&lt;/code&gt; immediately after session creation has a race condition where the session isn&amp;rsquo;t ready&lt;/li&gt;
&lt;li&gt;After setup, splitting panes and creating windows in that session will all start PowerShell in the Windows environment&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="elite-performance-through-preparation"&gt;Elite Performance Through Preparation&lt;/h2&gt;
&lt;p&gt;An insightful &lt;a href="https://x.com/DrDeepMD/status/2015810417839227390/"&gt;thread by Sandeep Palakodeti&lt;/a&gt; on what separates elite performers from amateurs. The core message: greatness is not about taking risks but eliminating variance through preparation.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When experts operate at their limit, they&amp;rsquo;re not &amp;ldquo;trying hard&amp;rdquo;—they&amp;rsquo;re executing scripts that have been debugged for decades through deliberate, high-fidelity repetition&lt;/li&gt;
&lt;li&gt;Fear is the biological response to the unknown; when you&amp;rsquo;ve accounted for every variable, fear is replaced by focus&lt;/li&gt;
&lt;li&gt;Elite performers aim &lt;em&gt;below&lt;/em&gt; the edge of their ability because that&amp;rsquo;s where consistency lives&lt;/li&gt;
&lt;li&gt;Every question gets answered upstream, every branch point gets a playbook—by the time the moment arrives, they&amp;rsquo;re executing state changes, not solving problems&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Don&amp;rsquo;t worship the risk. Worship the work that made the risk irrelevant.&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/p/Programming/Haskell&amp;#43;Applicative"&gt;Haskell Applicative&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/p/Programming/Haskell&amp;#43;Pattern&amp;#43;Matching"&gt;Haskell Pattern Matching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/p/Programming/Haskell&amp;#43;High-Order&amp;#43;Function&amp;#43;Composition"&gt;Haskell High-Order Function Composition&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Miran&amp;#43;Lipova%C4%8Da&amp;#43;-&amp;#43;Learn&amp;#43;You&amp;#43;a&amp;#43;Haskell&amp;#43;for&amp;#43;Great&amp;#43;Good&amp;#43;Chapter&amp;#43;11.&amp;#43;Functors%2C&amp;#43;Applicative&amp;#43;Functors&amp;#43;and&amp;#43;Monoids&amp;#43;%28Highlights%29"&gt;Miran Lipovača - Learn You a Haskell for Great Good Chapter 11. Functors, Applicative Functors and Monoids (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Miran&amp;#43;Lipova%C4%8Da&amp;#43;-&amp;#43;Learn&amp;#43;You&amp;#43;a&amp;#43;Haskell&amp;#43;for&amp;#43;Great&amp;#43;Good&amp;#43;Chapter&amp;#43;6.&amp;#43;Higher&amp;#43;Order&amp;#43;Functions&amp;#43;%28Highlights%29"&gt;Miran Lipovača - Learn You a Haskell for Great Good Chapter 6. Higher Order Functions (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Haskell&amp;#43;Authors&amp;#43;-&amp;#43;Foldl&amp;#43;%28Highlights%29"&gt;Haskell Authors - Foldl (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Tweets/Sandeep&amp;#43;Palakodeti&amp;#43;-&amp;#43;The&amp;#43;Tippy&amp;#43;Top&amp;#43;%28Highlights%29"&gt;Sandeep Palakodeti - The Tippy Top (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.iany.me/2026/01/use-tmux-for-powershell-in-windows-terminal/"&gt;♯ Use tmux for PowerShell in Windows Terminal&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>Use tmux for PowerShell in Windows Terminal</title><link>https://blog.iany.me/2026/01/use-tmux-for-powershell-in-windows-terminal/</link><pubDate>Thu, 29 Jan 2026 23:30:42 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/01/use-tmux-for-powershell-in-windows-terminal/</guid><description>&lt;p&gt;You can get tmux session persistence and multiplexing in Windows Terminal by running tmux inside WSL and setting the default shell to PowerShell. New panes and windows will then start &lt;code&gt;pwsh.exe&lt;/code&gt; instead of a Linux shell. Here’s a minimal setup using small wrappers and the &lt;code&gt;tmux -C attach&lt;/code&gt; trick to configure new sessions.&lt;/p&gt;
&lt;h2 id="why-tmux-under-wsl-with-powershell"&gt;Why tmux under WSL with PowerShell?&lt;/h2&gt;
&lt;p&gt;Since tmux does not run natively on Windows, the standard approach is to use it through WSL. However, if you launch &lt;code&gt;wsl tmux&lt;/code&gt; from Windows Terminal, all panes will default to your WSL shell. When working in a Windows directory, I prefer to use native PowerShell within the Windows environment.&lt;/p&gt;
&lt;p&gt;To ensure every tmux pane runs PowerShell, configure tmux to use &lt;code&gt;pwsh.exe&lt;/code&gt; as its default command. When you launch &lt;code&gt;pwsh.exe&lt;/code&gt; from WSL within a Windows directory, it brings us back to the Windows environment.&lt;/p&gt;
&lt;h2 id="wrappers-so-you-can-call-tmux-from-powershell"&gt;Wrappers so you can call tmux from PowerShell&lt;/h2&gt;
&lt;p&gt;Put these in a directory on your &lt;code&gt;PATH&lt;/code&gt; (e.g. &lt;code&gt;~/Documents/PowerShell/bin&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PowerShell&lt;/strong&gt; — &lt;code&gt;tmux.ps1&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;wsl tmux $args
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Cmd&lt;/strong&gt; — &lt;code&gt;tmux.cmd&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-batch"&gt;@echo off
wsl tmux %*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From PowerShell or Cmd you can then run &lt;code&gt;tmux new&lt;/code&gt;, &lt;code&gt;tmux attach&lt;/code&gt;, etc., and they all go to WSL’s tmux.&lt;/p&gt;
&lt;h2 id="creating-powershell-sessions"&gt;Creating PowerShell Sessions&lt;/h2&gt;
&lt;p&gt;The next piece is a script that creates a new session whose default command is &lt;code&gt;pwsh.exe&lt;/code&gt;, or attaches if that session already exists. Example usage: &lt;code&gt;tmux-pwsh dev&lt;/code&gt; or &lt;code&gt;tmux-pwsh work&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The script sets the default command only for sessions it creates; it does not affect sessions started with a standard &lt;code&gt;tmux new-session&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;tmux-pwsh.ps1&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;param(
[Parameter(Mandatory=$true)]
[string]$SessionName
)
$pwshCommand = &amp;quot;exec pwsh.exe -nologo&amp;quot;
# Check if session exists using exact name match
wsl tmux has-session -t &amp;quot;\=$SessionName&amp;quot; 2&amp;gt;$null
if ($LASTEXITCODE -ne 0) {
wsl tmux new-session -d -s $SessionName $pwshCommand
echo &amp;quot;set-option default-command `&amp;quot;$pwshCommand`&amp;quot;&amp;quot; | `
wsl tmux -C attach -t &amp;quot;\=$SessionName&amp;quot; &amp;gt;$null
}
wsl tmux attach -t &amp;quot;\=$SessionName&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;=&lt;/code&gt; prefix does an exact session name match and must be escaped when sending the command from PowerShell to WSL.&lt;/li&gt;
&lt;li&gt;The new session specifies command &lt;code&gt;pwsh.exe&lt;/code&gt; via the command argument.&lt;/li&gt;
&lt;li&gt;Running &lt;code&gt;wsl tmux set-option&lt;/code&gt; immediately after creating the session does not work because there&amp;rsquo;s a brief window where the session is not available for setting options. Instead, sending &lt;code&gt;set-option&lt;/code&gt; via &lt;code&gt;tmux -C attach&lt;/code&gt; reliably applies the setting.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After this, splitting panes and creating new windows in that session will all start &lt;code&gt;pwsh.exe -nologo&lt;/code&gt; in Windows Terminal.&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Use WSL for tmux and PowerShell as the default command by setting tmux’s default command to &lt;code&gt;exec pwsh.exe -nologo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tmux.ps1&lt;/code&gt;&lt;/strong&gt; / &lt;strong&gt;&lt;code&gt;tmux.cmd&lt;/code&gt;&lt;/strong&gt; — thin wrappers so &lt;code&gt;tmux&lt;/code&gt; from Windows means &lt;code&gt;wsl tmux&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tmux-pwsh.ps1 &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/strong&gt; — create (or attach to) a named session that uses PowerShell as the default command.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once these are on your &lt;code&gt;PATH&lt;/code&gt;, run e.g. &lt;code&gt;tmux-pwsh dev&lt;/code&gt; from Windows Terminal to get a tmux session where every pane is PowerShell.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/tmux/">Tmux</category><category domain="https://blog.iany.me/tags/powershell/">PowerShell</category><category domain="https://blog.iany.me/tags/wsl/">WSL</category><category domain="https://blog.iany.me/tags/windows-terminal/">Windows Terminal</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2026-01-24</title><link>https://blog.iany.me/journal/2026/01/what-i-touched-this-week-2026-01-24/</link><pubDate>Sat, 24 Jan 2026 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2026/01/what-i-touched-this-week-2026-01-24/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="erlang-state-machine-patterns-in-rust"&gt;Erlang State Machine Patterns in Rust&lt;/h2&gt;
&lt;p&gt;This week I created a presentation exploring how to implement Erlang OTP&amp;rsquo;s &lt;code&gt;gen_statem&lt;/code&gt; patterns in Rust using Tokio. The core insight is that Erlang&amp;rsquo;s behavior pattern separates the &lt;strong&gt;engine&lt;/strong&gt; (state machine framework) from the &lt;strong&gt;callback&lt;/strong&gt; (state-specific logic), following the strategy pattern.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The fundamental state transition follows: &lt;code&gt;State(S) × Event(E) → Actions(A), State(S')&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Erlang&amp;rsquo;s &amp;ldquo;everything is an event&amp;rdquo; philosophy means state entry, timeouts, and internal messages are all unified as events&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;enter event&lt;/strong&gt; triggers when entering a new state, useful for running initialization logic without duplicating code across multiple transition paths&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Postpone action&lt;/strong&gt; defers events to a later state when we can&amp;rsquo;t process them yet&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timeout actions&lt;/strong&gt; come in three flavors: event timeout (cleared on any event), state timeout (cleared on state change), and named timeout (manual clearing)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inserted events&lt;/strong&gt; enable preprocessing and state machine composition&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;a href="https://github.com/doitian/from-statem-to-tokio"&gt;code examples&lt;/a&gt; progressively build from basic process simulation to full-featured state machines with all these capabilities.&lt;/p&gt;
&lt;h2 id="bft-consensus-protocol-comparisons"&gt;BFT Consensus Protocol Comparisons&lt;/h2&gt;
&lt;p&gt;I studied the differences between PBFT, Tendermint, HotStuff, and HotStuff-2. These are all &lt;strong&gt;safety-preferring&lt;/strong&gt; protocols where locked parties guard the safety of a &lt;em&gt;potential&lt;/em&gt; commit. The key questions each protocol answers are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What does the leader learn about system status at the start of a view?&lt;/li&gt;
&lt;li&gt;How does the leader convince other parties about system status to get votes?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Among 2t+1 locks in a status response, up to t can be malicious, up to t can be honest but not locked on the committed value, but at least one honest party must be locked on the committed value. This honest party won&amp;rsquo;t vote for conflicting proposals, thus guarding commit safety.&lt;/p&gt;
&lt;p&gt;HotStuff introduces an additional phase: &amp;ldquo;if some party locks, then ≥2t+1 parties know about this lock and hold a &lt;em&gt;key&lt;/em&gt; corresponding to it.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://decentralizedthoughts.github.io/2023-04-01-hotstuff-2/"&gt;What is the difference between PBFT, Tendermint, HotStuff, and HotStuff-2&lt;/a&gt; by Kartik Nayak and Dahlia Malkhi&lt;/li&gt;
&lt;li&gt;&lt;a href="https://decentralizedthoughts.github.io/2022-09-10-provable-broadcast/"&gt;Provable Broadcast&lt;/a&gt; by Ittai Abraham and Alexander Spiegelman&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="ckb-virtual-transactions"&gt;CKB Virtual Transactions&lt;/h2&gt;
&lt;p&gt;I explored a design for implementing partial transactions in CKB through &amp;ldquo;Virtual Tx.&amp;rdquo; The approach mocks system calls so scripts access the virtual tx instead of the real current transaction.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Override syscalls in ckb-c-stdlib or ckb-std to redirect to virtual tx data&lt;/li&gt;
&lt;li&gt;Anyone can be economically incentivized to match partial txs since fees can be provided&lt;/li&gt;
&lt;li&gt;Use a special first input as the indicator that the tx is a vtx&lt;/li&gt;
&lt;li&gt;The first input&amp;rsquo;s type script witness specifies how each vtx maps to tx components (inputs, outputs, etc.)&lt;/li&gt;
&lt;li&gt;Inputs/outputs are handled exclusively, with mechanisms to tackle cell/header deps duplication&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/p/Programming/Gen&amp;#43;Statem&amp;#43;to&amp;#43;Tokio&amp;#43;-&amp;#43;Slides"&gt;Gen Statem to Tokio - Slides&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/p/Programming/Gen&amp;#43;Statem&amp;#43;to&amp;#43;Tokio&amp;#43;Learn&amp;#43;State&amp;#43;Machine&amp;#43;Pattern&amp;#43;from&amp;#43;Erlang"&gt;Gen Statem to Tokio Learn State Machine Pattern from Erlang&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/p/Programming/Erlang&amp;#43;gen_statem"&gt;Erlang gen_statem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Kartik&amp;#43;Nayak&amp;#43;et&amp;#43;al.&amp;#43;-&amp;#43;What&amp;#43;is&amp;#43;the&amp;#43;difference&amp;#43;between&amp;#43;PBFT%2C&amp;#43;Tendermint%2C&amp;#43;HotStuff%2C&amp;#43;and&amp;#43;HotStuff-2&amp;#43;%28Highlights%29"&gt;Kartik Nayak et al. - What is the difference between PBFT, Tendermint, HotStuff, and HotStuff-2 (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Ittai&amp;#43;Abraham&amp;#43;et&amp;#43;al.&amp;#43;-&amp;#43;Provable&amp;#43;Broadcast&amp;#43;%28Highlights%29"&gt;Ittai Abraham et al. - Provable Broadcast (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/c/CKB/CKB&amp;#43;Virtual&amp;#43;Tx"&gt;CKB Virtual Tx&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2026-01-17</title><link>https://blog.iany.me/journal/2026/01/what-i-touched-this-week-2026-01-17/</link><pubDate>Sat, 17 Jan 2026 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2026/01/what-i-touched-this-week-2026-01-17/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="temporal-patterns-and-bitemporality"&gt;Temporal Patterns and Bitemporality&lt;/h2&gt;
&lt;p&gt;This week I explored the concept of bitemporality and temporal patterns in data modeling. The key insight is that we need to track two dimensions of time: &lt;strong&gt;actual time&lt;/strong&gt; (when something happened) and &lt;strong&gt;record time&lt;/strong&gt; (when we learned about it). This becomes crucial when we need to answer questions like &amp;ldquo;what did we think the state was two months ago?&amp;rdquo; or &amp;ldquo;what did we know six months ago about something that happened earlier?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tigerbeetle.com/blog/2026-01-14-bitemporality/"&gt;One for the Treble, Two for the Time&lt;/a&gt; by Lewis Daly&lt;/li&gt;
&lt;li&gt;&lt;a href="https://martinfowler.com/eaaDev/timeNarrative.html"&gt;Temporal Patterns&lt;/a&gt; by Martin Fowler&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="go-performance-optimization-techniques"&gt;Go Performance Optimization Techniques&lt;/h2&gt;
&lt;p&gt;I learned several advanced Go performance optimization techniques from VictoriaMetrics, a high-performance time-series database. The key strategies include using &lt;code&gt;sync.Pool&lt;/code&gt; for object reuse to reduce GC pressure, implementing channel-based object pools for more controlled resource management, and using sharding to reduce lock contention by splitting large data structures into independent shards.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;sync.Pool&lt;/code&gt; for object reuse to minimize garbage collection overhead&lt;/li&gt;
&lt;li&gt;Channel-based object pools provide more control than &lt;code&gt;sync.Pool&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Sharding large data structures reduces lock contention&lt;/li&gt;
&lt;li&gt;Localized worker pools with independent channels improve multi-core scalability&lt;/li&gt;
&lt;li&gt;Buffered channels can serve as semaphores for rate limiting&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Source: &lt;a href="https://mp.weixin.qq.com/s/1svEokrz5C0FwBEA88YGqw"&gt;从入门到极致：VictoriaMetrics 教你写出最高效的 Go 代码&lt;/a&gt; by Tony Bai&lt;/p&gt;
&lt;h2 id="jdk-selection-guidelines"&gt;JDK Selection Guidelines&lt;/h2&gt;
&lt;p&gt;I reviewed best practices for choosing a Java Development Kit (JDK) version and distribution. The primary recommendation is to use &lt;strong&gt;Adoptium Eclipse Temurin 21&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Source: &lt;a href="https://whichjdk.com/"&gt;Which Version of JDK Should I Use&lt;/a&gt; by Jochen Christ&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Lewis&amp;#43;Daly&amp;#43;-&amp;#43;One&amp;#43;for&amp;#43;the&amp;#43;Treble%2C&amp;#43;Two&amp;#43;for&amp;#43;the&amp;#43;Time&amp;#43;%28Highlights%29"&gt;Lewis Daly - One for the Treble, Two for the Time (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Martin&amp;#43;Fowler&amp;#43;-&amp;#43;Temporal&amp;#43;Patterns&amp;#43;%28Highlights%29"&gt;Martin Fowler - Temporal Patterns (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Tony&amp;#43;Bai&amp;#43;-&amp;#43;%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E6%9E%81%E8%87%B4%EF%BC%9AVictoriaMetrics&amp;#43;%E6%95%99%E4%BD%A0%E5%86%99%E5%87%BA%E6%9C%80%E9%AB%98%E6%95%88%E7%9A%84&amp;#43;Go&amp;#43;%E4%BB%A3%E7%A0%81&amp;#43;%28Highlights%29"&gt;Tony Bai - 从入门到极致：VictoriaMetrics 教你写出最高效的 Go 代码 (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Jochen&amp;#43;Christ&amp;#43;-&amp;#43;Which&amp;#43;Version&amp;#43;of&amp;#43;JDK&amp;#43;Should&amp;#43;I&amp;#43;Use&amp;#43;%28Highlights%29"&gt;Jochen Christ - Which Version of JDK Should I Use (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2026-01-10</title><link>https://blog.iany.me/journal/2026/01/what-i-touched-this-week-2026-01-10/</link><pubDate>Sat, 10 Jan 2026 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2026/01/what-i-touched-this-week-2026-01-10/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="learning-c23-going-behind-the-curtain-of-modern-systems"&gt;Learning C23: Going Behind the Curtain of Modern Systems&lt;/h2&gt;
&lt;p&gt;This week I dove deep into &lt;a href="https://www.amazon.com/Why-Learn-Behind-Curtain-Systems-ebook/dp/B0FVKPKX6Y"&gt;Paul J. Lucas&amp;rsquo;s book on learning C23&lt;/a&gt;, which provides a comprehensive tour of the latest C standard and why understanding systems programming at this level matters. With 133 highlights, this was clearly a major focus of my learning.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Modern C Features&lt;/strong&gt;: C23 introduces many modern conveniences like &lt;code&gt;bool&lt;/code&gt;, &lt;code&gt;nullptr&lt;/code&gt;, &lt;code&gt;constexpr&lt;/code&gt;, &lt;code&gt;auto&lt;/code&gt;, &lt;code&gt;typeof&lt;/code&gt;, and compound literals. The language is evolving while maintaining its core philosophy of being &amp;ldquo;close to the machine.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;East const Convention&lt;/strong&gt;: A stylistic choice where &lt;code&gt;const&lt;/code&gt; is placed to the right (east) of the type, making pointer declarations more readable when reading from right to left. This consistency helps with complex pointer declarations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type System Enhancements&lt;/strong&gt;: New types like &lt;code&gt;_BitInt&lt;/code&gt; for bit-precise integers, decimal floating types (&lt;code&gt;_Decimal32&lt;/code&gt;, &lt;code&gt;_Decimal64&lt;/code&gt;, &lt;code&gt;_Decimal128&lt;/code&gt;), and complex number support show C&amp;rsquo;s continued evolution for specialized use cases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Practical Systems Understanding&lt;/strong&gt;: The book emphasizes that C was designed to work directly with machine details—understanding memory layout, alignment, and how data structures map to hardware is still valuable even when working in higher-level languages.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This connects to my broader interest in understanding how modern systems work under the hood, especially as I work with distributed systems and blockchain technology where performance and correctness matter deeply.&lt;/p&gt;
&lt;h2 id="time-management-and-reflective-living"&gt;Time Management and Reflective Living&lt;/h2&gt;
&lt;p&gt;Arnold Bennett&amp;rsquo;s classic &lt;a href="https://www.gutenberg.org/files/2274/2274-h/2274-h.htm"&gt;How to Live on 24 Hours a Day&lt;/a&gt; offers timeless wisdom about making the most of our daily time budget. The key insight is that we already have all the time there is—the challenge is how we use it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Inner Day&lt;/strong&gt;: Bennett suggests dedicating 7.5 hours per week (about 90 minutes per evening) to cultivating your mind. This &amp;ldquo;inner day&amp;rdquo; should begin at 6 p.m. and end at 10 a.m., separate from your workday.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Reflective Mood&lt;/strong&gt;: What&amp;rsquo;s most lacking in modern life is the reflective mood—taking time to examine conduct against principles, to think deeply rather than just consume information. The solitude of the evening commute is perfect for this.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slow, Deep Reading&lt;/strong&gt;: When reading for cultivation, give at least 45 minutes to careful, fatiguing reflection on what you&amp;rsquo;re reading. Your pace will be slow, but that&amp;rsquo;s the point—the good stuff rises up to meet you in the extra time you give it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Topic Reading with Limits&lt;/strong&gt;: Choose a limited period, subject, or single author. Define the direction and scope of your efforts rather than reading aimlessly. Forget the goal; think only of the surrounding country, and you&amp;rsquo;ll find yourself in a lovely town on a hill.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This resonates with my experience that slowing down often yields better results than rushing, whether in reading, coding, or thinking through problems.&lt;/p&gt;
&lt;h2 id="formal-methods-exploring-quint-and-tla"&gt;Formal Methods: Exploring Quint and TLA+&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been exploring formal specification languages, particularly &lt;a href="https://quint-lang.org/docs/language-basics"&gt;Quint&lt;/a&gt;, which provides a more accessible syntax for writing formal specifications compared to TLA+. This connects to my interest in distributed systems and ensuring correctness.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Quint&amp;rsquo;s Design Philosophy&lt;/strong&gt;: Quint restricts the prime operator (&lt;code&gt;'&lt;/code&gt;) to assignments in the format &lt;code&gt;&amp;lt;var&amp;gt;' = &amp;lt;expr&amp;gt;&lt;/code&gt;, preventing hard-to-grasp expressions while maintaining the power of temporal logic. The tooling reports errors if you misuse &lt;code&gt;'&lt;/code&gt; or forget to update variables in some branches.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State Machine Modeling&lt;/strong&gt;: Actions named &lt;code&gt;init&lt;/code&gt; and &lt;code&gt;step&lt;/code&gt; define the state machine—&lt;code&gt;init&lt;/code&gt; defines initial states, &lt;code&gt;step&lt;/code&gt; defines transitions. This makes the model structure clear and checkable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pure Functions and Actions&lt;/strong&gt;: Definitions prefixed with &lt;code&gt;pure&lt;/code&gt; are functional—same input always produces same output. Actions use the prime operator to modify state. This separation helps reason about what changes state and what doesn&amp;rsquo;t.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-determinism&lt;/strong&gt;: Quint supports non-deterministic definitions using &lt;code&gt;any&lt;/code&gt; and &lt;code&gt;oneOf&lt;/code&gt;, which can only be used inside actions before assignments. This is crucial for modeling systems with multiple possible behaviors.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The connection to &lt;a href="https://learntla.com/"&gt;TLA+&lt;/a&gt; is clear—both are tools for specifying and verifying system behavior, but Quint&amp;rsquo;s more familiar syntax might lower the barrier to entry for formal methods. This is particularly relevant as I work on systems where correctness is critical.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Books/Paul&amp;#43;J.&amp;#43;Lucas&amp;#43;-&amp;#43;Why&amp;#43;Learn&amp;#43;C&amp;#43;Go&amp;#43;Behind&amp;#43;the&amp;#43;Curtain&amp;#43;of&amp;#43;Modern&amp;#43;Systems&amp;#43;With&amp;#43;C23&amp;#43;%28Highlights%29"&gt;Paul J. Lucas - Why Learn C Go Behind the Curtain of Modern Systems With C23 (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Books/Arnold&amp;#43;Bennett&amp;#43;-&amp;#43;How&amp;#43;to&amp;#43;Live&amp;#43;on&amp;#43;24&amp;#43;Hours&amp;#43;a&amp;#43;Day&amp;#43;%28Highlights%29"&gt;Arnold Bennett - How to Live on 24 Hours a Day (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Quint&amp;#43;Authors&amp;#43;-&amp;#43;Language&amp;#43;Basics&amp;#43;%28Highlights%29"&gt;Quint Authors - Language Basics (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Quint&amp;#43;Authors&amp;#43;-&amp;#43;Lesson&amp;#43;0&amp;#43;-&amp;#43;Hello%2C&amp;#43;world&amp;#43;%28Highlights%29"&gt;Quint Authors - Lesson 0 - Hello, world (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/f/Formal&amp;#43;Method/Quint"&gt;Quint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/f/Formal&amp;#43;Method/TLA%2B"&gt;TLA+&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2026-01-03</title><link>https://blog.iany.me/journal/2026/01/what-i-touched-this-week-2026-01-03/</link><pubDate>Sat, 03 Jan 2026 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2026/01/what-i-touched-this-week-2026-01-03/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="training-the-idea-muscle-quantity-over-quality"&gt;Training the Idea Muscle: Quantity Over Quality&lt;/h2&gt;
&lt;p&gt;This week I came across a fascinating article about &lt;a href="https://sfalexandria.com/posts/rileys-ideas/"&gt;training the idea muscle&lt;/a&gt; that perfectly captures a principle I&amp;rsquo;ve been trying to internalize: if you reject your own ideas, the part of the brain that generates them will simply stop producing.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Never Reject During Generation&lt;/strong&gt;: The key insight is to generate ideas continuously without filtering them in the moment. You sort them out later. This is similar to the &lt;a href="https://kb.iany.me/para/lets/c/Creativity/Creativity&amp;#43;Faucet"&gt;Creativity Faucet&lt;/a&gt; concept—you need to let the bad ideas flow out first before the good ones emerge.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do It While Learning&lt;/strong&gt;: &amp;ldquo;I don&amp;rsquo;t think I&amp;rsquo;ve ever done anything I&amp;rsquo;m not good enough to do because I get good at the thing while doing it.&amp;rdquo; This reframes the common fear of not being ready—you become ready by starting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fun as the Filter&lt;/strong&gt;: Only work on things you think are fun. This is the real filter, not whether you&amp;rsquo;re &amp;ldquo;good enough&amp;rdquo; or whether the idea is &amp;ldquo;good enough&amp;rdquo; initially.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ideas Require Action&lt;/strong&gt;: Ideas are everywhere, but most people don&amp;rsquo;t act on them. The muscle being trained isn&amp;rsquo;t just idea generation—it&amp;rsquo;s the habit of execution.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This connects beautifully with &lt;a href="https://www.goodreads.com/book/show/59991434-the-motern-method"&gt;The Motern Method&lt;/a&gt;, a book about an artist who released 23,000+ songs by embracing quantity and rejecting perfectionism.&lt;/p&gt;
&lt;h2 id="slowing-down-to-go-deeper"&gt;Slowing Down to Go Deeper&lt;/h2&gt;
&lt;p&gt;David Cain&amp;rsquo;s article on &lt;a href="https://www.raptitude.com/2025/12/maybe-the-default-settings-are-too-high/"&gt;default settings being too high&lt;/a&gt; offers a counterintuitive productivity insight: slowing down often yields better results than rushing.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reading Aloud&lt;/strong&gt;: Limiting yourself to mouth-speed rather than eye-speed prevents rushing through text, missing important details, and losing interest. If you&amp;rsquo;re enjoying something, why would you want to be done with it sooner?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Vacuum Analogy&lt;/strong&gt;: Passing a vacuum too quickly over carpet means you miss half the dirt. Slow down, and you can hear how much more grit gets picked up. The same applies to reading and eating—the good stuff (meaning in text, pleasure in food) rises up to meet you in the extra time you give it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quality Over Speed&lt;/strong&gt;: This challenges the default assumption that faster is better. Sometimes our cultural &amp;ldquo;default settings&amp;rdquo; for speed are calibrated too high for actual comprehension and enjoyment.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This resonates with my experience using AI tools—the temptation is to go faster, but often the value comes from slowing down and engaging more deeply with the material.&lt;/p&gt;
&lt;h2 id="learning-tacit-knowledge-copying-better"&gt;Learning Tacit Knowledge: Copying Better&lt;/h2&gt;
&lt;p&gt;Cedric Chin&amp;rsquo;s piece on &lt;a href="https://commoncog.com/blog/how-to-learn-tacit-knowledge/"&gt;acquiring tacit knowledge from experts&lt;/a&gt; provides a practical framework for learning skills that can&amp;rsquo;t be easily articulated.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Four Key Elements&lt;/strong&gt;: When observing experts, focus on their &lt;strong&gt;cues&lt;/strong&gt; (what they notice first), &lt;strong&gt;expectancies&lt;/strong&gt; (what they expect to happen next), &lt;strong&gt;goals&lt;/strong&gt; (their priorities), and &lt;strong&gt;actions&lt;/strong&gt; (courses of action that immediately come to mind).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prototypes vs. Options&lt;/strong&gt;: Experts rarely compare multiple options—they pick the first available option that fits their criteria because they&amp;rsquo;ve built up mental prototypes. If you find yourself doing option comparison in a situation where a colleague doesn&amp;rsquo;t, that&amp;rsquo;s a signal they have a prototype you lack.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Narrative Construction&lt;/strong&gt;: Experts construct narratives or simulations to explain what they&amp;rsquo;re seeing, matching patterns against different prototypes. This is fundamentally different from following explicit rules.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This framework is particularly relevant for software engineering, where so much expertise is tacit—knowing when to refactor, which abstractions to choose, how to structure systems. These aren&amp;rsquo;t things you can learn from documentation alone.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Aadil&amp;#43;Pickle&amp;#43;-&amp;#43;Training&amp;#43;the&amp;#43;Idea&amp;#43;Muscle&amp;#43;%28Highlights%29"&gt;Aadil Pickle - Training the Idea Muscle (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/David&amp;#43;Cain&amp;#43;-&amp;#43;Maybe&amp;#43;the&amp;#43;Default&amp;#43;Settings&amp;#43;Are&amp;#43;Too&amp;#43;High&amp;#43;%28Highlights%29"&gt;David Cain - Maybe the Default Settings Are Too High (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Cedric&amp;#43;Chin&amp;#43;-&amp;#43;Copying&amp;#43;Better&amp;#43;How&amp;#43;to&amp;#43;Acquire&amp;#43;the&amp;#43;Tacit&amp;#43;Knowledge&amp;#43;of&amp;#43;Experts&amp;#43;%28Highlights%29"&gt;Cedric Chin - Copying Better How to Acquire the Tacit Knowledge of Experts (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Sean&amp;#43;Goedecke&amp;#43;-&amp;#43;Software&amp;#43;Engineers&amp;#43;Should&amp;#43;Be&amp;#43;a&amp;#43;Little&amp;#43;Bit&amp;#43;Cynical&amp;#43;%28Highlights%29"&gt;Sean Goedecke - Software Engineers Should Be a Little Bit Cynical (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2025-12-27</title><link>https://blog.iany.me/journal/2025/12/what-i-touched-this-week-2025-12-27/</link><pubDate>Sat, 27 Dec 2025 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2025/12/what-i-touched-this-week-2025-12-27/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="state-machines-for-simplifying-complex-logic"&gt;State Machines for Simplifying Complex Logic&lt;/h2&gt;
&lt;p&gt;This week I dove into using state machines as a tool for managing complex application logic. Dave Thomas&amp;rsquo;s approach in his book &amp;ldquo;Simplicity&amp;rdquo; resonated with me: state machines aren&amp;rsquo;t as complicated as people make them out to be, and they&amp;rsquo;re a great way of turning deeply nested code into nice linear chunks.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data Over Code&lt;/strong&gt;: The core insight is that data is easier to work with than code—it&amp;rsquo;s easier to reason about, manipulate, and change. You don&amp;rsquo;t need complex libraries or long-winded pattern-based approaches when you need a state machine; all you need is a lookup table.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State Transition Formula&lt;/strong&gt;: The fundamental model is &lt;code&gt;State(S) x Event(E) -&amp;gt; Actions(A), State(S')&lt;/code&gt;. This simple formula captures the essence of event-driven behavior in a declarative way.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Event Generation&lt;/strong&gt;: Actions can return either new state values or generate new events. If an event is returned, the code immediately performs another transition—this allows for elegant chaining of state changes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State Enter Calls&lt;/strong&gt;: Erlang&amp;rsquo;s &lt;code&gt;gen_statem&lt;/code&gt; has a powerful feature where the state callback is automatically called with special arguments whenever the state changes, making it easy to perform entry actions without cluttering transition logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Postponing Events&lt;/strong&gt;: A clever pattern is the ability to postpone events—if an event arrives but shouldn&amp;rsquo;t be handled in the current state, you can defer it until a state change occurs when the event queue restarts from the oldest postponed event.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://articles.pragdave.me/p/simplify-logic-with-state-machines"&gt;Simplify Logic With State Machines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.erlang.org/doc/system/statem"&gt;gen_statem Behaviour&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="tokio-and-async-rust-patterns"&gt;Tokio and Async Rust Patterns&lt;/h2&gt;
&lt;p&gt;Continuing my study of async Rust, I worked through the Tokio tutorials and explored patterns for shared state and concurrent data structures.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mutex Selection&lt;/strong&gt;: As a rule of thumb, using a synchronous mutex from within asynchronous code is fine as long as contention remains low and the lock is not held across calls to &lt;code&gt;.await&lt;/code&gt;. The Tokio mutex&amp;rsquo;s primary feature is that it can be held across an &lt;code&gt;.await&lt;/code&gt; without issues, but if contention becomes a problem, the best fix is rarely to switch to the Tokio mutex.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Concurrent Hash Maps&lt;/strong&gt;: For high-contention scenarios, there are several excellent crate options: &lt;a href="https://docs.rs/dashmap"&gt;dashmap&lt;/a&gt; provides a sharded hash map implementation, while &lt;a href="https://docs.rs/leapfrog"&gt;leapfrog&lt;/a&gt; uses leapfrog probing, and &lt;a href="https://docs.rs/flurry"&gt;flurry&lt;/a&gt; is a port of Java&amp;rsquo;s &lt;code&gt;ConcurrentHashMap&lt;/code&gt;. I also discovered &lt;a href="https://docs.rs/papaya"&gt;papaya&lt;/a&gt;, which is optimized for read-heavy workloads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Async Trait Variants&lt;/strong&gt;: The &lt;a href="https://docs.rs/trait-variant"&gt;trait_variant&lt;/a&gt; and &lt;a href="https://docs.rs/dynosaur"&gt;dynosaur&lt;/a&gt; crates help with async trait patterns—useful for creating both sync and async versions of traits without code duplication.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing with tokio-test&lt;/strong&gt;: The &lt;a href="https://docs.rs/tokio-test/latest/tokio_test/io/struct.Builder.html"&gt;tokio_test::io::Builder&lt;/a&gt; provides mock I/O for testing async code, making it easier to write deterministic tests for network protocols.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://tokio.rs/tokio/tutorial/shared-state"&gt;Tokio Tutorial - Shared State&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="clean-code-maintenance-as-the-core-of-software"&gt;Clean Code: Maintenance as the Core of Software&lt;/h2&gt;
&lt;p&gt;I revisited Robert C. Martin&amp;rsquo;s &amp;ldquo;Clean Code&amp;rdquo; this week, and the framing around maintenance struck me differently in the AI era.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;80% is Maintenance&lt;/strong&gt;: In software, 80% or more of what we do is &amp;ldquo;maintenance&amp;rdquo;—the act of repair. Rather than the Western focus on producing good software, we should think more like home repairmen or auto mechanics: proactive maintenance rather than waiting for bugs to surface.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A Million Selfless Acts&lt;/strong&gt;: Quality is the result of a million selfless acts of care—not just of any great method that descends from the heavens. These acts are simple but not simplistic, forming the fabric of greatness in any human endeavor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reading vs Writing Ratio&lt;/strong&gt;: The ratio of time spent reading vs. writing code is well over 10:1. We are constantly reading old code as part of the effort to write new code, so making code easy to read actually makes it easier to write.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LeBlanc&amp;rsquo;s Law&lt;/strong&gt;: &amp;ldquo;Later equals never.&amp;rdquo; We&amp;rsquo;ve all felt the relief of seeing our messy program work and deciding that a working mess is better than nothing, promising to clean it up later. This resonates especially with vibe-coded AI-generated solutions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Boy Scout Rule&lt;/strong&gt;: Leave the campground cleaner than you found it. Small continuous improvements compound over time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clean Code Characteristics&lt;/strong&gt;: Clean code reads like well-written prose, never obscures the designer&amp;rsquo;s intent, and is full of crisp abstractions and straightforward lines of control. It should make you smile the way a well-crafted music box or well-designed car would.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt;: Clean Code by Robert C. Martin&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Dave&amp;#43;Thomas&amp;#43;-&amp;#43;Simplify&amp;#43;Logic&amp;#43;With&amp;#43;State&amp;#43;Machines&amp;#43;%28Highlights%29"&gt;Dave Thomas - Simplify Logic With State Machines (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Erlang&amp;#43;Authors&amp;#43;-&amp;#43;gen_statem&amp;#43;Behaviour&amp;#43;%28Highlights%29"&gt;Erlang Authors - gen_statem Behaviour (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Books/Dave&amp;#43;Thomas&amp;#43;-&amp;#43;Simplicity&amp;#43;Sustainable%2C&amp;#43;Humane%2C&amp;#43;and&amp;#43;Effective&amp;#43;Software&amp;#43;Development&amp;#43;%28Highlights%29"&gt;Dave Thomas - Simplicity Sustainable, Humane, and Effective Software Development (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Tokio&amp;#43;Authors&amp;#43;-&amp;#43;Shared&amp;#43;state&amp;#43;%28Highlights%29"&gt;Tokio Authors - Shared state (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Books/Robert&amp;#43;C.&amp;#43;Martin&amp;#43;-&amp;#43;Clean&amp;#43;Code&amp;#43;%28Highlights%29"&gt;Robert C. Martin - Clean Code (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2025-12-20</title><link>https://blog.iany.me/journal/2025/12/what-i-touched-this-week-2025-12-20/</link><pubDate>Sat, 20 Dec 2025 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2025/12/what-i-touched-this-week-2025-12-20/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="the-ladder-of-abstraction-in-system-design"&gt;The Ladder of Abstraction in System Design&lt;/h2&gt;
&lt;p&gt;Bret Victor&amp;rsquo;s &amp;ldquo;The Ladder of Abstraction&amp;rdquo; explores how to design interactive systems by moving between levels of abstraction. The core insight is that the most powerful way to gain insight into a system is by stepping up and down the ladder—abstracting over variables to see high-level patterns, then stepping down to concrete representations to understand the details.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Abstracting Over Time&lt;/strong&gt;: Instead of watching a system evolve in real-time (which is limiting, like a film editor who can&amp;rsquo;t pause or rewind), we can abstract over time by turning time into a parameter. This creates a trajectory—a function that returns a point for any given time, showing the system&amp;rsquo;s behavior across all time values.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stepping Up and Down&lt;/strong&gt;: We step up to an abstraction by devising a representation that depicts the system across all values of a parameter. We step down by interactively selecting one particular value, allowing us to explore regions of interest quickly. This corresponds to partial application in functional programming.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visual vs. Symbolic Abstractions&lt;/strong&gt;: The human brain excels at pattern matching with visual patterns but struggles with logical inferences. Visual abstractions leverage our natural strengths, making it easier to see high-level patterns and tune out low-level clutter.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metrics and Patterns&lt;/strong&gt;: By choosing metrics—numbers that summarize system behavior in interesting ways—we can step up another level of abstraction. For example, plotting time to completion versus turning rate reveals patterns that aren&amp;rsquo;t visible in individual trajectories.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Starting Simple&lt;/strong&gt;: Beginning with the simplest possible algorithm and thoroughly exploring it helps us understand why and how it&amp;rsquo;s terrible, providing the foundation for better solutions. The tools we build for simple cases continue to serve us throughout the design process.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://worrydream.com/LadderOfAbstraction/"&gt;The Ladder of Abstraction&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="ai-investment-bubble-and-infrastructure-economics"&gt;AI Investment Bubble and Infrastructure Economics&lt;/h2&gt;
&lt;p&gt;Howard Marks examines whether AI is experiencing a financial bubble, distinguishing between technological hype and actual market bubbles. His analysis reveals the complex economics of AI infrastructure investment and the risks of debt-financed expansion.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bubbles vs. Technological Progress&lt;/strong&gt;: Market bubbles aren&amp;rsquo;t caused directly by technological developments but by excessive optimism applied to those developments. However, Marks notes that &amp;ldquo;inflection bubbles&amp;rdquo; based on revolutionary developments can accelerate technological progress while still destroying wealth—they&amp;rsquo;re both collective delusions and expressions of collective vision.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Compression Effect&lt;/strong&gt;: If people remained patient and value-insistent, novel technologies would take decades to build out. The hysteria of bubbles compresses this process into a short period, with some money going to life-changing investments in winners but a lot being incinerated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Circular Capital Flows&lt;/strong&gt;: OpenAI receives billions from tech companies but also sends billions back to pay for computing power. Nvidia&amp;rsquo;s $100 billion investment in OpenAI flows back as the startup buys or leases Nvidia&amp;rsquo;s chips, creating circular capital flows that raise questions about actual value creation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debt-Financed Expansion&lt;/strong&gt;: Major tech companies (Oracle, Meta, Alphabet) have issued 30-year bonds to finance AI investments. With close to half a trillion in expected spending next year but only $350bn collectively in the bank, heavy debt usage creates risks of overcapacity and depressed returns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Scale of Trillions&lt;/strong&gt;: A trillion dollars is a dollar a second for 31,700 years—a scale that&amp;rsquo;s difficult to comprehend but represents the magnitude of AI infrastructure investment being discussed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://readwise-assets.s3.amazonaws.com/media/wisereads/articles/is-it-a-bubble/1056.pdf"&gt;Is It A Bubble&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="becoming-ai-native-in-daily-workflows"&gt;Becoming AI-Native in Daily Workflows&lt;/h2&gt;
&lt;p&gt;Jeff Su&amp;rsquo;s video provides practical strategies for integrating AI into daily work, moving beyond simple prompting to building sustainable AI-native workflows that anchor conversations, leverage swipe files, and strategically allocate tasks between AI and manual work.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Anchoring Important Conversations&lt;/strong&gt;: If an AI conversation takes more than 10 minutes or produces something you&amp;rsquo;ll reference again, anchor it to your workspace immediately. This creates a searchable archive of valuable AI interactions and outputs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI Swipe Files&lt;/strong&gt;: Create a dedicated folder for good examples—business proposals, code patterns, writing samples—that AI can reference. Instead of starting from scratch, you open your swipe folder to find examples that guide AI toward the quality and style you want. This acts as a curated reference library for AI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Task Breakdown and Allocation&lt;/strong&gt;: Before writing anything, break work into steps and microtasks. Then decide whether each microtask should be done manually or with AI, and specify the exact tool best suited for that task. For projects taking more than an hour, spend 5-10 minutes mapping steps and tagging which are AI or manual.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Battle-Tested Prompts&lt;/strong&gt;: You don&amp;rsquo;t need a thousand random prompts—you need 10-15 battle-tested ones that you can use every day. Focus on quality and refinement rather than quantity.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;: &lt;a href="https://www.youtube.com/watch?v=E7YiKBeOneo"&gt;Give Me 9 Minutes, I&amp;rsquo;ll Make You AI-Native&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Bret&amp;#43;Victor&amp;#43;-&amp;#43;The&amp;#43;Ladder&amp;#43;of&amp;#43;Abstraction&amp;#43;%28Highlights%29"&gt;Bret Victor - The Ladder of Abstraction (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Howard&amp;#43;Marks&amp;#43;-&amp;#43;Is&amp;#43;It&amp;#43;A&amp;#43;Bubble&amp;#43;%28Highlights%29"&gt;Howard Marks - Is It A Bubble (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Jeff&amp;#43;Su&amp;#43;-&amp;#43;Give&amp;#43;Me&amp;#43;9&amp;#43;Minutes%2C&amp;#43;I%27ll&amp;#43;Make&amp;#43;You&amp;#43;AI-Native&amp;#43;%28Highlights%29"&gt;Jeff Su - Give Me 9 Minutes, I&amp;rsquo;ll Make You AI-Native (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2025-12-13</title><link>https://blog.iany.me/journal/2025/12/what-i-touched-this-week-2025-12-13/</link><pubDate>Sat, 13 Dec 2025 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2025/12/what-i-touched-this-week-2025-12-13/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="social-architecture-and-open-source-community-building"&gt;Social Architecture and Open Source Community Building&lt;/h2&gt;
&lt;p&gt;This week I dove deep into Pieter Hintjens&amp;rsquo; &amp;ldquo;Social Architecture,&amp;rdquo; a comprehensive guide to building sustainable open source communities. The book provides a framework for understanding how successful communities function, with direct relevance to my work on CKB Fiber and other open source projects.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The C4 Process&lt;/strong&gt;: An evolution of the GitHub Fork + Pull Model that emphasizes problem-first thinking. All patches must start by identifying a clear problem before proposing solutions. This creates a culture where contributors focus on real needs rather than speculative features.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimistic Merging&lt;/strong&gt;: Maintainers should merge patches rapidly, even if imperfect. This creates opportunities for mentoring—when a patch needs work, other contributors naturally step in to help, building relationships and improving code quality through collaboration rather than gatekeeping.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free Entry and Non-Tribalism&lt;/strong&gt;: Successful communities welcome competition and experimentation. If starting a competing project gets a negative emotional response, the group is too tribal. Healthy communities applaud new competitors as spaces for learning and innovation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The 20 Principles&lt;/strong&gt;: From strong mission and transparency to minimalism and sense of humor, Hintjens identifies key characteristics of thriving communities. Particularly resonant: &amp;ldquo;Perfection precludes participation&amp;rdquo;—releasing buggy, half-finished work provokes contributions more effectively than polished releases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Economic Incentives&lt;/strong&gt;: We&amp;rsquo;re economic animals, and architecture must provide economic incentive to invest. Whether it&amp;rsquo;s reputation, money, or skill development, contributors need clear value propositions. Think of architecture as a marketplace, not just an engineering design.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://hintjens.wdfiles.com/local--files/books/social-architecture.pdf"&gt;Social Architecture&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="ai-assisted-development-and-learning"&gt;AI-Assisted Development and Learning&lt;/h2&gt;
&lt;p&gt;Multiple articles this week explored how to effectively use AI as a development and learning partner, moving beyond simple code generation to more sophisticated workflows.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Learning with AI&lt;/strong&gt;: Scott Young emphasizes using AI for book recommendations and practice problem generation rather than summaries. AI tutoring works better than teaching because specific questions constrain answers more effectively. For repetitive learning tasks, building applications can be more consistent than repeated prompting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent Design Challenges&lt;/strong&gt;: Armin Ronacher highlights that differences between models are significant enough that you&amp;rsquo;ll likely need custom abstractions. Key insights include using static system prompts with dynamic messages for caching, and hiding execution failures from context by running potentially failing tasks in subagents until they succeed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLAUDE.md Best Practices&lt;/strong&gt;: Kyle Mistele recommends keeping &lt;code&gt;CLAUDE.md&lt;/code&gt; under 300 lines (ideally under 60), with task-specific instructions in separate markdown files. Most importantly: &amp;ldquo;Never send an LLM to do a linter&amp;rsquo;s job&amp;rdquo;—use deterministic tools whenever possible since LLMs are expensive and slow compared to traditional tooling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Additionally, Ed sent me an email to introduce his app, &lt;a href="https://grasp.study/"&gt;Grasp&lt;/a&gt;, which I found impressively aligned with these best practices and strategies from the past two weeks&amp;rsquo; articles.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.scotthyoung.com/blog/2025/12/02/5-strategies-to-learn-better-with-ai-and-traps-to-avoid/"&gt;5 Strategies to Learn Better With AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lucumr.pocoo.org/2025/11/21/agents-are-hard/"&gt;Agent Design Is Still Hard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.humanlayer.dev/blog/writing-a-good-claude-md"&gt;Writing a good CLAUDE.md&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="git-workflow-improvements"&gt;Git Workflow Improvements&lt;/h2&gt;
&lt;p&gt;I published a blog post about backing up ignored files using Git remote branches, solving a problem I&amp;rsquo;ve had for years. The solution uses Git&amp;rsquo;s low-level plumbing commands to store ignored files (like local configs, IDE settings, or personal scripts) in a separate remote branch without affecting the main branch or working directory.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Problem&lt;/strong&gt;: Files that need backup but shouldn&amp;rsquo;t be in main branch history—local development settings, IDE configurations, personal notes, or workflow-specific scripts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Solution&lt;/strong&gt;: A &lt;code&gt;git-store-file&lt;/code&gt; script that uses a temporary Git index to commit ignored files to a remote branch (default: &lt;code&gt;origin/_store&lt;/code&gt;). This keeps everything in one repository while maintaining clear separation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Technical Approach&lt;/strong&gt;: Uses &lt;code&gt;GIT_INDEX_FILE&lt;/code&gt; for isolated index manipulation, &lt;code&gt;git read-tree&lt;/code&gt; to load branch state, &lt;code&gt;git add -f&lt;/code&gt; for ignored files, and &lt;code&gt;git commit-tree&lt;/code&gt; for direct commit creation—all without touching the working directory.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://blog.iany.me/2025/12/backup-ignored-files-with-git-remote-branch/"&gt;♯ Backup Ignored Files with Git Remote Branch&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Pieter&amp;#43;Hintjens&amp;#43;-&amp;#43;Social&amp;#43;Architecture&amp;#43;%28Highlights%29"&gt;Pieter Hintjens - Social Architecture (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Scott&amp;#43;Young&amp;#43;-&amp;#43;5&amp;#43;Strategies&amp;#43;to&amp;#43;Learn&amp;#43;Better&amp;#43;With&amp;#43;AI&amp;#43;%28Highlights%29"&gt;Scott Young - 5 Strategies to Learn Better With AI (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Armin&amp;#43;Ronacher&amp;#43;-&amp;#43;Agent&amp;#43;Design&amp;#43;Is&amp;#43;Still&amp;#43;Hard&amp;#43;%28Highlights%29"&gt;Armin Ronacher - Agent Design Is Still Hard (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Kyle&amp;#43;Mistele&amp;#43;-&amp;#43;Writing&amp;#43;a&amp;#43;good&amp;#43;CLAUDE.md&amp;#43;%28Highlights%29"&gt;Kyle Mistele - Writing a good CLAUDE.md (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>Backup Ignored Files with Git Remote Branch</title><link>https://blog.iany.me/2025/12/backup-ignored-files-with-git-remote-branch/</link><pubDate>Fri, 12 Dec 2025 03:22:58 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2025/12/backup-ignored-files-with-git-remote-branch/</guid><description>&lt;p&gt;When working with Git repositories, there are often files that need to be backed up but shouldn&amp;rsquo;t be committed to the main branch. These might include local development settings, IDE configuration files, personal notes, or development scripts that are specific to your workflow. The challenge is finding a way to back up these ignored files without polluting the main repository history.&lt;/p&gt;
&lt;h2 id="the-original-solution"&gt;The Original Solution&lt;/h2&gt;
&lt;p&gt;My original approach was to use a separate repository to store backup files for all repositories and create symbolic links. This worked, but had several drawbacks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Backup files were disconnected from their source repositories, making it harder to track what belongs where&lt;/li&gt;
&lt;li&gt;Backing up new files required moving them to the backup repository and then creating symbolic links&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="a-better-approach-remote-branch"&gt;A Better Approach: Remote Branch&lt;/h2&gt;
&lt;p&gt;Instead of using a separate repository, we can use a remote branch within the same repository. This approach offers several advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Everything stays in one repository&lt;/li&gt;
&lt;li&gt;When you clone the repository, the backup branch comes with it&lt;/li&gt;
&lt;li&gt;Backup files are stored in a separate branch, keeping the main branch clean&lt;/li&gt;
&lt;li&gt;Full Git history for backup files, just like any other branch&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-script"&gt;The Script&lt;/h2&gt;
&lt;p&gt;With AI assistance, I developed &lt;code&gt;git-store-file&lt;/code&gt;, a tool for managing ignored files by storing them in a remote branch (default: &lt;code&gt;origin/_store&lt;/code&gt;). This keeps backup files separate from your working branch without interfering with regular development. The script is available in &lt;a href="https://github.com/doitian/dotfiles-public/blob/master/default/bin/git-store-file"&gt;Python&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="installation"&gt;Installation&lt;/h3&gt;
&lt;p&gt;Download the script and make it executable:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;curl -o ~/bin/git-store-file https://raw.githubusercontent.com/doitian/dotfiles-public/master/default/bin/git-store-file
chmod +x ~/bin/git-store-file
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="usage"&gt;Usage&lt;/h3&gt;
&lt;p&gt;The script has four main commands: &lt;code&gt;store&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;restore&lt;/code&gt;, and &lt;code&gt;ls&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="store-files"&gt;Store Files&lt;/h4&gt;
&lt;p&gt;Store one or more files to the remote branch, including files that are ignored by Git:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# Store a single file
git-store-file config.local.json
# Store multiple files
git-store-file config.local.json secrets.env
# Use a custom branch name
git-store-file --branch backup config.local.json
# Use a custom remote
git-store-file --remote upstream config.local.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you run the store command, the script will:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a temporary Git index to avoid conflicts with your working directory&lt;/li&gt;
&lt;li&gt;Load the target branch&amp;rsquo;s current state into the temporary index&lt;/li&gt;
&lt;li&gt;Add the specified files using &lt;code&gt;git add -f&lt;/code&gt; to include ignored files&lt;/li&gt;
&lt;li&gt;Create a commit with the changes&lt;/li&gt;
&lt;li&gt;Display commit statistics and prompt for confirmation&lt;/li&gt;
&lt;li&gt;Push the commit to the remote branch&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="check-status"&gt;Check Status&lt;/h4&gt;
&lt;p&gt;Check which files stored in the remote branch have been modified in your local working directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# Check status
git-store-file status
# Show diff for modified files
git-store-file status --diff
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="restore-files"&gt;Restore Files&lt;/h4&gt;
&lt;p&gt;Restore files from the remote branch to your working directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# Restore a specific file
git-store-file restore config.local.json
# Restore all files from the branch
git-store-file restore
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="list-files"&gt;List Files&lt;/h4&gt;
&lt;p&gt;List all files stored in the remote branch:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;git-store-file ls
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="options"&gt;Options&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-b, --branch BRANCH&lt;/code&gt;: Specify the branch name (default: &lt;code&gt;_store&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-r, --remote REMOTE&lt;/code&gt;: Specify the remote name (default: &lt;code&gt;origin&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-h, --help&lt;/code&gt;: Show help message&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="examples"&gt;Examples&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# Store local configuration
git-store-file .env.local
# Check what's changed
git-store-file status -d
# Restore after cloning
git-store-file restore .env.local
# List all backed up files
git-store-file ls
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="how-it-works"&gt;How It Works&lt;/h2&gt;
&lt;p&gt;The script uses Git&amp;rsquo;s low-level plumbing commands to manipulate the repository without affecting your working directory:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Temporary Index&lt;/strong&gt;: Uses the &lt;code&gt;GIT_INDEX_FILE&lt;/code&gt; environment variable to create a temporary index, completely isolated from your working directory&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read Tree&lt;/strong&gt;: Loads the target branch&amp;rsquo;s tree structure into the temporary index&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Force Add&lt;/strong&gt;: Uses &lt;code&gt;git add -f&lt;/code&gt; to add files even if they&amp;rsquo;re listed in &lt;code&gt;.gitignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Commit Tree&lt;/strong&gt;: Creates a commit object directly using &lt;code&gt;git commit-tree&lt;/code&gt;, bypassing the normal commit workflow&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct Push&lt;/strong&gt;: Pushes the commit hash directly to the remote branch reference&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This low-level approach ensures that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your working directory remains unchanged&lt;/li&gt;
&lt;li&gt;The main branch is never affected&lt;/li&gt;
&lt;li&gt;Ignored files can be stored without modifying &lt;code&gt;.gitignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The backup branch maintains full Git history&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;git-store-file&lt;/code&gt; script solves the problem of backing up ignored files in a clean, Git-native way. By leveraging a remote branch, it keeps everything in one repository while maintaining clear separation between your main codebase and backup files. Whether you&amp;rsquo;re managing local configurations, IDE settings, or personal development scripts, this tool provides a simple and reliable backup solution that integrates seamlessly with your existing Git workflow.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/git/">Git</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/backup/">Backup</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2025-12-06</title><link>https://blog.iany.me/journal/2025/12/what-i-touched-this-week-2025-12-06/</link><pubDate>Sat, 06 Dec 2025 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2025/12/what-i-touched-this-week-2025-12-06/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="fuzzing-strategies-from-tigerbeetle"&gt;Fuzzing Strategies from TigerBeetle&lt;/h2&gt;
&lt;p&gt;This week I dove deep into TigerBeetle&amp;rsquo;s fuzzing philosophy through several articles by Aleksey Kladov. The collection reveals a systematic approach to building testable systems from the ground up.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Design for Fuzzability First&lt;/strong&gt;: You don&amp;rsquo;t build a system and then add a fuzzer—the process is almost reversed. Start by sketching minimal interfaces that yield themselves to efficient fuzzing. Minimize the interface by eliminating accidental dependencies and leaving only essential ones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data-Oriented Design&lt;/strong&gt;: Apply data-oriented design principles—think in terms of input data, output data, and the fundamental data transformation the system implements. This makes the code more easily fuzzable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Four Fuzzer Strategy&lt;/strong&gt;: Use multiple fuzzing approaches: (1) positive space fuzzing with ideal lab environments to verify correctness, (2) negative space fuzzing calling all public methods in random order to break things, (3) boundary testing for valid vs. almost-valid-but-invalid codes, and (4) whole subsystem fuzzing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Randomness in Tests&lt;/strong&gt;: Don&amp;rsquo;t always use hard-coded seeds. Zig&amp;rsquo;s approach is best—provide &lt;code&gt;std.testing.random_seed&lt;/code&gt; that&amp;rsquo;s different per run but generated outside the test process. Run tests twice: once with a hard-coded seed for regression testing, and once with a truly random seed for coverage.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tigerbeetle.com/blog/2025-11-28-tale-of-four-fuzzers/"&gt;A Tale of Four Fuzzers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://matklad.github.io/2021/11/07/generate-all-the-things.html"&gt;Generate All the Things&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tigerbeetle.com/blog/2025-04-23-swarm-testing-data-structures/"&gt;Swarm Testing Data Structures&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="lightning-network-mobile-nodes-and-self-custody"&gt;Lightning Network Mobile Nodes and Self-Custody&lt;/h2&gt;
&lt;p&gt;Stephen DeLorme&amp;rsquo;s article on Lightning self-custody and mobile nodes provided valuable insights into the future of Bitcoin payments, with direct relevance to my work on CKB Fiber.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mobile Receiving Nodes&lt;/strong&gt;: For receiving-only nodes, you don&amp;rsquo;t need routing, meaning you don&amp;rsquo;t need all that database storage. This was pioneered by projects like Breez, Phoenix, Mutiny wallet, Blixt, and newer versions of Zeus.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offline Payments Problem&lt;/strong&gt;: Even the sharpest performing mobile lightning wallets suffer from a lack of offline payments, which has severely tarnished the perception of Lightning. BIP-353 user names distributed via DNS and recent LDK PRs for async payments are addressing this.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Capital Efficiency&lt;/strong&gt;: Splicing is a powerful tool for easing capital constraints for LSPs. The &lt;code&gt;option_zero_reserve&lt;/code&gt; allows channel partners to choose not to use a channel reserve balance—great for UX, though it requires solving minimal capacity restrictions (relevant for CKB Fiber).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Graduated Wallet Model&lt;/strong&gt;: A wallet that begins with a custodial service or trust-minimized technology, then graduates to a Lightning channel when the balance grows to a certain threshold. This helps LSPs avoid opening &amp;ldquo;non-productive&amp;rdquo; channels to users just experimenting with the app.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dust Limit Considerations&lt;/strong&gt;: Amounts below the dust limit (like a 21 sat Nostr zap) wouldn&amp;rsquo;t be claimable on-chain anyway, suggesting alternative approaches for micro-transactions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://www.voltage.cloud/blog/lightning-self-custody-works"&gt;Lightning Self-Custody Works: Why Mobile Nodes Are the Future of Bitcoin&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="think-first-ai-second"&gt;Think First, AI Second&lt;/h2&gt;
&lt;p&gt;Ines Lee&amp;rsquo;s article on using AI as a thinking partner rather than a solution generator resonated deeply with my own experience using AI tools.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Active vs. Passive AI Use&lt;/strong&gt;: Active AI use means building understanding while collaborating with the model. Frame the problem yourself, make an initial pass, then use AI to challenge assumptions, uncover blind spots, and sharpen arguments. Studies show this approach maintains cognitive engagement even while using AI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI as Coach, Not Cheerleader&lt;/strong&gt;: Shift AI from talking to you about your work (where it&amp;rsquo;s sycophantic) to talking to other editors about your work (where it&amp;rsquo;s professionally candid). You&amp;rsquo;re no longer the audience, so it drops the protective politeness.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Combat Illusion of Understanding&lt;/strong&gt;: AI explains things in ways that are easy to follow, making us susceptible to the &amp;ldquo;illusion of understanding.&amp;rdquo; We overestimate what we&amp;rsquo;ve learned. Real comprehension emerges when forced to articulate thinking, because explaining reveals gaps we didn&amp;rsquo;t know existed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Practical Prompts&lt;/strong&gt;: Use prompts like &amp;ldquo;Ask exactly 5 high-leverage questions before proposing solutions,&amp;rdquo; &amp;ldquo;Act as an editor recommending whether to publish this piece,&amp;rdquo; &amp;ldquo;Analyze my reasoning and create an architecture of my thinking,&amp;rdquo; &amp;ldquo;Switch to devil&amp;rsquo;s advocate mode and dismantle my thesis,&amp;rdquo; and &amp;ldquo;Ask me to explain this as if teaching it to someone else.&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://every.to/emails/think-first-ai-second"&gt;Think First, AI Second&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Aleksey&amp;#43;Kladov&amp;#43;-&amp;#43;A&amp;#43;Tale&amp;#43;of&amp;#43;Four&amp;#43;Fuzzers&amp;#43;%28Highlights%29"&gt;Aleksey Kladov - A Tale of Four Fuzzers (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Aleksey&amp;#43;Kladov&amp;#43;-&amp;#43;Generate&amp;#43;All&amp;#43;the&amp;#43;Things&amp;#43;%28Highlights%29"&gt;Aleksey Kladov - Generate All the Things (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Aleksey&amp;#43;Kladov&amp;#43;-&amp;#43;Swarm&amp;#43;Testing&amp;#43;Data&amp;#43;Structures&amp;#43;%28Highlights%29"&gt;Aleksey Kladov - Swarm Testing Data Structures (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Stephen&amp;#43;DeLorme&amp;#43;-&amp;#43;Lightning&amp;#43;Self-Custody&amp;#43;Works&amp;#43;Why&amp;#43;Mobile&amp;#43;Nodes&amp;#43;Are&amp;#43;the&amp;#43;Future&amp;#43;of&amp;#43;Bitcoin&amp;#43;%28Highlights%29"&gt;Stephen DeLorme - Lightning Self-Custody Works Why Mobile Nodes Are the Future of Bitcoin (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Ines&amp;#43;Lee&amp;#43;-&amp;#43;Think&amp;#43;First%2C&amp;#43;AI&amp;#43;Second&amp;#43;%28Highlights%29"&gt;Ines Lee - Think First, AI Second (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>What I Touched This Week 2025-11-29</title><link>https://blog.iany.me/journal/2025/11/what-i-touched-this-week-2025-11-29/</link><pubDate>Sat, 29 Nov 2025 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/journal/2025/11/what-i-touched-this-week-2025-11-29/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
This journal is generated by AI
&lt;/summary&gt;
&lt;/details&gt;
&lt;h2 id="software-design-insights-from-tigerbeetle"&gt;Software Design Insights from TigerBeetle&lt;/h2&gt;
&lt;p&gt;This week I went deep into several articles from Alexey Kladov (matklad) and TigerBeetle&amp;rsquo;s engineering blog. The collection offers practical wisdom on building robust systems.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Code Review Workflow&lt;/strong&gt;: Instead of reviewing diffs in a web UI, pull the branch locally, soft-reset to the base, then navigate through the diff using magit. Use the git staging area to mark files you&amp;rsquo;ve reviewed—treating the review process like authoring the code yourself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clockless Time Tracking&lt;/strong&gt;: Rather than injecting a &lt;code&gt;Clock&lt;/code&gt; dependency, push time into the system at fixed cadence via a &lt;code&gt;tick&lt;/code&gt; method. For single-instant needs, use Call Site Dependency Injection by passing &lt;code&gt;now: Instant&lt;/code&gt; directly to methods.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write Last, Read First&lt;/strong&gt;: When dealing with systems of record (source of truth) and systems of reference (derived data), write to the system of reference first, then write to the system of record last. Read in reverse order. This maintains application-level consistency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Function Shape Matters&lt;/strong&gt;: Functions should have an &amp;ldquo;inverted hourglass&amp;rdquo; shape—large implementations relative to their interfaces. Hourglass-shaped functions (small body, large interface) are a code smell. The 100-column limit exists so you can fit two files side-by-side on modern displays. Functions should fit on a screen (~60-70 lines).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tigerbeetle.com/blog/2025-08-04-code-review-can-be-better"&gt;Code Review Can Be Better&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tigerbeetle.com/blog/2025-10-21-clockless-time"&gt;Clockless Time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tigerbeetle.com/blog/2025-11-06-the-write-last-read-first-rule"&gt;Write Last, Read First&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://matklad.github.io/2025/11/28/size-matters.html"&gt;Size Matters&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="self-consistency-over-self-discipline"&gt;Self-Consistency Over Self-Discipline&lt;/h2&gt;
&lt;p&gt;An essay on achieving a good life without rigid self-discipline (&amp;ldquo;谈谈不自律的良好生活&amp;rdquo;) resonated with me this week. The author argues for balance over brute-force willpower.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Balanced Love&lt;/strong&gt;: Drawing from Fromm&amp;rsquo;s &lt;em&gt;The Art of Loving&lt;/em&gt;, healthy self-regard requires combining unconditional and conditional love. Pure unconditional love becomes indulgence; pure conditional love becomes self-attack.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Understanding Over Judgment&lt;/strong&gt;: When you break a diet for your favorite ice cream, the productive response isn&amp;rsquo;t self-criticism—it&amp;rsquo;s understanding &lt;em&gt;why&lt;/em&gt; you acted that way. Change requires awareness, care, creativity, and patience rather than pure constraint.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Goals as Direction&lt;/strong&gt;: Goals shouldn&amp;rsquo;t be used to judge or lock down your future. A proper goal is merely a direction—like pointing at a distant peak and saying &amp;ldquo;I want to go there.&amp;rdquo; This supports &lt;a href="https://www.youtube.com/watch?v=aAk9zvROJFk"&gt;Don&amp;rsquo;t Set a Goal For 2026 Until You Watch This&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Life&amp;rsquo;s Balance&lt;/strong&gt;: Abstract learning (philosophy) without grounding becomes detached from life&amp;rsquo;s simple pleasures. All socializing without solitude leaves no room for reflection and creativity. Life needs all five flavors, both meat and vegetables.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://sspai.com/post/103819"&gt;谈谈不自律的良好生活&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="effective-reading-strategies"&gt;Effective Reading Strategies&lt;/h2&gt;
&lt;p&gt;A practical guide on retaining what you read offered some actionable techniques worth implementing.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Preview First&lt;/strong&gt;: Spend 5-10 minutes per chapter skimming the table of contents, headings, introduction, and summary before diving into details. This builds a mental scaffold.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Question-Driven Reading&lt;/strong&gt;: Before reading a section, convert the heading into a question (e.g., &amp;ldquo;What is X?&amp;rdquo;) and read to find the answer. This active approach improves engagement and retention.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Concise Personal Notes&lt;/strong&gt;: Rather than transcribing passages verbatim, write brief paraphrases or bullet points in your own words. The act of reformulation strengthens memory.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://readwise-assets.s3.amazonaws.com/media/wisereads/articles/how-to-remember-everything-you/1031.pdf"&gt;How to Remember Everything You Read&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="obsidian-links"&gt;Obsidian Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Alexey&amp;#43;Kladov&amp;#43;-&amp;#43;Code&amp;#43;Review&amp;#43;Can&amp;#43;Be&amp;#43;Better&amp;#43;%28Highlights%29"&gt;Alexey Kladov - Code Review Can Be Better (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Alexey&amp;#43;Kladov&amp;#43;-&amp;#43;Tracking&amp;#43;Time&amp;#43;Without&amp;#43;Clock&amp;#43;%28Highlights%29"&gt;Alexey Kladov - Tracking Time Without Clock (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Dominik&amp;#43;Tornow&amp;#43;-&amp;#43;The&amp;#43;Write&amp;#43;Last%2C&amp;#43;Read&amp;#43;First&amp;#43;Rule&amp;#43;%28Highlights%29"&gt;Dominik Tornow - The Write Last, Read First Rule (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Alex&amp;#43;Kladov&amp;#43;-&amp;#43;Size&amp;#43;Matters&amp;#43;%28Highlights%29"&gt;Alex Kladov - Size Matters (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Sylvan&amp;#43;Wang&amp;#43;-&amp;#43;%E8%B0%88%E8%B0%88%E4%B8%8D%E8%87%AA%E5%BE%8B%E7%9A%84%E8%89%AF%E5%A5%BD%E7%94%9F%E6%B4%BB&amp;#43;%28Highlights%29"&gt;Sylvan Wang - 谈谈不自律的良好生活 (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/robot/Readwise&amp;#43;Library/Articles/Polymath&amp;#43;Investor&amp;#43;-&amp;#43;How&amp;#43;to&amp;#43;Remember&amp;#43;Everything&amp;#43;You&amp;#43;Read&amp;#43;%28Highlights%29"&gt;Polymath Investor - How to Remember Everything You Read (Highlights)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/journal/">Journals</category><category domain="https://blog.iany.me/series/what-i-touched/">What I Touched</category><category domain="https://blog.iany.me/tags/ai-generated/">Ai Generated</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>How to Activate mise for Cursor When It Runs Shell Commands</title><link>https://blog.iany.me/2025/11/how-to-activate-mise-for-cursor-when-it-runs-shell-commands/</link><pubDate>Wed, 26 Nov 2025 11:31:37 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2025/11/how-to-activate-mise-for-cursor-when-it-runs-shell-commands/</guid><description>&lt;p&gt;When using &lt;a href="https://mise.jdx.dev/"&gt;mise&lt;/a&gt; with Cursor, you may notice that the mise environment is not activated when Cursor executes shell commands. This occurs because Cursor launches a non-login shell, which initializes differently than an interactive login shell and therefore does not automatically source your usual mise setup.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Cursor runs commands in a non-login shell, which means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For &lt;strong&gt;zsh&lt;/strong&gt;: Only &lt;code&gt;.zshenv&lt;/code&gt; is loaded (not &lt;code&gt;.zshrc&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;bash&lt;/strong&gt;: Only &lt;code&gt;.bash_profile&lt;/code&gt; is loaded (not &lt;code&gt;.bashrc&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;ve configured mise activation in &lt;code&gt;.zshrc&lt;/code&gt; or &lt;code&gt;.zprofile&lt;/code&gt;, it won&amp;rsquo;t be available when Cursor executes commands.&lt;/p&gt;
&lt;h2 id="the-solution"&gt;The Solution&lt;/h2&gt;
&lt;p&gt;The solution is to activate mise in &lt;code&gt;.zshenv&lt;/code&gt; (for zsh) or &lt;code&gt;.bash_profile&lt;/code&gt; (for bash) when the file is loaded by Cursor. You can detect Cursor by checking for the &lt;code&gt;CURSOR_AGENT&lt;/code&gt; environment variable.&lt;/p&gt;
&lt;p&gt;Add this to your &lt;code&gt;~/.zshenv&lt;/code&gt; or &lt;code&gt;~/.bash_profile&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;# Activate mise when running in Cursor
if [[ -n &amp;quot;$CURSOR_AGENT&amp;quot; ]]; then
eval &amp;quot;$(mise activate)&amp;quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="why-this-works"&gt;Why This Works&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.zshenv&lt;/code&gt; (or &lt;code&gt;.bash_profile&lt;/code&gt;) is always sourced for non-login shells&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;CURSOR_AGENT&lt;/code&gt; environment variable is set when Cursor runs commands&lt;/li&gt;
&lt;li&gt;By conditionally activating mise only when &lt;code&gt;CURSOR_AGENT&lt;/code&gt; is present, you avoid potential conflicts or slowdowns in other non-login shell contexts&lt;/li&gt;
&lt;li&gt;This ensures mise and all its configured tools are available when Cursor executes terminal commands&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After adding this configuration, rerun Cursor chats, and mise should be available in all Cursor shell commands.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/mise/">Mise</category><category domain="https://blog.iany.me/tags/cursor/">Cursor</category><category domain="https://blog.iany.me/tags/shell/">Shell</category><category domain="https://blog.iany.me/tags/zsh/">Zsh</category><category domain="https://blog.iany.me/tags/vibe-coding/">Vibe Coding</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>Temporary Vi Mode in PowerShell</title><link>https://blog.iany.me/2025/11/temporary-vi-mode-in-powershell/</link><pubDate>Sat, 22 Nov 2025 23:17:05 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2025/11/temporary-vi-mode-in-powershell/</guid><description>&lt;p&gt;I miss the feature in readline (Bash/Zsh) where &lt;code&gt;Ctrl+x, Ctrl+v&lt;/code&gt; switches to Vi command mode temporarily. In that workflow, entering Insert mode switches back to Emacs mode. &lt;code&gt;PSReadLine&lt;/code&gt; has a command &lt;code&gt;ViCommandMode&lt;/code&gt;, but binding it directly in Emacs mode will report errors on every key input.&lt;/p&gt;
&lt;p&gt;The solution requires handling the mode change event to toggle the global &lt;code&gt;EditMode&lt;/code&gt; between &lt;code&gt;Emacs&lt;/code&gt; and &lt;code&gt;Vi&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When entering &amp;ldquo;Insert&amp;rdquo; mode (the default for Emacs users), we force the mode to &lt;code&gt;Emacs&lt;/code&gt; and set up the trigger for the temporary Vi mode. When that trigger (&lt;code&gt;Ctrl+x, Ctrl+v&lt;/code&gt;) is fired, we switch the edit mode to &lt;code&gt;Vi&lt;/code&gt; and jump to command mode.&lt;/p&gt;
&lt;p&gt;Here is the configuration for your &lt;code&gt;$PROFILE&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;function OnViModeChange {
if ($args[0] -eq 'Insert') {
Set-PSReadLineOption -EditMode Emacs
Set-PSReadlineKeyHandler -Chord &amp;quot;Ctrl+x,Ctrl+v&amp;quot; -ScriptBlock {
Set-PSReadLineOption -EditMode Vi
[Microsoft.PowerShell.PSConsoleReadLine]::ViCommandMode()
}
# Add other Emacs key bindings here.
# Switching `EditMode` resets key bindings to their defaults.
# Remember to reconfigure them.
# Set-PSReadlineKeyHandler -Chord &amp;quot;Ctrl+w&amp;quot; -Function BackwardKillWord
}
}
Set-PSReadLineOption -ViModeIndicator Script
Set-PSReadLineOption -ViModeChangeHandler $Function:OnViModeChange
Set-PSReadLineOption -EditMode Emacs
# Reuse the function to start Emacs mode
OnViModeChange Insert
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Switching &lt;code&gt;EditMode&lt;/code&gt; resets key bindings to their defaults. Remember to reconfigure them.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/powershell/">PowerShell</category><category domain="https://blog.iany.me/tags/vim/">Vim</category><category domain="https://blog.iany.me/tags/emacs/">Emacs</category><category domain="https://blog.iany.me/tags/console/">Console</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>Study on Quotient Spaces</title><link>https://blog.iany.me/2025/11/study-on-quotient-spaces/</link><pubDate>Tue, 18 Nov 2025 21:23:25 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2025/11/study-on-quotient-spaces/</guid><description>&lt;p&gt;I&amp;rsquo;m reading &lt;em&gt;Linear Algebra Done Right&lt;/em&gt; by Axler and found the section on quotient spaces difficult to understand, so I researched and took these notes.&lt;/p&gt;
&lt;h2 id="definitions"&gt;Definitions&lt;/h2&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.95 notion: $v + U$
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;p&gt;Suppose &lt;code&gt;$v \in V$&lt;/code&gt; and &lt;code&gt;$U \subseteq V$&lt;/code&gt;. Then &lt;code&gt;$v + U$&lt;/code&gt; is the subset of &lt;code&gt;$V$&lt;/code&gt; defined by&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[v + U = \{v + u : u \in U\}.\]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;Also called a translate. &lt;strong&gt;Attention&lt;/strong&gt; that a translate is a set.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.97 definition: &lt;em&gt;translate&lt;/em&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
Suppose &lt;code&gt;$v \in V$&lt;/code&gt; and &lt;code&gt;$U \subseteq V$&lt;/code&gt;, the set &lt;code&gt;$v + U$&lt;/code&gt; is said to be a &lt;em&gt;translate&lt;/em&gt; of &lt;code&gt;$U$&lt;/code&gt;.
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;Quotient space is a set of all translates (set of sets):&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.99 definition: &lt;em&gt;quotient space&lt;/em&gt;, $V/U$
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;p&gt;Suppose &lt;code&gt;$U$&lt;/code&gt; is a subspace of &lt;code&gt;$V$&lt;/code&gt;. Then the &lt;em&gt;quotient space&lt;/em&gt; &lt;code&gt;$V/U$&lt;/code&gt; is the set of all translates of &lt;code&gt;$U$&lt;/code&gt;. Thus&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[V/U = \{v + U : v \in V\}.\]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;Quotient space is a set of sets. There are duplicates for each &lt;code&gt;$v \in V$&lt;/code&gt; because for some &lt;code&gt;$v_1, v_2 \in V$&lt;/code&gt;, &lt;code&gt;$v_1 + U$&lt;/code&gt; and &lt;code&gt;$v_2 + U$&lt;/code&gt; can be identical set.&lt;/p&gt;
&lt;p&gt;A quotient space &lt;code&gt;$V/U$&lt;/code&gt; is formed by &amp;ldquo;collapsing&amp;rdquo; a subspace &lt;code&gt;$U$&lt;/code&gt; to zero within a larger vector space &lt;code&gt;$V$&lt;/code&gt;. This construction is based on an equivalence relation where two vectors &lt;code&gt;$x, y \in V$&lt;/code&gt; are considered equivalent if their difference lies in &lt;code&gt;$U$&lt;/code&gt;—that is, &lt;code&gt;$x \sim y$&lt;/code&gt; if and only if &lt;code&gt;$x - y \in U$&lt;/code&gt;. &lt;a href="https://en.wikipedia.org/wiki/Quotient_space_%28linear_algebra%29"&gt;wikipedia&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="lemmas"&gt;Lemmas&lt;/h2&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.101 &lt;em&gt;two translates of a subspace are equal or disjoint&lt;/em&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;p&gt;Suppose &lt;code&gt;$U$&lt;/code&gt; is a subspace of &lt;code&gt;$V$&lt;/code&gt; and &lt;code&gt;$v, w \in V$&lt;/code&gt;. Then&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
v - w \in U \iff v + U = w + U \iff (v + U) \cap (w + U) \neq \emptyset
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;If two translates are not disjoint (the union set is not empty), they must be equal. So they are equal or disjoint.&lt;/p&gt;
&lt;p&gt;All distinct translates of a subspace are disjoint. Given any &lt;code&gt;$v \in V$&lt;/code&gt;, it belongs to only one translate.&lt;/p&gt;
&lt;p&gt;Since the quotient space &lt;code&gt;$V/U$&lt;/code&gt; is a set of translates of a subspace, it is like a disjoint partition of values in &lt;code&gt;$V$&lt;/code&gt;. By using the definition of quotient map&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.104 definition: &lt;em&gt;quotient map&lt;/em&gt;, $\pi$
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;p&gt;Suppose &lt;code&gt;$U$&lt;/code&gt; is a subspace of &lt;code&gt;$V$&lt;/code&gt;. The &lt;em&gt;quotient map&lt;/em&gt; &lt;code&gt;$\pi : V \to V/U$&lt;/code&gt; is the linear map defined by&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[\pi(v) = v + U\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;for each &lt;code&gt;$v \in V$&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;We can write that&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\pi(v_1) = \pi(v_2) \iff v_1 - v_2 \in U
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The quotient map has two essential properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;null space&lt;/strong&gt; of &lt;code&gt;$\pi$&lt;/code&gt; is exactly the subspace &lt;code&gt;$U$&lt;/code&gt;, because &lt;code&gt;$v+U=0+U \iff v-0 \in U \iff v \in U$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;range&lt;/strong&gt; of &lt;code&gt;$\pi$&lt;/code&gt; is the entire quotient space &lt;code&gt;$V/U$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="quotient-space-is-a-vector-space"&gt;Quotient Space Is a Vector Space&lt;/h2&gt;
&lt;p&gt;First define the addition and scalar multiplication operations:&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.102 definition: &lt;em&gt;addition and scalar multiplication on&lt;/em&gt; $V/U$
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;p&gt;Suppose &lt;code&gt;$U$&lt;/code&gt; is a subspace of &lt;code&gt;$V$&lt;/code&gt;. Then addition and scalar multiplication are defined on &lt;code&gt;$V/U$&lt;/code&gt; by&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[\begin{align*}
(v + U) + (w + U) &amp;amp;= (v + w) + U \\
\lambda(v + U) &amp;amp;= (\lambda v) + U
\end{align*}\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;for all &lt;code&gt;$v, w \in V$&lt;/code&gt; and &lt;code&gt;$\lambda \in \mathbf{F}$&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;&lt;code&gt;$v+U$&lt;/code&gt; is not the unique way to represent a member in &lt;code&gt;$V/U$&lt;/code&gt;, because there may exist &lt;code&gt;$v'\ne v$&lt;/code&gt; that &lt;code&gt;$u + U = v' + U$&lt;/code&gt;. The operations make sense only when the choice of &lt;code&gt;$v$&lt;/code&gt; to represent a translate makes no differences.&lt;/p&gt;
&lt;p&gt;Specifically, suppose &lt;code&gt;$v_1, v_2, w_1, w_2 \in V$&lt;/code&gt; such that&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
v_1 + U = v_2 + U \quad\textrm{and}\quad w_1 + U = w_2 + U
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From the addition definition:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\begin{align*}
(v_1+U) + (w_1+U) &amp;amp;= (v_1 + w_1) + U \\
(v_2+U) + (w_2+U) &amp;amp;= (v_2 + w_2) + U
\end{align*}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The left side of the two equations indeed are the different representation of the same equation, so we must show that the right side equal: &lt;code&gt;$(v_1 + w_1)+U=(v2+w2)+U$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This applies to scalar multiplication as well:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\begin{align*}
\lambda(v_1 + U) &amp;amp;= (\lambda v_1) + U \\
\lambda(v_2 + U) &amp;amp;= (\lambda v_2) + U
\end{align*}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We must show that &lt;code&gt;$(\lambda v_1) + U = (\lambda v_2) + U$&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="dimension"&gt;Dimension&lt;/h2&gt;
&lt;p&gt;The dimension of the quotient space is given by a simple subtraction, relating the dimension of &lt;code&gt;$V/U$&lt;/code&gt; to the &amp;ldquo;lost&amp;rdquo; dimension of &lt;code&gt;$U$&lt;/code&gt;:&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.105 &lt;em&gt;dimension of quotient space&lt;/em&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;p&gt;Suppose &lt;code&gt;$V$&lt;/code&gt; is finite-dimensional and &lt;code&gt;$U$&lt;/code&gt; is a subspace of V. Then&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[\text{dim } V/U = \text{dim }V - \text{dim }U.\]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="linear-map-from-vnull-t-to-w"&gt;Linear Map from V/(null T) to W&lt;/h2&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.106 notation: $\widetilde{T}$
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;p&gt;Suppose &lt;code&gt;$T \in \mathcal{L}(V, W)$&lt;/code&gt;. Define &lt;code&gt;$\widetilde{T}: V/(\text{null } T) \to W$&lt;/code&gt; by&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[\widetilde{T}(v + \text{null } T) = Tv.\]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;Think of merging inputs having the same output. These inputs will be the same input in the quotient space &lt;code&gt;$V/(\text{null } T)$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For any &lt;code&gt;$v_1, v_2 \in V$&lt;/code&gt; that &lt;code&gt;$Tv_1 = Tv_2$&lt;/code&gt;, &lt;code&gt;$v_1 + \mathrm{null}\, T$&lt;/code&gt; and &lt;code&gt;$v_2 + \mathrm{null}\, T$&lt;/code&gt; are the same value in &lt;code&gt;$V/(\mathrm{null}\, T)$&lt;/code&gt;. This makes &lt;code&gt;$\widetilde{T}$&lt;/code&gt; injective. Because &lt;code&gt;$\mathrm{range}\,\widetilde{T}=\mathrm{range}\, T$&lt;/code&gt;, &lt;code&gt;$\widetilde{T}$&lt;/code&gt; is also surjective on to &lt;code&gt;$\mathrm{range}\, T$&lt;/code&gt;.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.63 &lt;em&gt;invertibility&lt;/em&gt; $\iff$ &lt;em&gt;injectivity and surjectivity&lt;/em&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
A linear map is invertible if and only if it is injective and surjective.
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;3.63 shows us that &lt;code&gt;$\widetilde{T}$&lt;/code&gt; is invertible, and according to the definition of isomorphic, &lt;code&gt;$V/(\mathrm{null}\, T)$&lt;/code&gt; and &lt;code&gt;$\mathrm{range}\,T$&lt;/code&gt; are isomorphic vector spaces and &lt;code&gt;$\widetilde{T}$&lt;/code&gt; is their isomorphism.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.69 definition: &lt;em&gt;isomorphism, isomorphic&lt;/em&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;ul&gt;
&lt;li&gt;An &lt;em&gt;isomorphism&lt;/em&gt; is an invertible linear map.&lt;/li&gt;
&lt;li&gt;Two vector spaces are called isomorphic if there is an isomorphism from one vector space onto the other one.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;One of the key uses of &lt;code&gt;$\widetilde{T}$&lt;/code&gt; is demonstrating a canonical isomorphism. For any linear map &lt;code&gt;$T \in \mathcal{L}(V, W)$&lt;/code&gt;, the quotient space &lt;code&gt;$V/(\text{null } T)$&lt;/code&gt; is isomorphic to the image space &lt;code&gt;$\text{range } T$&lt;/code&gt;. This shows that the quotient space &lt;code&gt;$V/(\text{null } T)$&lt;/code&gt; serves as a way to &amp;ldquo;mod out&amp;rdquo; the non-injective part of &lt;code&gt;$T$&lt;/code&gt;.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/math/">Math</category><category domain="https://blog.iany.me/tags/linear-algebra/">Linear Algebra</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>Hosting Static Site on Cloudflare R2</title><link>https://blog.iany.me/2025/10/hosting-static-site-on-cloudflare-r2/</link><pubDate>Wed, 22 Oct 2025 18:46:38 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2025/10/hosting-static-site-on-cloudflare-r2/</guid><description>&lt;p&gt;This post explains how to set up a static site on Cloudflare R2.&lt;/p&gt;
&lt;p&gt;I recently migrated my blog to Cloudflare R2, and the process went smoothly until I encountered R2&amp;rsquo;s lack of native support for rewriting URLs to &lt;code&gt;index.html&lt;/code&gt; files. This post explains how I resolved this issue.&lt;/p&gt;
&lt;h2 id="setting-up"&gt;Setting Up&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Create the bucket.&lt;/li&gt;
&lt;li&gt;Add the custom domain &lt;code&gt;blog.iany.me&lt;/code&gt;. To redirect &lt;code&gt;iany.me&lt;/code&gt; and &lt;code&gt;www.iany.me&lt;/code&gt; to the blog, add these as custom domains as well. Since I host the domain in Cloudflare, the DNS record is automatically configured.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="publishing"&gt;Publishing&lt;/h2&gt;
&lt;p&gt;I use &lt;code&gt;rclone&lt;/code&gt; to deploy the site to R2. Below is the GitHub workflow I employ. Noting that the &lt;code&gt;fetch-depth&lt;/code&gt; option for &lt;code&gt;actions/checkout&lt;/code&gt; retrieves the complete repository history. This works with Hugo&amp;rsquo;s &lt;code&gt;--enableGitInfo&lt;/code&gt; flag to accurately determine article creation dates.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;name: Deploy Hugo Site to Cloudflare R2
on:
push:
branches:
- master
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install latest Hugo
run: |
TAG=$(curl -s https://api.github.com/repos/gohugoio/hugo/releases/latest \
| grep '&amp;quot;tag_name&amp;quot;:' \
| head -1 \
| sed -E 's/.*&amp;quot;([^&amp;quot;]+)&amp;quot;.*/\1/')
curl -L &amp;quot;https://github.com/gohugoio/hugo/releases/download/${TAG}/hugo_${TAG#v}_Linux-64bit.tar.gz&amp;quot; \
-o hugo.tar.gz
tar -xzf hugo.tar.gz hugo
sudo mv hugo /usr/local/bin/
hugo version
- name: Install rclone
run: |
curl https://rclone.org/install.sh | sudo bash
- name: Build site with Hugo
run: hugo --minify --enableGitInfo
- name: Configure rclone for Cloudflare R2
env:
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
run: |
rclone config create r2 s3 \
provider Cloudflare \
access_key_id &amp;quot;${R2_ACCESS_KEY_ID}&amp;quot; \
secret_access_key &amp;quot;${R2_SECRET_ACCESS_KEY}&amp;quot; \
endpoint &amp;quot;${R2_ENDPOINT}&amp;quot; \
--quiet
- name: Deploy to Cloudflare R2
env:
R2_BUCKET: ${{ secrets.R2_BUCKET }}
run: |
rclone copy public/ r2:&amp;quot;${R2_BUCKET}&amp;quot; \
--checksum \
--no-traverse \
--verbose
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="rules"&gt;Rules&lt;/h2&gt;
&lt;p&gt;Go to the dashboard of the domain &lt;code&gt;iany.me&lt;/code&gt; and go to the section &lt;code&gt;Rules&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="redirect-rules"&gt;Redirect Rules&lt;/h3&gt;
&lt;p&gt;I have added 3 redirect rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When incoming requests have a hostname &lt;code&gt;iany.me&lt;/code&gt; or &lt;code&gt;www.iany.me&lt;/code&gt;, redirect to &lt;code&gt;concat(&amp;quot;https://blog.iany.me&amp;quot;, http.request.uri.path)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Redirect &lt;code&gt;https://blog.iany.me/*/index.html&lt;/code&gt; to &lt;code&gt;https://blog.iany.me/${1}/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Redirect &lt;code&gt;http://*&lt;/code&gt; to &lt;code&gt;https://${1}&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Although the Redirect Rules manual says if multiple rules make the same modification, the last executed rule wins, I have to put rule 2 before 3.&lt;/p&gt;
&lt;h3 id="url-rewrite-rules"&gt;URL Rewrite Rules&lt;/h3&gt;
&lt;p&gt;I added 2 rewrite rules for &lt;code&gt;index.html&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When the request URL is &lt;code&gt;https://blog.iany.me&lt;/code&gt; or &lt;code&gt;https://blog.iany.me/&lt;/code&gt;, rewrite the URL path to &lt;code&gt;/index.html&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When the request URL matches &lt;code&gt;https://blog.iany.me/*/&lt;/code&gt;, rewrite the path to &lt;code&gt;${1}/index.html&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="cache-rules"&gt;Cache Rules&lt;/h3&gt;
&lt;p&gt;I added 2 cache rules for images and static content:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Set cache TTL to 1 month for &lt;code&gt;png&lt;/code&gt;, &lt;code&gt;jpg&lt;/code&gt;, &lt;code&gt;jpeg&lt;/code&gt;, and &lt;code&gt;svg&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;Set cache TTL to 1 year for files in &lt;code&gt;/js&lt;/code&gt;, &lt;code&gt;/fonts&lt;/code&gt;, &lt;code&gt;/css&lt;/code&gt;, and &lt;code&gt;/uploads&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/ci/">CI</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>Explain Atomic Cross-Chain Swaps by Herlihy</title><link>https://blog.iany.me/2025/09/explain-atomic-cross-chain-swaps-by-herlihy/</link><pubDate>Fri, 19 Sep 2025 22:26:46 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2025/09/explain-atomic-cross-chain-swaps-by-herlihy/</guid><description>&lt;p&gt;This article explains the paper [Herlihy, 2018]&lt;sup id="fnref:1"&gt;&lt;span class="footnote-ref" role="doc-noteref"&gt;1&lt;/span&gt;&lt;/sup&gt; in simple words. In the paper, Herlihy has introduced an effective atomic cross-chain swap protocol in order to exchange assets across multiple blockchains among multiple parties.&lt;/p&gt;
&lt;h2 id="model"&gt;Model&lt;/h2&gt;
&lt;p&gt;A swap can be modeled using a directed graph &lt;code&gt;$(V, A)$&lt;/code&gt;. &lt;code&gt;$V$&lt;/code&gt; is a set of involved users, and &lt;code&gt;$A$&lt;/code&gt; is a set of payments. Each payment is annotated as &lt;code&gt;$(u, v)$&lt;/code&gt; where &lt;code&gt;$u$&lt;/code&gt; is the payer and &lt;code&gt;$v$&lt;/code&gt; is the payee. The Atomic Swap Protocol ensures that when all users follow the protocol, if one payment succeeds, all other payments must succeed.&lt;/p&gt;
&lt;p&gt;For example, following model represents a swap that Alice sends a payment to Bob, and Bob sends a payment to Carol.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\begin{array}{rl}
V &amp;amp;= \{\mathrm{Alice}, \mathrm{Bob}, \mathrm{Carol}\} \\
A &amp;amp;= \{(\mathrm{Alice},\mathrm{Bob}), (\mathrm{Bob}, \mathrm{Carol})\}
\end{array}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Atomic-Swap-Simple-Model.excalidraw"
loading="lazy"
src="https://blog.iany.me/2025/09/explain-atomic-cross-chain-swaps-by-herlihy/atomic-swap-simple-model.excalidraw.svg" /&gt;
&lt;/figure&gt;
&lt;h2 id="simple-protocol"&gt;Simple Protocol&lt;/h2&gt;
&lt;p&gt;When all payments can be connected into a path, like the example above, the protocol is simple and is similar to the payment hops in lightning network.&lt;/p&gt;
&lt;p&gt;A path is annotated as &lt;code&gt;$(u_1, \ldots, u_l)$&lt;/code&gt;, which is consisted of &lt;code&gt;$l-1$&lt;/code&gt; payments: &lt;code&gt;$(u_1, u_2),(u_2, u_3),\ldots,(u_{l-1}, u_l)$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The final payee &lt;code&gt;$u_l$&lt;/code&gt; generates a secret &lt;code&gt;$s$&lt;/code&gt; and sends its hash &lt;code&gt;$H(s)$&lt;/code&gt; to the payer &lt;code&gt;$u_1$&lt;/code&gt;. The payer &lt;code&gt;$u_1$&lt;/code&gt; creates a Hash Time Locked Contract &lt;code&gt;$(H(s), 2(l-1)\Delta)$&lt;/code&gt; that either &lt;code&gt;$u_2$&lt;/code&gt; can get the fund by providing the secret &lt;code&gt;$s$&lt;/code&gt;, or &lt;code&gt;$u_1$&lt;/code&gt; can get the refund when time has elapsed &lt;code&gt;$2(l-1)\Delta$&lt;/code&gt; units. The users &lt;code&gt;$u_i$&lt;/code&gt; from &lt;code&gt;$u_2$&lt;/code&gt; to &lt;code&gt;$u_{l-2}$&lt;/code&gt; creates a Hash Time Locked Contract &lt;code&gt;$(H(s), 2(l-i)\Delta)$&lt;/code&gt; when they confirm that they have received the inbound contract. The payee &lt;code&gt;$u_l$&lt;/code&gt; can settle the deal by providing the secret &lt;code&gt;$s$&lt;/code&gt; to &lt;code&gt;$u_{l-1}$&lt;/code&gt;, and each user uses the received secret &lt;code&gt;$s$&lt;/code&gt; to settle their inbound contract.&lt;/p&gt;
&lt;p&gt;The simple protocol also works when payee and payer are the same user (&lt;code&gt;$u_1 = u_l$&lt;/code&gt;). From engineering points, it should work for most scenarios.&lt;/p&gt;
&lt;h2 id="arbitrary-connected-directed-graph"&gt;Arbitrary Connected Directed Graph&lt;/h2&gt;
&lt;p&gt;The paper also extends the swap protocol to arbitrary connected directed graph. A connected directed graph means for any two user &lt;code&gt;$u, v$&lt;/code&gt;, there are a payment path from &lt;code&gt;$u$&lt;/code&gt; to &lt;code&gt;$v$&lt;/code&gt;, and a payment path from &lt;code&gt;$v$&lt;/code&gt; to &lt;code&gt;$u$&lt;/code&gt;. Such directed graphs contain loops, as in the example below:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\begin{array}{rl}
V = \{&amp;amp;A, B, C, D, E, F\} \\
A = \{ \\
&amp;amp;(A, B), (B, C), (C, A), \\
&amp;amp;(C, D), (D, B), \\
&amp;amp;(D, E), (E, F), (F, D) \\
\} \\
\end{array}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Atomic-Swap-Complex-Connected-Graph.excalidraw"
loading="lazy"
src="https://blog.iany.me/2025/09/explain-atomic-cross-chain-swaps-by-herlihy/atomic-swap-complex-connected-graph.excalidraw.svg" /&gt;
&lt;/figure&gt;
&lt;h3 id="leaders-selection"&gt;Leaders Selection&lt;/h3&gt;
&lt;p&gt;The protocol must select a Feedback Vertex Set of the directed graph as leaders. A feedback vertex set is a subset of &lt;code&gt;$V$&lt;/code&gt; that once removed from the graph, there will be no loops in the graph. For example, &lt;code&gt;$(B, D)$&lt;/code&gt; is a feedback vertex set of the example above. When there are multiple candidates, choose an arbitrary set.&lt;/p&gt;
&lt;p&gt;The leaders generate secrets and publish their hashes. If &lt;code&gt;$(B, D)$&lt;/code&gt; are selected, they should publish &lt;code&gt;$(H(s_B), H(s_D))$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The protocol then move forward in two phases.&lt;/p&gt;
&lt;h3 id="phase-one"&gt;Phase One&lt;/h3&gt;
&lt;p&gt;In the first phase, users offer outbound contracts to counterparty for each payment.&lt;/p&gt;
&lt;p&gt;Leaders must offer contracts first:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Publish a contract on every outbound payments, then&lt;/li&gt;
&lt;li&gt;wait until all inbound contracts have been published.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Followers must confirm inbound contracts first:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Wait until correct inbound contracts have been published, then&lt;/li&gt;
&lt;li&gt;publish a contract on every outbound payments.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The contracts are Hash Time Locked Contracts &lt;code&gt;$(h, t)$&lt;/code&gt; that payee can get the fund using the proof to unlock &lt;code&gt;$h$&lt;/code&gt;, or the payer gets the refund after timeout &lt;code&gt;$t$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For a payment &lt;code&gt;$(u, v)$&lt;/code&gt;, the proof of &lt;code&gt;$h$&lt;/code&gt; is a triple &lt;code&gt;$(s, p, \sigma)$&lt;/code&gt; where&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$p$&lt;/code&gt; is any payment path &lt;code&gt;$(u_0, \ldots, u_k)$&lt;/code&gt; starting from the payee &lt;code&gt;$v$&lt;/code&gt; (&lt;code&gt;$u_0 = v$&lt;/code&gt;) to a leader &lt;code&gt;$u_k$&lt;/code&gt;. The path &lt;code&gt;$p$&lt;/code&gt; can have a length of 0 if &lt;code&gt;$v$&lt;/code&gt; itself is a leader.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$s$&lt;/code&gt; is the secret generated by the ending leader of the path &lt;code&gt;$p$&lt;/code&gt;, and &lt;code&gt;$h = H(s)$&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$\sigma$&lt;/code&gt; is a nested signatures of &lt;code&gt;$s$&lt;/code&gt; signed by users in the path &lt;code&gt;$p$&lt;/code&gt;.
&lt;ul&gt;
&lt;li&gt;The innest signature is &lt;code&gt;$\mathrm{sig}_k = \mathit{sig}(s, u_k)$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;For user &lt;code&gt;$u_i$&lt;/code&gt; (&lt;code&gt;$0 \leq i \leq k - 1$&lt;/code&gt;), the signature is &lt;code&gt;$\mathrm{sig}_i = \mathit{sig}(\mathrm{sig}_{i+1}, u_i)$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$\sigma = \mathrm{sig}_0$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The hash lock &lt;code&gt;$h$&lt;/code&gt; can be set to &lt;code&gt;$H(s)$&lt;/code&gt; for the shortest &lt;code&gt;$p$&lt;/code&gt;, or just the hashes of all secrets to all unlocking using arbitrary ending leader of the path &lt;code&gt;$p$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The time lock &lt;code&gt;$t$&lt;/code&gt; is &lt;code&gt;$(\mathit{diam}(\mathcal{D}) + |p|)\Delta$&lt;/code&gt;, where&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$\mathit{diam}(\mathcal{D})$&lt;/code&gt; is the longest length of a payment path without duplicated users in the directed graph.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$|p|$&lt;/code&gt; is the length of the path &lt;code&gt;$p$&lt;/code&gt; in the hash lock.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$\Delta$&lt;/code&gt; is a configured time unit constant for the time out.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;rsquo;s see two examples.&lt;/p&gt;
&lt;p&gt;The first example is the payment from a leader: &lt;code&gt;$(B, C)$&lt;/code&gt;.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Atomic-Swap-Complex-Connected-Graph-B-C-Contract.excalidraw"
loading="lazy"
src="https://blog.iany.me/2025/09/explain-atomic-cross-chain-swaps-by-herlihy/atomic-swap-complex-connected-graph-b-c-contract.excalidraw.svg" /&gt;
&lt;/figure&gt;
&lt;p&gt;There are three candidate path &lt;code&gt;$p$&lt;/code&gt; from the payee &lt;code&gt;$C$&lt;/code&gt; to a leader either &lt;code&gt;$B$&lt;/code&gt; or &lt;code&gt;$D$&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;$(C, A, B)$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$(C, D)$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$(C, D, B)$&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;$B$&lt;/code&gt; can choose any one, but the shortest one is recommended. Let&amp;rsquo;s use &lt;code&gt;$(C, D)$&lt;/code&gt;. &lt;code&gt;$B$&lt;/code&gt; can publish the contract because he/she is the leader.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The hash lock for &lt;code&gt;$(B, C)$&lt;/code&gt; is &lt;code&gt;$H(s_D)$&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The time lock is &lt;code&gt;$6\Delta$&lt;/code&gt;, because the length of the longest path &lt;code&gt;$(A, B, C, D, E, F)$&lt;/code&gt; is 5, and the length of &lt;code&gt;$(C, D)$&lt;/code&gt; is 1.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The second example is the payment from a follower: &lt;code&gt;$(C, A)$&lt;/code&gt;.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Atomic-Swap-Complex-Connected-Graph-C-a-Contract.excalidraw"
loading="lazy"
src="https://blog.iany.me/2025/09/explain-atomic-cross-chain-swaps-by-herlihy/atomic-swap-complex-connected-graph-c-a-contract.excalidraw.svg" /&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;code&gt;$C$&lt;/code&gt; can choose the path &lt;code&gt;$(A, B)$&lt;/code&gt; and publish the contract:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The hash lock is &lt;code&gt;$H(s_B)$&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The time lock is &lt;code&gt;$6\Delta$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="phase-two"&gt;Phase Two&lt;/h3&gt;
&lt;p&gt;In phase two, leaders can unlock the inbound contracts because they know all secrets. For example, the leader &lt;code&gt;$B$&lt;/code&gt; can unlock the inbound payment &lt;code&gt;$(A, B)$&lt;/code&gt; by offering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The path &lt;code&gt;$p = (B)$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The secret &lt;code&gt;$s_B$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The signature &lt;code&gt;$sig(s_B, B)$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Followers must trace outbound contracts unlocking events. For example, &lt;code&gt;$A$&lt;/code&gt; can unlock the contract for &lt;code&gt;$(C, A)$&lt;/code&gt; after receiving the unlocking event above:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The path &lt;code&gt;$p = (A, B)$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The secret &lt;code&gt;$s_B$&lt;/code&gt; since B has published it when unlocking &lt;code&gt;$(A, B)$&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The signature &lt;code&gt;$sig(sig(s_B, B), A)$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Herlihy, M. (2018). Atomic Cross-Chain Swaps (No. arXiv:1801.09515). arXiv. &lt;a href="https://doi.org/10.48550/arXiv.1801.09515"&gt;https://doi.org/10.48550/arXiv.1801.09515&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/blockchain/">Blockchain</category><category domain="https://blog.iany.me/tags/cryptography/">Cryptography</category><category domain="https://blog.iany.me/tags/distributed-system/">Distributed System</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>Envrc Alternative for PowerShell in Windows</title><link>https://blog.iany.me/2024/04/envrc-alternative-for-powershell-in-windows/</link><pubDate>Sat, 06 Apr 2024 08:55:34 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2024/04/envrc-alternative-for-powershell-in-windows/</guid><description>&lt;p&gt;This post introduces a solution for automatically setting up and tearing down shell environments for PowerShell in Windows. It is proposed as a potential alternative to the bash-based tool &lt;a href="https://github.com/direnv/direnv"&gt;direnv&lt;/a&gt;, which, while effective at loading &lt;code&gt;.envrc&lt;/code&gt; files in the current or nearest ancestor directory, has limited compatibility with PowerShell in Windows.&lt;/p&gt;
&lt;h2 id="envrcps1"&gt;.envrc.ps1&lt;/h2&gt;
&lt;p&gt;The basic idea is creating a file &lt;code&gt;.envrc.ps1&lt;/code&gt; with two entries: setup and teardown. The setup entry is responsible for setting up the environment up entering the directory, and the teardown entry is used when leaving it.&lt;/p&gt;
&lt;p&gt;PowerShell allows code to be executed from a file using the dot command. This means that the entire file can be used as the setup entry. For the teardown entry, a function can be defined within the same file, and I use the function name &lt;code&gt;down&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The layout of the file &lt;code&gt;.envrc.ps1&lt;/code&gt; looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;# setup
$env:GH_TOKEN = &amp;quot;secret&amp;quot;
# teardown
function global:down {
$env:GH_TOKEN = &amp;quot;&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;global:&lt;/code&gt; scope before the function name is necessary when this file is loaded in the hook, as I will do later.&lt;/p&gt;
&lt;p&gt;The corresponding commands for setup and teardown entries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Setup: &lt;code&gt;. .envrc.ps1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Teardown: &lt;code&gt;down&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="hook"&gt;Hook&lt;/h2&gt;
&lt;p&gt;To hook the setup and teardown entries, I reference &lt;a href="https://github.com/PowerShell/PowerShell/issues/14484#issuecomment-1731647083"&gt;this example&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;using namespace System;
using namespace System.Management.Automation;
$hook = [EventHandler[LocationChangedEventArgs]] {
param([object] $source, [LocationChangedEventArgs] $eventArgs)
end {
# 1. `down` for $eventArgs.OldPath
# 2. `. .envrc.ps1` for $eventArgs.NewPath
}
};
$currentAction = $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction;
if ($currentAction) {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = [Delegate]::Combine($currentAction, $hook);
} else {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = $hook;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run the code snippet, PowerShell 7 or later is required. You can install it from the &lt;a href="https://github.com/PowerShell/PowerShell"&gt;repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here is the algorithm for identifying setup and teardown entries:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find the nearest &lt;code&gt;.envrc.ps1&lt;/code&gt; for the old path.&lt;/li&gt;
&lt;li&gt;Find the nearest &lt;code&gt;.envrc.ps1&lt;/code&gt; for the new path.&lt;/li&gt;
&lt;li&gt;If the two paths are the same, skip the next step.&lt;/li&gt;
&lt;li&gt;If they differ, invoke the &lt;code&gt;down&lt;/code&gt; function and remove it if it exists. Then, if found, load the &lt;code&gt;.envrc.ps1&lt;/code&gt; file for the new path.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The full code example to be added into PowerShell &lt;code&gt;$PROFILE&lt;/code&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;using namespace System;
using namespace System.Management.Automation;
# existing PowerShell profile contents
# skip when version is less than 7
if ($PSVersionTable.PSVersion.Major -lt 7) {
return
}
# find the .envrc.ps1 in the currenct directory or the nearest ancestor directory.
function Find-NearestEnvrc {
param (
[Parameter(Mandatory=$true)]
[string]$StartDir
)
$currentDir = Resolve-Path $StartDir
while ($currentDir -ne &amp;quot;&amp;quot;) {
$envrcPath = Join-Path $currentDir &amp;quot;.envrc.ps1&amp;quot;
if (Test-Path $envrcPath) {
return $envrcPath
}
$currentDir = Split-Path $currentDir -Parent
}
return &amp;quot;&amp;quot;
}
# hook the setup and teardown entries
$hook = [EventHandler[LocationChangedEventArgs]] {
param([object] $source, [LocationChangedEventArgs] $eventArgs)
end {
$oldEnvrc = Find-NearestEnvrc $eventArgs.OldPath
$newEnvrc = Find-NearestEnvrc $eventArgs.NewPath
if ($oldEnvrc -ne $newEnvrc) {
Get-Command down -ErrorAction SilentlyContinu
if (Get-Command down -ErrorAction SilentlyContinu) {
down
Remove-Item Function:down
}
if ($newEnvrc -ne &amp;quot;&amp;quot;) {
. $newEnvrc
}
}
}
};
$currentAction = $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction;
if ($currentAction) {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = [Delegate]::Combine($currentAction, $hook);
} else {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = $hook;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="example-usage"&gt;Example Usage&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;# setup
# set environment variable
$env:GH_TOKEN = &amp;quot;secret&amp;quot;
# load python virtual env
. .venv\Scripts\Activate.ps1
# add a helper function
function global:build {
cargo build
}
# teardown
function global:down {
# unset environment variable
$env:GH_TOKEN = &amp;quot;&amp;quot;
# unload python virtual env
deactivate
# remove the helper function
Remove-Item Function:build
}
&lt;/code&gt;&lt;/pre&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/environment-variables/">Environment Variables</category><category domain="https://blog.iany.me/tags/powershell/">PowerShell</category><category domain="https://blog.iany.me/tags/windows/">Windows</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>How to Verify JoyID WebAuthn Signature</title><link>https://blog.iany.me/2023/12/how-to-verify-joyid-webauthn-signature/</link><pubDate>Sun, 17 Dec 2023 17:35:33 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2023/12/how-to-verify-joyid-webauthn-signature/</guid><description>&lt;p&gt;&lt;a href="https://docs.joy.id/guide"&gt;JoyID&lt;/a&gt; is a multichain, cross-platform, passwordless and mnemonic-free wallet solution based on FIDO WebAuthn protocol and Nervos CKB.&lt;/p&gt;
&lt;p&gt;This post shows how to verify the signature from the method &lt;a href="https://docs.joy.id/guide/ckb/sign-message"&gt;signChallenge&lt;/a&gt; of the &lt;code&gt;@joyid/ckb&lt;/code&gt; package. The method reference page has a demo. I use the demo to obtain an example response then verify the response using the OpenSSL command line and the Python library &lt;a href="https://pycryptodome.readthedocs.io/en/latest/src/introduction.html"&gt;PyCryptodome&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The JoyID follows the WebAuthn specification and employs secp256r1 for signing. Although the guide references &lt;a href="https://www.w3.org/TR/webauthn-2/#sctn-op-get-assertion"&gt;section 6.3.3&lt;/a&gt; of the WebAuthn specification, titled &amp;ldquo;The authenticatorGetAssertion Operation&amp;rdquo;, I discovered that the example in &lt;a href="https://github.com/duo-labs/py_webauthn/blob/master/webauthn/authentication/verify_authentication_response.py"&gt;this repository&lt;/a&gt; provided me much more helps.&lt;/p&gt;
&lt;h2 id="the-response-parsing"&gt;The Response Parsing&lt;/h2&gt;
&lt;p&gt;This is the example I obtained from the demo.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
&amp;quot;signature&amp;quot;: &amp;quot;MEUCICF25qdO6nLreEoBHnyaw-9R6XFHbIu-NwsAI53t016qAiEAgmhlwTEMxoWxKj79R1rUkB_6nrhJfws82DqHkY_HnqQ&amp;quot;,
&amp;quot;message&amp;quot;: &amp;quot;K4sF4fAwPvuJj-TW3mARmMenuGSrvmohxzsueH4YfFIFAAAAAHsidHlwZSI6IndlYmF1dGhuLmdldCIsImNoYWxsZW5nZSI6IlUybG5iaUIwYUdseklHWnZjaUJ0WlEiLCJvcmlnaW4iOiJodHRwczovL3Rlc3RuZXQuam95aWQuZGV2IiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0&amp;quot;,
&amp;quot;challenge&amp;quot;: &amp;quot;Sign this for me&amp;quot;,
&amp;quot;alg&amp;quot;: -7,
&amp;quot;pubkey&amp;quot;: &amp;quot;3538dfd53ad93d2e0a6e7f470295dcd71057d825e1f87229e5afe2a906aa7cfc099fdfa04442dac33548b6988af8af58d2052529088f7b73ef00800f7fbcddb3&amp;quot;,
&amp;quot;keyType&amp;quot;: &amp;quot;main_key&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="pubkey"&gt;pubkey&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;pubkey&lt;/code&gt; field represents the uncompressed public key concatenating two 32-byte integers in hex. PyCryptodome can import the key by prepending the flag &lt;code&gt;0x04&lt;/code&gt;. OpenSSL uses PEM to encode keys, and PyCryptodome can help here to export the key in PEM format.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;from Crypto.PublicKey import ECC
pubkey_raw_hex = &amp;quot;3538dfd53ad93d2e0a6e7f470295dcd71057d825e1f87229e5afe2a906aa7cfc099fdfa04442dac33548b6988af8af58d2052529088f7b73ef00800f7fbcddb3&amp;quot;
pubkey = ECC.import_key(bytes.fromhex(&amp;quot;04&amp;quot; + pubkey_raw_hex), curve_name=&amp;quot;secp256r1&amp;quot;)
with open(&amp;quot;pubkey.pem&amp;quot;, &amp;quot;wt&amp;quot;) as pemfile:
pemfile.write(pubkey.export_key(format=&amp;quot;PEM&amp;quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Double check the key using OpenSSL:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell-session"&gt;$ openssl ec -text -inform PEM -in pubkey.pem -pubin
...
Public-Key: (256 bit)
pub:
04:35:38:df:d5:3a:d9:3d:2e:0a:6e:7f:47:02:95:
dc:d7:10:57:d8:25:e1:f8:72:29:e5:af:e2:a9:06:
aa:7c:fc:09:9f:df:a0:44:42:da:c3:35:48:b6:98:
8a:f8:af:58:d2:05:25:29:08:8f:7b:73:ef:00:80:
0f:7f:bc:dd:b3
ASN1 OID: prime256v1
NIST CURVE: P-256
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="message"&gt;message&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;message&lt;/code&gt; is a binary encoded by base64 &lt;a href="https://datatracker.ietf.org/doc/html/rfc4648#section-5"&gt;RFC 4648 §5&lt;/a&gt; without the equal sign (&lt;code&gt;=&lt;/code&gt;) paddings. Many base64 tools and libraries require padding equal sign (&lt;code&gt;=&lt;/code&gt;) in the end of the string to make the length multiple of 4. The &lt;code&gt;message&lt;/code&gt; in the example response has a length 351, which requires one &lt;code&gt;=&lt;/code&gt; padding. A trick is always padding two equals at the end of the string before decoding.&lt;/p&gt;
&lt;p&gt;The first 37 bytes in &lt;code&gt;message&lt;/code&gt; are authenticator data, and the following bytes are client data in JSON.&lt;/p&gt;
&lt;p&gt;The section &lt;a href="https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data"&gt;section 6.1&lt;/a&gt; in the WebAuthn specification defines the layout of the authenticator data.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rpIdHash&lt;/code&gt;, 32 bytes: the sha256 checksum of the text &lt;code&gt;testnet.joyid.dev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flags&lt;/code&gt;, 1 byte: &lt;code&gt;0x05&lt;/code&gt; in JoyID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;signCount&lt;/code&gt;, 4 bytes: all zeros&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;base64 -d &amp;lt;&amp;lt;&amp;lt;'
K4sF4fAwPvuJj-TW3mARmMenuGSrvmohxzsueH4YfFIFAAAAAHsidHlwZSI6Indl
YmF1dGhuLmdldCIsImNoYWxsZW5nZSI6IlUybG5iaUIwYUdseklHWnZjaUJ0WlEi
LCJvcmlnaW4iOiJodHRwczovL3Rlc3RuZXQuam95aWQuZGV2IiwiY3Jvc3NPcmln
aW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90
IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUg
aHR0cHM6Ly9nb28uZ2wveWFiUGV4In0=' |
dd bs=1 count=37 2&amp;gt;/dev/null |
xxd
#=&amp;gt; 00000000: 2b8b 05e1 f030 3efb 898f e4d6 de60 1198
#=&amp;gt; 00000010: c7a7 b864 abbe 6a21 c73b 2e78 7e18 7c52
#=&amp;gt; 00000020: 0500 0000 00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check the first two lines with the sha256 checksum:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;echo -n 'testnet.joyid.dev' | sha256sum
#=&amp;gt; 2b8b05e1f0303efb898fe4d6de601198c7a7b864abbe6a21c73b2e787e187c52 -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The client data JSON looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;base64 -d &amp;lt;&amp;lt;&amp;lt;'
K4sF4fAwPvuJj-TW3mARmMenuGSrvmohxzsueH4YfFIFAAAAAHsidHlwZSI6Indl
YmF1dGhuLmdldCIsImNoYWxsZW5nZSI6IlUybG5iaUIwYUdseklHWnZjaUJ0WlEi
LCJvcmlnaW4iOiJodHRwczovL3Rlc3RuZXQuam95aWQuZGV2IiwiY3Jvc3NPcmln
aW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90
IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUg
aHR0cHM6Ly9nb28uZ2wveWFiUGV4In0=' |
dd bs=1 skip=37 2&amp;gt;/dev/null |
jq
{
&amp;quot;type&amp;quot;: &amp;quot;webauthn.get&amp;quot;,
&amp;quot;challenge&amp;quot;: &amp;quot;U2lnbiB0aGlzIGZvciBtZQ&amp;quot;,
&amp;quot;origin&amp;quot;: &amp;quot;https://testnet.joyid.dev&amp;quot;,
&amp;quot;crossOrigin&amp;quot;: false,
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the &lt;code&gt;challenge&lt;/code&gt; field. It is the parameter passed to &lt;code&gt;signChallenge&lt;/code&gt;, in base64.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;base64 -d &amp;lt;&amp;lt;&amp;lt;'U2lnbiB0aGlzIGZvciBtZQ=='
#=&amp;gt; Sign this for me
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Attention that message is not the binary to be signed. According to the Figure 4, Generating an assertion signature, in &lt;a href="https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data"&gt;the WebAuthn specification&lt;/a&gt;, the binary to be signed is a concatenation of the authenticator data and the sha256 checksum of the client data JSON.&lt;/p&gt;
&lt;p&gt;The following code shows how to prepare the message to sign and save it into the file &lt;code&gt;message.bin&lt;/code&gt;. Attention that base64 must use the alternative keys &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt; to replace &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt; respectively.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
Attention
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
To decode base64 &amp;ldquo;RFC 4648 §5&amp;rdquo; in python, use either &lt;code&gt;base64.b64decode(s, altchars=&amp;quot;-_&amp;quot;)&lt;/code&gt; or &lt;code&gt;binascii.urlsafe_b64decode(s)&lt;/code&gt;.
&lt;/div&gt;
&lt;/details&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import base64
from Crypto.Hash import SHA256
message_bin = base64.urlsafe_b64decode(
&amp;quot;K4sF4fAwPvuJj-TW3mARmMenuGSrvmohxzsueH4YfFIFAAAAAHsidHlwZSI6Indl&amp;quot;
&amp;quot;YmF1dGhuLmdldCIsImNoYWxsZW5nZSI6IlUybG5iaUIwYUdseklHWnZjaUJ0WlEi&amp;quot;
&amp;quot;LCJvcmlnaW4iOiJodHRwczovL3Rlc3RuZXQuam95aWQuZGV2IiwiY3Jvc3NPcmln&amp;quot;
&amp;quot;aW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90&amp;quot;
&amp;quot;IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUg&amp;quot;
&amp;quot;aHR0cHM6Ly9nb28uZ2wveWFiUGV4In0==&amp;quot;,
)
authenticator_data = message_bin[:37]
client_data = message_bin[37:]
message_to_sign = authenticator_data + SHA256.new(client_data).digest()
with open(&amp;quot;message.bin&amp;quot;, &amp;quot;wb&amp;quot;) as fout:
fout.write(message_to_sign)
&lt;/code&gt;&lt;/pre&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
Attention
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
The &lt;code&gt;message&lt;/code&gt; in the response is not the binary to be signed. Instead, the binary to be signed is a concatenation of the authenticator data and the sha256 checksum of the client data JSON.
&lt;/div&gt;
&lt;/details&gt;
&lt;h3 id="signature"&gt;signature&lt;/h3&gt;
&lt;p&gt;The field signature are two 32-byte integers first encoded in &lt;a href="https://wiki.openssl.org/index.php/DER"&gt;DER&lt;/a&gt;, then base64 &lt;a href="https://datatracker.ietf.org/doc/html/rfc4648#section-5"&gt;RFC 4648 §5&lt;/a&gt; without the equal sign (&lt;code&gt;=&lt;/code&gt;) paddings.&lt;/p&gt;
&lt;p&gt;Many base64 tools and libraries require padding equal sign (&lt;code&gt;=&lt;/code&gt;) in the end of the string to make the length multiple of 4. The signature in the example response has a length 95, which requires one &lt;code&gt;=&lt;/code&gt; padding.&lt;/p&gt;
&lt;p&gt;OpenSSL also stores signature in DER, let&amp;rsquo;s save one in the file &lt;code&gt;signature.der&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;base64 -d &amp;lt;&amp;lt;&amp;lt;&amp;quot;MEUCICF25qdO6nLreEoBHnyaw-9R6XFHbIu-NwsAI53t016qAiEAgmhlwTEMxoWxKj79R1rUkB_6nrhJfws82DqHkY_HnqQ=&amp;quot; &amp;gt; signature.der
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The command &lt;code&gt;openssl asn1parse&lt;/code&gt; can parse the file &lt;code&gt;signature.der&lt;/code&gt; in the DER format.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;openssl asn1parse -dump -inform DER -in signature.der
# Output =&amp;gt;
# 0:d=0 hl=2 l= 69 cons: SEQUENCE
# 2:d=1 hl=2 l= 32 prim: INTEGER :2176E6A74EEA72EB784A011E7C9AC3EF51E971476C8BBE370B00239DEDD35EAA
# 36:d=1 hl=2 l= 33 prim: INTEGER :826865C1310CC685B12A3EFD475AD4901FFA9EB8497F0B3CD83A87918FC79EA4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PyCryptodome expects the signature of 64 bytes for two 32-byte integers. Following code uses the &lt;code&gt;asn1&lt;/code&gt; module to extract the raw signature from the DER binary.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import base64
from Crypto.Util.asn1 import DerSequence
signature_der = base64.urlsafe_b64decode(
&amp;quot;MEUCICF25qdO6nLreEoBHnyaw-9R6XFHbIu-NwsAI53t016qAiEAgmhlwTEMxoWx&amp;quot;
&amp;quot;Kj79R1rUkB_6nrhJfws82DqHkY_HnqQ=&amp;quot;,
)
signature_seq = DerSequence()
signature_seq.decode(signature_der)
print(signature_seq[0].to_bytes(32).hex())
# =&amp;gt; 2176e6a74eea72eb784a011e7c9ac3ef51e971476c8bbe370b00239dedd35eaa
print(signature_seq[1].to_bytes(32).hex())
# =&amp;gt; 826865c1310cc685b12a3efd475ad4901ffa9eb8497f0b3cd83a87918fc79ea4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="verifying"&gt;Verifying&lt;/h2&gt;
&lt;p&gt;PyCryptodome:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;from Crypto.Hash import SHA256
from Crypto.Signature import DSS
DSS.new(pubkey, &amp;quot;fips-186-3&amp;quot;).verify(SHA256.new(message_to_sign), signature)
print(&amp;quot;Verified OK&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OpenSSL:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;openssl dgst -sha256 -verify pubkey.pem -signature signature.der message.bin
&lt;/code&gt;&lt;/pre&gt;
&lt;details class="kg-card kg-callout kg-callout-code" data-callout-type="code"&gt;
&lt;summary class="kg-callout-title"&gt;
&lt;i class="fas fa-chevron-right"&gt;&lt;/i&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-code"&gt;&lt;/i&gt;
Full Python code (&lt;a href="https://gist.github.com/doitian/b1f5c60203e9dbaffccff7d0920d9529"&gt;Gist&lt;/a&gt;)
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import base64
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS
from Crypto.Util.asn1 import DerSequence
response = {
&amp;quot;signature&amp;quot;: &amp;quot;MEUCICF25qdO6nLreEoBHnyaw-9R6XFHbIu-NwsAI53t016qAiEAgmhlwTEMxoWx&amp;quot;
&amp;quot;Kj79R1rUkB_6nrhJfws82DqHkY_HnqQ&amp;quot;,
&amp;quot;message&amp;quot;: &amp;quot;K4sF4fAwPvuJj-TW3mARmMenuGSrvmohxzsueH4YfFIFAAAAAHsidHlwZSI6IndlYmF1dGhuLmdldCIsImNoYWxsZW5nZSI6IlUybG5iaUIwYUdseklHWnZjaUJ0WlEiLCJvcmlnaW4iOiJodHRwczovL3Rlc3RuZXQuam95aWQuZGV2IiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0&amp;quot;,
&amp;quot;challenge&amp;quot;: &amp;quot;Sign this for me&amp;quot;,
&amp;quot;alg&amp;quot;: -7,
&amp;quot;pubkey&amp;quot;: &amp;quot;3538dfd53ad93d2e0a6e7f470295dcd71057d825e1f87229e5afe2a906aa7cfc099fdfa04442dac33548b6988af8af58d2052529088f7b73ef00800f7fbcddb3&amp;quot;,
&amp;quot;keyType&amp;quot;: &amp;quot;main_key&amp;quot;,
}
pubkey = ECC.import_key(
bytes.fromhex(&amp;quot;04&amp;quot; + response[&amp;quot;pubkey&amp;quot;]),
curve_name=&amp;quot;secp256r1&amp;quot;,
)
with open(&amp;quot;pubkey.pem&amp;quot;, &amp;quot;wt&amp;quot;) as fout:
fout.write(pubkey.export_key(format=&amp;quot;PEM&amp;quot;))
message_bin = base64.urlsafe_b64decode(response[&amp;quot;message&amp;quot;] + &amp;quot;==&amp;quot;)
authenticator_data = message_bin[:37]
client_data = message_bin[37:]
# https://github.com/duo-labs/py_webauthn/blob/master/webauthn/authentication/verify_authentication_response.py
message_to_sign = authenticator_data + SHA256.new(client_data).digest()
with open(&amp;quot;message.bin&amp;quot;, &amp;quot;wb&amp;quot;) as fout:
fout.write(message_to_sign)
signature_der = base64.urlsafe_b64decode(response[&amp;quot;signature&amp;quot;] + &amp;quot;==&amp;quot;)
with open(&amp;quot;signature.der&amp;quot;, &amp;quot;wb&amp;quot;) as fout:
fout.write(signature_der)
signature_seq = DerSequence()
signature_seq.decode(signature_der)
signature = signature_seq[0].to_bytes(32) + signature_seq[1].to_bytes(32)
DSS.new(pubkey, &amp;quot;fips-186-3&amp;quot;).verify(SHA256.new(message_to_sign), signature)
print(&amp;quot;Verified OK&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/cryptography/">Cryptography</category><category domain="https://blog.iany.me/tags/javascript/">JavaScript</category><category domain="https://blog.iany.me/tags/webauthn/">Webauthn</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>Renaming Browser Tab Names</title><link>https://blog.iany.me/2023/07/renaming-browser-tab-names/</link><pubDate>Fri, 21 Jul 2023 20:31:59 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2023/07/renaming-browser-tab-names/</guid><description>&lt;p&gt;Renaming browser tab names may seem like a simple task, but it can actually be quite challenging.&lt;/p&gt;
&lt;p&gt;Most browsers sync the tab name with the web page title. Therefore, it seems simple to set tab name by setting &lt;code&gt;document.title&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;document.title = &amp;quot;Custom Tab Name&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, many web pages use JavaScript to alter the page title, which would override the custom name. While &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty"&gt;&lt;code&gt;Object.defineProperty&lt;/code&gt;&lt;/a&gt; in JavaScript can override the property setter function, it&amp;rsquo;s not possible to access the original setter function for &lt;code&gt;document.title&lt;/code&gt;. Fortunately, browsers sync page titles with the first &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; tag, so setting its content acts as a workaround to set the page title.&lt;/p&gt;
&lt;p&gt;Here is the bookmark for renaming a tab. I also add &lt;code&gt;%t&lt;/code&gt; as the token for the original page tab title. This is handy such as adding a custom prefix with &lt;code&gt;Work: %t&lt;/code&gt;, or restoring the original title without reloading the page by using &lt;code&gt;%t&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;// Rename the document title using a bookmarklet.
//
// As a user,
// - Once I have renamed the tab, it should not be overwritten.
// - I can include the token %t as placeholder for the real title.
// - To restore, I can rename the tab to %t
//
// Install: Copy the code to
//
// https://caiorss.github.io/bookmarklet-maker/
//
// and generate the bookmarklet.
// Init only once
if (!(&amp;quot;_renameTitle&amp;quot; in document)) {
// Force creating the title tag.
document.title = document.title || &amp;quot;&amp;quot;;
const titleEl = document.getElementsByTagName(&amp;quot;title&amp;quot;)[0];
const titleTokenRegex = /%t/g;
// Remembers the real title
let titleWithoutRenaming = document.title;
// User set title
let titleWithRenaming = &amp;quot;&amp;quot;;
// Rename the document title to v.
// If v contains the token %t, replace all occurences to the
// real title.
document._renameTitle = (v) =&amp;gt; {
titleWithRenaming = v;
titleEl.innerText = v.replace(titleTokenRegex, titleWithoutRenaming);
};
Object.defineProperty(document, &amp;quot;title&amp;quot;, {
// Other code will use document.title setter to change the title.
//
// Remember the new value as the real title but still use the
// title set by user.
set: (v) =&amp;gt; {
titleWithoutRenaming = v;
// Once document has defined the title property, its value is
// not synchronized to the tab name.
//
// Here uses a workaround to set the title tag content.
titleEl.innerText = titleWithRenaming.replace(titleTokenRegex, v);
},
get: () =&amp;gt; titleEl.innerText,
});
}
const title = prompt(
&amp;quot;Rename tab (Use token %t for original title)&amp;quot;,
document.title
);
// title is null when user cancel the dialog.
if (title !== null) {
document._renameTitle(title);
}
&lt;/code&gt;&lt;/pre&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/browser/">Browser</category><category domain="https://blog.iany.me/tags/javascript/">JavaScript</category><category domain="https://blog.iany.me/tags/productivity/">Productivity</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>Transforming Markdown to Attractive PDFs: A Guide to Using Pandoc with Xelatex</title><link>https://blog.iany.me/2023/04/transforming-markdown-to-attractive-pdfs-a-guide-to-using-pandoc-with-xelatex/</link><pubDate>Sat, 29 Apr 2023 21:43:02 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2023/04/transforming-markdown-to-attractive-pdfs-a-guide-to-using-pandoc-with-xelatex/</guid><description>&lt;p&gt;I used to read lengthy and complex articles in the PDF format. Despite the abundance of PDF exporting options, the resulting files often appear unappealing. However, there is a solution. With the pandoc tool and the xelatex backend, you can transform Markdown files into aesthetically pleasing PDFs. In this tutorial, I will guide you through the steps of using pandoc with xelatex and share the lessons I&amp;rsquo;ve learned.&lt;/p&gt;
&lt;h2 id="installation"&gt;Installation&lt;/h2&gt;
&lt;p&gt;Ubuntu users can install the package &lt;code&gt;texlive-xetex&lt;/code&gt; and &lt;code&gt;pandoc&lt;/code&gt;, In macOS, I recommend a minimal installation:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;brew install librsvg pandoc
brew install --cask basictex
sudo tlmgr update --self
sudo tlmgr update --all
for pkg in texliveonfly xelatex adjustbox tcolorbox collectbox ucs environ \
trimspaces titling enumitem rsfs xecjk fvextra svg transparent; do
sudo tlmgr install $pkg
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The library &lt;code&gt;librsvg&lt;/code&gt; is required to support SVG images.&lt;/p&gt;
&lt;p&gt;Verify the installation by converting a simple markdown file.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;echo &amp;quot;# Hello World&amp;quot; &amp;gt; test.md
pandoc --pdf-engine=xelatex -i test.md -o test.pdf
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="tips"&gt;Tips&lt;/h2&gt;
&lt;p&gt;In LaTeX, the document class serves as the template for the basic page styles.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m a fan of &lt;a href="https://ctan.org/pkg/koma-script?lang=en"&gt;koma-script&lt;/a&gt;, and I will choose between &lt;em&gt;scrreprt&lt;/em&gt; and &lt;em&gt;scrbook&lt;/em&gt; depending whether the file to create is more like a report or a book. I recommend try them and the standard classes like &lt;em&gt;report&lt;/em&gt; and &lt;em&gt;book&lt;/em&gt;, then choose the one that best suites your preferences.&lt;/p&gt;
&lt;p&gt;To customize the document class in pandoc, simply set the variable &lt;code&gt;documentclass&lt;/code&gt;. Other &lt;a href="https://pandoc.org/MANUAL.html#variables-for-latex"&gt;variables&lt;/a&gt; are available for controlling the LaTeX style. I use some of them to define my default style.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;DOCUMENTCLASS=&amp;quot;${DOCUMENTCLASS:-scrreprt}&amp;quot;
MAINFONT=&amp;quot;${MAINFONT:-Helvetica}&amp;quot;
MONOFONT=&amp;quot;${MONOFONT:-Cartograph CF}&amp;quot;
pandoc --pdf-engine=xelatex \
--variable fontsize=12pt \
--variable linestretch=1.5 \
--variable geometry=a4paper \
--variable documentclass=&amp;quot;${DOCUMENTCLASS}&amp;quot; \
--variable mainfont=&amp;quot;${MAINFONT}&amp;quot; \
--variable monofont=&amp;quot;${MONOFONT}&amp;quot; \
-i test.md -o test.pdf
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="gotchas"&gt;Gotchas&lt;/h2&gt;
&lt;h3 id="cjk"&gt;CJK&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;CJK&lt;/strong&gt; is a collective term for the Chinese, Japanese, and Korean languages. When &lt;code&gt;CJKmainfont&lt;/code&gt; is set, pandoc handles CJK characters with the &lt;code&gt;xecjk&lt;/code&gt; package.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;pandoc --variable CJKmainfont=&amp;quot;Noto Serif CJK SC&amp;quot; ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My recommendation is to only set &lt;code&gt;CJKmainfont&lt;/code&gt; when needed to avoid messing up the quotation marks display. Where the article contains a mixture of English and Asian characters resulting in strange looking quotation marks &lt;code&gt;'&amp;quot;“”‘’&lt;/code&gt;, consider trying the following solution.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a file &lt;code&gt;cjk.latex&lt;/code&gt; to force English quotation marks.
&lt;pre&gt;&lt;code class="language-latex"&gt;% cjk.tex
\AtBeginDocument{%
\XeTeXcharclass`^^^^2019=0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Include this file via the pandoc argument &lt;code&gt;-H&lt;/code&gt; such as &lt;code&gt;pandoc -H cjk.tex&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The figure below shows the results from the same markdown file on my MacBook. However, when I test this in Ubuntu, it seems there are no such issues.&lt;/p&gt;
&lt;figure class="kg-image-card kg-width-fit"&gt;
&lt;img alt="Fix CJK Quotation Marks" class="kg-image" loading="lazy" src="https://blog.iany.me/2023/04/transforming-markdown-to-attractive-pdfs-a-guide-to-using-pandoc-with-xelatex/pandoc-pdf-quotation-marks_hu_6ee5b1f00c685afb.png" srcset="https://blog.iany.me/2023/04/transforming-markdown-to-attractive-pdfs-a-guide-to-using-pandoc-with-xelatex/pandoc-pdf-quotation-marks_hu_d70fb009ffb94210.png 400w, https://blog.iany.me/2023/04/transforming-markdown-to-attractive-pdfs-a-guide-to-using-pandoc-with-xelatex/pandoc-pdf-quotation-marks_hu_5b4591b20e622c46.png 800w, https://blog.iany.me/2023/04/transforming-markdown-to-attractive-pdfs-a-guide-to-using-pandoc-with-xelatex/pandoc-pdf-quotation-marks_hu_6ee5b1f00c685afb.png 1061w" sizes="(max-width: 800px) 100vw, 1061px" /&gt;
&lt;figcaption &gt;Fix CJK Quotation Marks&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This is the input markdown file used in the example.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;English's &amp;quot;straight quotations&amp;quot; and this’s “curly quotations”.
中文的&amp;quot;直引号&amp;quot;和“弯引号”。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="code-block-syntax-highlight"&gt;Code Block Syntax Highlight&lt;/h3&gt;
&lt;p&gt;Pandoc does not wrap lines in code blocks by default. It truncated the lines which are too long to fit on the page.&lt;/p&gt;
&lt;p&gt;To solve the problem mentioned above, I followed a solution on &lt;a href="https://github.com/jgm/pandoc/issues/4302#issuecomment-360799891"&gt;GitHub&lt;/a&gt; suggested by jannick0. It involves the creation of a file that contains the LaTeX snippets below. These snippets can then be included by using the pandoc argument &lt;code&gt;-H highlighting.tex&lt;/code&gt;. Additionally, I added a border around the code block.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-file" data-callout-type="file"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-file"&gt;&lt;/i&gt;
&lt;code&gt;highlighting.tex&lt;/code&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;pre&gt;&lt;code class="language-latex"&gt;% use this file via pandoc -H highlighting.tex
\usepackage{fvextra}
\DefineVerbatimEnvironment{Highlighting}{Verbatim}{breaklines,breaknonspaceingroup,breakanywhere,frame=single,framesep=8pt,rulecolor=\color[HTML]{aaaaaa},commandchars=\\\{\}}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;As I &lt;a href="https://github.com/jgm/pandoc/issues/4302#issuecomment-1508595755"&gt;replied in the thread&lt;/a&gt;, this solution does not work for code blocks without setting a language. I use a Lua filter to set the language of these code blocks to &amp;ldquo;text&amp;rdquo;. To run the filter, save it in a file, say &lt;code&gt;highlighting.lua&lt;/code&gt;, and invoke pandoc with &lt;code&gt;--lua-filter=highlighting.lua&lt;/code&gt;&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-file" data-callout-type="file"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-file"&gt;&lt;/i&gt;
&lt;code&gt;highlighting.lua&lt;/code&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;pre&gt;&lt;code class="language-lua"&gt;function CodeBlock(el)
if #el.classes == 0 then
el.classes[1] = 'text'
end
return el
end
return {{CodeBlock = CodeBlock}}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;h3 id="disable-figures-floating"&gt;Disable Figures Floating&lt;/h3&gt;
&lt;p&gt;The last issue to address is the floating of figures in LaTeX, which moves the images to avoid large blank space in pages. While LaTeX attempts to place figures in appropriate locations, authors may refer to them as &amp;ldquo;above&amp;rdquo; or &amp;ldquo;below&amp;rdquo; in the markdown files. This can be confusing for readers who may find that the referenced chart or diagram is actually on the following page in the PDF file.&lt;/p&gt;
&lt;p&gt;To solve the issue, simply disable floating figures. Create a file &lt;code&gt;disable-floating.tex&lt;/code&gt; and invoke pandoc with arguments &lt;code&gt;-H disable-floating.tex&lt;/code&gt;.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-file" data-callout-type="file"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-file"&gt;&lt;/i&gt;
&lt;code&gt;disable-floating.tex&lt;/code&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;pre&gt;&lt;code class="language-latex"&gt;% https://stackoverflow.com/a/58840456/667158
\usepackage{float}
\let\origfigure\figure
\let\endorigfigure\endfigure
\renewenvironment{figure}[1][2] {
\expandafter\origfigure\expandafter[H]
} {
\endorigfigure
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="putting-it-all-together"&gt;Putting It All Together&lt;/h2&gt;
&lt;p&gt;See the repo &lt;a href="https://github.com/doitian/pandoc-ide"&gt;doitian/pandoc-ide&lt;/a&gt;.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/latex/">LaTeX</category><category domain="https://blog.iany.me/tags/markdown/">Markdown</category><category domain="https://blog.iany.me/tags/pdf/">PDF</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>The Ultimate Guide to Customizing Obsidian Vim Mode via QuickAdd</title><link>https://blog.iany.me/2023/04/the-ultimate-guide-to-customizing-obsidian-vim-mode-via-quickadd/</link><pubDate>Thu, 27 Apr 2023 21:22:30 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2023/04/the-ultimate-guide-to-customizing-obsidian-vim-mode-via-quickadd/</guid><description>&lt;p&gt;Obsidian is my go-to note-taking app because of its availability on all desktop and mobile platforms with the bonus of Vim mode. In this guide, I&amp;rsquo;ll show you how I customize the Vim mode in Obsidian to maximize my note-taking efficiency.&lt;/p&gt;
&lt;h2 id="setup-user-launch-scripts-via-quickadd"&gt;Setup User Launch Scripts via QuickAdd&lt;/h2&gt;
&lt;p&gt;To customize the Vim mode in Obsidian, JavaScript is the only solution. Obsidian allows running user scripts by implementing an plugin. I use an easy alternative to run a JavaScript file on launch via QuickAdd.&lt;/p&gt;
&lt;p&gt;First, let’s create the JavaScript file in the vault. Save the skeleton below as the file &lt;code&gt;vimrc.js&lt;/code&gt; in any folder in the vault. Keep in mind that Obsidian does not support editing JavaScript files, so use an external editor.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-file" data-callout-type="file"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-file"&gt;&lt;/i&gt;
vimrc.js
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;function notice(text) {
new Notice(text);
return text;
}
module.exports = async function (context) {
const vim = window.CodeMirrorAdapter?.Vim;
if (vim === undefined) {
new Notice(`🔴error: vim mode is disabled`);
return;
}
// Add customizations here.
console.log(`🔵info: vimrc loaded`);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;Then, install QuickAdd by browsing the community plugins in Settings and follow the instructions on the Obsidian help page &lt;a href="https://help.obsidian.md/Extending&amp;#43;Obsidian/Community&amp;#43;plugins"&gt;Community Plugins&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, include the script in a QuickAdd macro and activate the option to execute the macro upon the plugin load.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open the tab QuickAdd in Settings.&lt;/li&gt;
&lt;li&gt;Click the button “Manage Macros”. In the pop-up dialog, fill in the macro name “vimrc” and click the button “Add macro“ to add a new macro vimrc.&lt;/li&gt;
&lt;li&gt;Toggle the option “Run on plugin load” on under the newly created macro.&lt;/li&gt;
&lt;li&gt;Configure the macro vimrc. Find the script file &lt;code&gt;vimrc&lt;/code&gt; in the “User Scripts” section and add the script as the only step in the macro.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Attention that the script runs once on launch or reload. Running “Reload app without saving” via Command Palette will reload the file after changes,&lt;/p&gt;
&lt;h2 id="vim-mode-customization-techniques"&gt;Vim Mode Customization Techniques&lt;/h2&gt;
&lt;p&gt;For the best tutorial on customizing Vim mode and what methods the &lt;code&gt;vim&lt;/code&gt; object provides, refer to &lt;a href="https://github.com/replit/codemirror-vim/blob/master/src/vim.js"&gt;how CodeMirror establishes the Vim mode&lt;/a&gt;. I will only introduce the methods that I use in &lt;a href="https://github.com/doitian/quickadd-settings/blob/main/shore/quickadd/vimrc.js"&gt;my own customizations&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="vimdefineex"&gt;&lt;code&gt;vim.defineEx&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;defineEx&lt;/code&gt; method enables users to execute a new ex command in Vim by typing &lt;code&gt;:&lt;/code&gt; in normal mode. For instance, the ex command &lt;code&gt;:w&lt;/code&gt; saves the file.&lt;/p&gt;
&lt;p&gt;I introduce &lt;code&gt;defineEx&lt;/code&gt; first because I need an ex command to run any Obsidian command so that I can use it to add key bindings later. I name this command &lt;code&gt;obr&lt;/code&gt;, which is the abbreviation of &lt;strong&gt;OB&lt;/strong&gt;sidian &lt;strong&gt;R&lt;/strong&gt;un. The command requires the Obsidian command identifier as the argument, for example, &lt;code&gt;:obr app:open-settings&lt;/code&gt; will open the Settings dialog.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;vim.defineEx(&amp;quot;obr&amp;quot;, &amp;quot;&amp;quot;, function (cm, params) {
if (params?.args?.length !== 1) {
throw new Error(notice(&amp;quot;🔴error: obr requires exactly 1 parameter&amp;quot;));
}
const command = params.args[0];
context.app.commands.executeCommandById(command);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To find the command identifier, I add another ex command &lt;code&gt;:obl&lt;/code&gt; to display a list of identifiers and copy the selected one. It accepts an argument to filter the result. For example, &lt;code&gt;obl open&lt;/code&gt; will list identifiers that contain the word &amp;ldquo;open&amp;rdquo;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;vim.defineEx(&amp;quot;obl&amp;quot;, &amp;quot;&amp;quot;, async function (cm, params) {
let commands = Object.keys(context.app.commands.commands);
for (const keyword of params?.args ?? []) {
commands = commands.filter((command) =&amp;gt; command.includes(keyword));
}
const choice = await context.quickAddApi.suggester(commands, commands);
if (choice !== null) {
await context.quickAddApi.utility.setClipboard(choice);
new Notice(`🔵info: copied ${choice} to the clipboard`);
}
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="vimmap"&gt;&lt;code&gt;vim.map&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;vim.map&lt;/code&gt; method takes two arguments, &lt;code&gt;lhs&lt;/code&gt; and &lt;code&gt;rhs&lt;/code&gt;, which can both be either a key sequence or an ex command. The meaning of the method varies depending on the arguments passed.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When both &lt;code&gt;lhs&lt;/code&gt; and &lt;code&gt;rhs&lt;/code&gt; are key sequence, it maps the key sequence &lt;code&gt;lhs&lt;/code&gt; to &lt;code&gt;rhs&lt;/code&gt;. For example, &lt;code&gt;vim.map('D', 'dd')&lt;/code&gt; will redefine &lt;code&gt;D&lt;/code&gt; to delete the whole line.&lt;/li&gt;
&lt;li&gt;When both &lt;code&gt;lhs&lt;/code&gt; and &lt;code&gt;rhs&lt;/code&gt; are ex commands, it creates a new ex command alias. For example, &lt;code&gt;vim.map(':Reload', ':obr app:reload')&lt;/code&gt; adds the alias &lt;code&gt;:Reload&lt;/code&gt; to run the Obsidian command &lt;code&gt;app:reload&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;lhs&lt;/code&gt; is a key sequence, and &lt;code&gt;rhs&lt;/code&gt; is an ex command, it maps the key sequence to run the ex command. For example &lt;code&gt;vim.map('ZZ', ':obr app:reload')&lt;/code&gt; allows reloading Obsidian by typing ZZ.&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;lhs&lt;/code&gt; is an ex command, and &lt;code&gt;rhs&lt;/code&gt; is a key sequence, executing the ex command has the same effect as typing the key sequence.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I have a bunch of mappings from key sequences to &lt;code&gt;:obr&lt;/code&gt;, for example, the z-family to operate on folds:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;vim.map(&amp;quot;zo&amp;quot;, &amp;quot;:obr editor:toggle-fold&amp;quot;);
vim.map(&amp;quot;zc&amp;quot;, &amp;quot;:obr editor:toggle-fold&amp;quot;);
vim.map(&amp;quot;za&amp;quot;, &amp;quot;:obr editor:toggle-fold&amp;quot;);
vim.map(&amp;quot;zR&amp;quot;, &amp;quot;:obr editor:unfold-all&amp;quot;);
vim.map(&amp;quot;zM&amp;quot;, &amp;quot;:obr editor:fold-all&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To use the space key as a key mapping prefix, it must first be &lt;code&gt;unmap&lt;/code&gt;ped.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;vim.unmap(&amp;quot;&amp;lt;Space&amp;gt;&amp;quot;);
vim.map(&amp;quot;&amp;lt;Space&amp;gt;&amp;lt;Space&amp;gt;&amp;quot;, &amp;quot;:obr switcher:open&amp;quot;);
vim.map(&amp;quot;&amp;lt;Space&amp;gt;n&amp;quot;, &amp;quot;:nohl&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="vimdefineaction-and--vimdefineoperator"&gt;&lt;code&gt;vim.defineAction&lt;/code&gt; and &lt;code&gt;vim.defineOperator&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;We can use both action and operator in &lt;code&gt;vim.mapCommand&lt;/code&gt;. The difference is that an operator performs upon text objects. See how CodeMirror defines the built-in &lt;a href="https://github.com/replit/codemirror-vim/blob/7e70ff7d321f9aa6600616a4d2ee81327394533a/src/vim.js#L2450"&gt;actions&lt;/a&gt; and &lt;a href="https://github.com/replit/codemirror-vim/blob/7e70ff7d321f9aa6600616a4d2ee81327394533a/src/vim.js#L2290"&gt;operators&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I have an action &lt;code&gt;swapLine&lt;/code&gt; to move lines around. It&amp;rsquo;s not perfect, since undo will reverse one step only.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;vim.defineAction(&amp;quot;swapLine&amp;quot;, function(_cm, { repeat, down }) {
const command = down ? &amp;quot;editor:swap-line-down&amp;quot; : &amp;quot;editor:swap-line-up&amp;quot;;
for (let i = 0; i &amp;lt; repeat; i++) {
context.app.commands.executeCommandById(command);
}
});
// undo after 5]e will only swap one line up.
vim.mapCommand(&amp;quot;]e&amp;quot;, &amp;quot;action&amp;quot;, &amp;quot;swapLine&amp;quot;, { down: true });
vim.mapCommand(&amp;quot;[e&amp;quot;, &amp;quot;action&amp;quot;, &amp;quot;swapLine&amp;quot;, { down: false });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The example for &lt;code&gt;vim.defineOperator&lt;/code&gt; is &lt;code&gt;titleCase&lt;/code&gt;, which formats the text in the &lt;a href="https://kb.iany.me/para/lets/w/Writing/Format&amp;#43;the&amp;#43;Title&amp;#43;Using&amp;#43;APA&amp;#43;Title&amp;#43;Case&amp;#43;Capitalization"&gt;APA Title Case Capitalization&lt;/a&gt;. For instance, &lt;code&gt;gzap&lt;/code&gt; formats the current paragraph in the title case.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;function titleCase(str, options) {
// See https://github.com/words/ap-style-title-case/blob/master/index.js how to format str to apa title case
}
// Following helper funnctions are borrowed from https://codemirror.net/5/keymap/vim.js
function cursorIsBefore(cur1, cur2) {
if (cur1.line &amp;lt; cur2.line) {
return true;
}
if (cur1.line == cur2.line &amp;amp;&amp;amp; cur1.ch &amp;lt; cur2.ch) {
return true;
}
return false;
}
function cursorMin(cur1, cur2) {
return cursorIsBefore(cur1, cur2) ? cur1 : cur2;
}
function findFirstNonWhiteSpaceCharacter(text) {
if (!text) {
return 0;
}
var firstNonWS = text.search(/\S/);
return firstNonWS == -1 ? text.length : firstNonWS;
}
vim.defineOperator(&amp;quot;titleCase&amp;quot;, function(cm, args, ranges, oldAnchor, newHead) {
const selections = cm.getSelections();
const newSelections = selections.map((s) =&amp;gt;
titleCase(s, { keepSpaces: true })
);
cm.replaceSelections(newSelections);
if (args.shouldMoveCursor) {
return newHead;
} else if (
!cm.state.vim.visualMode &amp;amp;&amp;amp;
args.linewise &amp;amp;&amp;amp;
ranges[0].anchor.line + 1 == ranges[0].head.line
) {
return {
line: oldAnchor.line,
ch: findFirstNonWhiteSpaceCharacter(cm.getLine(oldAnchor.line)),
};
} else if (args.linewise) {
return oldAnchor;
} else {
return cursorMin(ranges[0].anchor, ranges[0].head);
}
return newHead;
});
vim.mapCommand(&amp;quot;gz&amp;quot;, &amp;quot;operator&amp;quot;, &amp;quot;titleCase&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/obsidian/">Obsidian</category><category domain="https://blog.iany.me/tags/vim/">Vim</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>Practice Autofocus Method in Todoist</title><link>https://blog.iany.me/2022/10/practice-autofocus-method-in-todoist/</link><pubDate>Mon, 03 Oct 2022 19:26:38 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2022/10/practice-autofocus-method-in-todoist/</guid><description>&lt;p&gt;I read the article &lt;em&gt;Autofocus: The Productivity System That Treats Your to-Do List Like a River&lt;/em&gt;&lt;sup id="fnxref:1"&gt;&lt;a href="https://blog.iany.me/2022/10/practice-autofocus-method-in-todoist/#fnx:1"&gt;1&lt;/a&gt;&lt;/sup&gt; recently. I used to fill the time slots with tasks daily, but I rarely completed all the planned tasks. Even for the completed ones, I often missed the scheduled time slot. I felt stressed and guilty. So I decide to give the Autofocus Method a try.&lt;/p&gt;
&lt;h2 id="the-river-filter"&gt;The River Filter&lt;/h2&gt;
&lt;p&gt;I will continue using Todoist. I created a filter named &lt;em&gt;River&lt;/em&gt;. The tasks are sorted by added date. I added the filter to the sidebar and set it as my home view for easy access.&lt;/p&gt;
&lt;p&gt;The filter has three sections:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Overdue tasks: &lt;code&gt;overdue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tasks I am doing: &lt;code&gt;today | @now💧&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tasks in River: &lt;code&gt;!##⏰ Events &amp;amp; !overdue &amp;amp; !today &amp;amp; !@now💧 &amp;amp; !@someday🪨&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Todoist supports filter sections by separating queries with comma (&lt;code&gt;,&lt;/code&gt;). So here is the final filter query:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;overdue, today | @now💧, !##⏰ Events &amp;amp; !overdue &amp;amp; !today &amp;amp; !@now💧 &amp;amp; !@someday🪨
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="section-1-overdue-tasks"&gt;Section 1: Overdue Tasks&lt;/h3&gt;
&lt;p&gt;This section allows me to re-schedule the overdue tasks.&lt;/p&gt;
&lt;p&gt;I decided to not totally abandon the due dates. However, I use them with great moderation. I set a due date if I must complete the task on that date, and before the day comes, I can forget the task.&lt;/p&gt;
&lt;p&gt;I add such tasks to the project &lt;code&gt;⏰ Events&lt;/code&gt;. They will appear in the River filter when they are due or overdue.&lt;/p&gt;
&lt;h3 id="section-2-tasks-i-am-doing"&gt;Section 2: Tasks I Am Doing&lt;/h3&gt;
&lt;p&gt;This section lists tasks that I am doing now.&lt;/p&gt;
&lt;p&gt;There are two kinds of such tasks.&lt;/p&gt;
&lt;p&gt;The first is the scheduled tasks that are due today.&lt;/p&gt;
&lt;p&gt;The second is the selected tasks from the river. I add the tag &lt;code&gt;@now💧&lt;/code&gt; to the task if I feel like doing it.&lt;/p&gt;
&lt;h3 id="section-3-tasks-in-river"&gt;Section 3: Tasks in River&lt;/h3&gt;
&lt;p&gt;This section contains the tasks in the river. It excludes tasks from the project &lt;code&gt;⏰ Events&lt;/code&gt;, and the dismissed tasks. I tag dismissed tasks with &lt;code&gt;@someday🪨&lt;/code&gt; .&lt;/p&gt;
&lt;h2 id="the-workflow"&gt;The Workflow&lt;/h2&gt;
&lt;p&gt;I add new tasks to the inbox without triaging the projects, tags, due dates, and priorities. Since the filter&lt;/p&gt;
&lt;p&gt;When I have time to do something, I open the River filter and scan the tasks from top to bottom without scrolling down.&lt;/p&gt;
&lt;p&gt;First, I re-schedule the overdue tasks.&lt;/p&gt;
&lt;p&gt;Then, I do a quick scan on the second section—the tasks that I planned before. If I feel like doing a task in the list, I start working on it.&lt;/p&gt;
&lt;p&gt;If I failed to find a task to work on in the previous step, I continue the scan to the last section—the tasks in the river. I add tag &lt;code&gt;@now💧&lt;/code&gt; to pop the task to the top of the list.&lt;/p&gt;
&lt;p&gt;If there are no items stands out for me, I have three choices to remove tasks from the first screen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Defer the scheduled tasks into future.&lt;/li&gt;
&lt;li&gt;Remove the tag &lt;code&gt;@now💧&lt;/code&gt; from the task.&lt;/li&gt;
&lt;li&gt;Dismiss a task by adding the tag &lt;code&gt;@someday🪨&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Autofocus Method suggests to cross the item off the list and re-enter it at the end of the list if I haven&amp;rsquo;t finished it yet. In Todoist, I duplicate the task and cross the original item off.&lt;/p&gt;
&lt;p&gt;Here is my setup to practice the Autofocus Method in Todoist. I&amp;rsquo;m still experimenting to see whether it can improve my productivity.&lt;/p&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a name="fnx:1"&gt;&lt;/a&gt; McKay, B., &amp;amp; McKay, K. (2022, September 20). &lt;em&gt;Autofocus: The Productivity System That Treats Your To-Do List Like a River&lt;/em&gt;. The Art of Manliness. &lt;a href="https://www.artofmanliness.com/character/behavior/autofocus-the-productivity-system-that-treats-your-to-do-list-like-a-river/"&gt;https://www.artofmanliness.com/character/behavior/autofocus-the-productivity-system-that-treats-your-to-do-list-like-a-river/&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/productivity/">Productivity</category><category domain="https://blog.iany.me/tags/todoist/">Todoist</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item><item><title>Set the Minimum Width of the Active Pane in Obsidian</title><link>https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/</link><pubDate>Fri, 23 Sep 2022 16:44:51 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-info" data-callout-type="info"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-circle-info"&gt;&lt;/i&gt;
Updates
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
I have created &lt;a href="https://github.com/doitian/obsidian-min-width"&gt;an plugin&lt;/a&gt; for this feature.
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;A simple CSS rule sets the minimum width of the active pane in Obsidian.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-file" data-callout-type="file"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-file"&gt;&lt;/i&gt;
min-width.css
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;pre&gt;&lt;code class="language-css"&gt;.mod-root .mod-active {
min-width: min(88%, 40rem);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;It sets the minimum width to either 40 columns or 88% of the whole editing area, depending on which is smaller. Obsidian will auto resize the active pane to ensure it is wider enough and shrink other panes accordingly.&lt;/p&gt;
&lt;p&gt;The snippet does not work in horizontal splits where panes stack vertically. See the chapter &lt;a href="https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/#javascript-helper"&gt;JavaScript Helper&lt;/a&gt; below how to work around it.&lt;/p&gt;
&lt;h2 id="how-to-add-css-snippets-in-obsidian"&gt;How to Add CSS Snippets in Obsidian&lt;/h2&gt;
&lt;p&gt;Save the CSS snippet as file &lt;code&gt;.obsidian/snippets/min-width.css&lt;/code&gt; in the vault. Create the folder &lt;code&gt;.obsidian/snippets&lt;/code&gt; if it does not exist.&lt;/p&gt;
&lt;p&gt;Now the file should appear in the Appearance settings page. Toggle the switcher to enable the CSS snippet in the file.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Enable CSS Snippet in Obsidian" class="kg-image" loading="lazy" src="https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/enable-obsidian-min-width-css-snippet_hu_9b04bc616568732a.png" srcset="https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/enable-obsidian-min-width-css-snippet_hu_a064331118fdb57f.png 400w, https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/enable-obsidian-min-width-css-snippet_hu_9bcda2927c03a47e.png 800w, https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/enable-obsidian-min-width-css-snippet_hu_62713a31b9adf826.png 1200w, https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/enable-obsidian-min-width-css-snippet_hu_9b04bc616568732a.png 1524w" sizes="(max-width: 1200px) 100vw, 1524px" /&gt;
&lt;figcaption &gt;Enable CSS Snippet in Obsidian&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="javascript-helper"&gt;JavaScript Helper&lt;/h2&gt;
&lt;p&gt;CSS is useless when it can not select the target elements. To make the snippet work in horizontal splits, the CSS rule has to select the &lt;code&gt;.mod-horizontal&lt;/code&gt; node when it has a &lt;code&gt;.mod-active&lt;/code&gt; child. However, the pseudo-class selector &lt;code&gt;:has()&lt;/code&gt;&lt;sup id="fnref:1"&gt;&lt;span class="footnote-ref" role="doc-noteref"&gt;1&lt;/span&gt;&lt;/sup&gt; is not available in Obsidian yet.&lt;/p&gt;
&lt;p&gt;JavaScript can help to add classes and attributes to HTML elements.&lt;/p&gt;
&lt;p&gt;I also want to make some panes wider, such as Excalibrain&lt;sup id="fnref:2"&gt;&lt;span class="footnote-ref" role="doc-noteref"&gt;2&lt;/span&gt;&lt;/sup&gt;. The JavaScript code can also set the view type to the proper elements. The view type of the Excalibrain pane is &lt;code&gt;excalidraw&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;First, update the CSS snippet to make Excalibrain panes wider. Indeed, it makes all Excalidraw panes wider.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-file" data-callout-type="file"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-file"&gt;&lt;/i&gt;
min-width.css
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;pre&gt;&lt;code class="language-css"&gt;.mod-root .mod-active {
min-width: min(88%, 40rem);
}
.mod-root .mod-active[data-type=&amp;quot;excalidraw&amp;quot;] {
min-width: min(88%, 60rem);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;The last, let Obsidian run some code when the active pane has changed. I use QuickAdd&lt;sup id="fnref:3"&gt;&lt;span class="footnote-ref" role="doc-noteref"&gt;3&lt;/span&gt;&lt;/sup&gt; to manage the scripts. Save the following code snippet as &lt;code&gt;min-width.js&lt;/code&gt; in the vault.&lt;/p&gt;
&lt;details class="kg-card kg-callout kg-callout-file" data-callout-type="file"&gt;
&lt;summary class="kg-callout-title"&gt;
&lt;i class="fas fa-chevron-right"&gt;&lt;/i&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-file"&gt;&lt;/i&gt;
min-width.js
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;const DATA_TYPE = &amp;quot;data-type&amp;quot;;
function setOrRemoveDataType(el, dataType) {
if (dataType !== undefined) {
el.setAttribute(DATA_TYPE, dataType);
} else {
el.removeAttribute(DATA_TYPE);
}
}
module.exports = async function ({ app }) {
app.workspace.on(&amp;quot;active-leaf-change&amp;quot;, (leaf) =&amp;gt; {
// clear .mod-active on .mod-horizontal
for (const el of app.workspace.containerEl.getElementsByClassName(
&amp;quot;mod-active mod-horizontal&amp;quot;
)) {
el.classList.remove(&amp;quot;mod-active&amp;quot;);
}
// bubble up data-type
const dataType = leaf.containerEl
.getElementsByClassName(&amp;quot;workspace-leaf-content&amp;quot;)[0]
?.getAttribute(DATA_TYPE);
setOrRemoveDataType(leaf.containerEl, dataType);
// add .mod-active and data-type to current horizontal split container
const parentNode = leaf.containerEl.parentNode;
if (parentNode.classList.contains(&amp;quot;mod-horizontal&amp;quot;)) {
parentNode.classList.add(&amp;quot;mod-active&amp;quot;);
setOrRemoveDataType(parentNode, dataType);
}
});
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;Then run it when Obsidian starts by following these steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Click the button &amp;ldquo;Manage Macros&amp;rdquo; in the QuickAdd settings.&lt;/li&gt;
&lt;li&gt;Add a new macro and add the user script &amp;ldquo;min-width&amp;rdquo; to the macro.&lt;/li&gt;
&lt;li&gt;Save the macro and check the option &amp;ldquo;Run on plugin load&amp;rdquo;.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="kg-card kg-gallery-card kg-width-wide"&gt;
&lt;div class="kg-gallery-container"&gt;
&lt;div class="kg-gallery-row"&gt;
&lt;div class="kg-gallery-image"&gt;&lt;img alt="Add a Macro" class="tippy" loading="lazy" width="1392" height="976" src="https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/add-obsidian-min-width-macro_hu_9d498df0c5af90ec.png" srcset="https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/add-obsidian-min-width-macro_hu_5ed066d20785371e.png 400w, https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/add-obsidian-min-width-macro_hu_68ca1600e0a08c7e.png 800w, https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/add-obsidian-min-width-macro_hu_4d0fa959701f025a.png 1200w, https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/add-obsidian-min-width-macro_hu_9d498df0c5af90ec.png 1392w" sizes="(max-width: 1200px) 100vw, 1392px" data-tippy-content="Add a Macro" /&gt;&lt;/div&gt;
&lt;div class="kg-gallery-image"&gt;&lt;img alt="Run on plugin load" class="tippy" loading="lazy" width="434" height="685" src="https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/run-min-width-script-on-plugin-load_hu_cde56c5fdacffcf7.png" srcset="https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/run-min-width-script-on-plugin-load_hu_8859fd6b79bb9b25.png 400w, https://blog.iany.me/2022/09/set-the-minimum-width-of-the-active-pane-in-obsidian/run-min-width-script-on-plugin-load_hu_cde56c5fdacffcf7.png 434w" sizes="(max-width: 400px) 100vw, 434px" data-tippy-content="Run on plugin load" /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;figcaption&gt;Run JavaScript Code on Start With QuickAdd&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;That&amp;rsquo;s it. Restart Obsidian and the code should be effective. See how it works in the screen recording below.&lt;/p&gt;
&lt;figure class="kg-card kg-embed-card"&gt;
&lt;iframe src="https://player.vimeo.com/video/752964835" width="640" height="391" frameborder="0" allow="autoplay; fullscreen" allowfullscreen&gt;&lt;/iframe&gt;
&lt;figcaption&gt;Set the Minimum Width of the Active Pane in Obsidian&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;There are two extra plugins used in the video, Snippet Commands&lt;sup id="fnref:4"&gt;&lt;span class="footnote-ref" role="doc-noteref"&gt;4&lt;/span&gt;&lt;/sup&gt; and Cycle through Panes.&lt;sup id="fnref:5"&gt;&lt;span class="footnote-ref" role="doc-noteref"&gt;5&lt;/span&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2 id="similar-work"&gt;Similar Work&lt;/h2&gt;
&lt;p&gt;This work is inspired by the Vim option &lt;code&gt;winwidth&lt;/code&gt;, one of my favorite feature of Vim.&lt;/p&gt;
&lt;p&gt;The awesome plugin Sliding Panes&lt;sup id="fnref:6"&gt;&lt;span class="footnote-ref" role="doc-noteref"&gt;6&lt;/span&gt;&lt;/sup&gt; provides the similar feature for Obsidian. However, it has three problems that I cannot stand.&lt;/p&gt;
&lt;p&gt;The first, when the option &amp;ldquo;Leaf Width on Desktop/Mobile&amp;rdquo; is wider than the whole editing area, I have to scroll horizontally to see the current active pane. Although the plugin has separated the options for Desktop and Mobile, but different devices have different screen sizes. What&amp;rsquo;s worser, the app window can be any size on Desktop. The option must be less than the smallest screen width when Obsidian Sync is in use, which is not a good choice for wider devices.&lt;/p&gt;
&lt;p&gt;The second, the plugin does not play well with horizontal splits.&lt;/p&gt;
&lt;p&gt;The last, I really want to set the different minimum widths for different windows.&lt;/p&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;MDN. (2016, October 27). :&lt;em&gt;has() - CSS: Cascading Style Sheets&lt;/em&gt;. MDN. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:has"&gt;https://developer.mozilla.org/en-US/docs/Web/CSS/:has&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;zsviczian. (2022, May). &lt;em&gt;Excalibrain: A Graph View to Navigate Your Obsidian Vault&lt;/em&gt;. GitHub. &lt;a href="https://github.com/zsviczian/excalibrain"&gt;https://github.com/zsviczian/excalibrain&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;chhoumann. (2021, June). &lt;em&gt;QuickAdd for Obsidian&lt;/em&gt;. GitHub. &lt;a href="https://github.com/chhoumann/quickadd"&gt;https://github.com/chhoumann/quickadd&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;deathau. (2021, October). &lt;em&gt;Snippet Commands Obsidian Plugin&lt;/em&gt;. GitHub. &lt;a href="https://github.com/deathau/snippet-commands-obsidian"&gt;https://github.com/deathau/snippet-commands-obsidian&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;phibr0. (2020, November). &lt;em&gt;Cycle Through Panes Obsidian Plugin&lt;/em&gt;. GitHub. &lt;a href="https://github.com/phibr0/cycle-through-panes"&gt;https://github.com/phibr0/cycle-through-panes&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;deathau. (2020, October). &lt;em&gt;Sliding Panes (Andy Matuschak Mode) Obsidian Plugin&lt;/em&gt;. GitHub. &lt;a href="https://github.com/deathau/sliding-panes-obsidian"&gt;https://github.com/deathau/sliding-panes-obsidian&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/css/">CSS</category><category domain="https://blog.iany.me/tags/obsidian/">Obsidian</category><category domain="https://blog.iany.me/tags/productivity/">Productivity</category><category domain="https://blog.iany.me/tags/x/">X</category><category domain="https://blog.iany.me/tags/zettel/permanent/">Zettel Permanent</category></item></channel></rss>