<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Hello Charlie</title><description>Make everyday life easier—and a little more interesting—with useful tips and stories worth knowing.</description><link>https://hellocharlie.blog/</link><item><title>3 Frontend RBAC Quiz: Would Your Permission Check Pass?</title><link>https://hellocharlie.blog/posts/tech/3-frontend-rbac-quiz/</link><guid isPermaLink="true">https://hellocharlie.blog/posts/tech/3-frontend-rbac-quiz/</guid><description>Answers to the three questions I left hanging in my last post.</description><pubDate>Sat, 05 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;aside class=&quot;post-link&quot; aria-label=&quot;Related article&quot; data-astro-cid-aj63f33u&gt;&lt;article class=&quot;article-card related-posts__card article-card--with-image&quot;&gt;&lt;a class=&quot;article-card__link&quot; href=&quot;https://hellocharlie.blog/posts/tech/5-minutes-into-frontend-rbac/&quot;&gt;&lt;div class=&quot;article-card__media&quot;&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/frontend-rbac-patterns.C8AidZwb_1Na9x2.png&quot; srcset=&quot;https://hellocharlie.blog/_astro/frontend-rbac-patterns.C8AidZwb_1Na9x2.png 480w, https://hellocharlie.blog/_astro/frontend-rbac-patterns.C8AidZwb_1Na9x2.png 720w, https://hellocharlie.blog/_astro/frontend-rbac-patterns.C8AidZwb_1Na9x2.png 960w, https://hellocharlie.blog/_astro/frontend-rbac-patterns.C8AidZwb_1Na9x2.png 1280w&quot; alt=&quot;Frontend and backend trust boundary: the browser decides what to show; the server decides what is allowed.&quot; sizes=&quot;(min-width: 1080px) 50vw, (min-width: 760px) 66vw, 100vw&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1400&quot; height=&quot;647&quot;&gt;&lt;/div&gt;&lt;div class=&quot;article-card__content&quot;&gt;&lt;h2&gt;5 Minutes into Frontend RBAC&lt;/h2&gt;&lt;p class=&quot;meta&quot;&gt;05 Sept 2026&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/article&gt;&lt;/aside&gt;
&lt;p&gt;I’ve got answers to the 3 questions from my last post.
In that post, I wrote about 3 ways FE can handle authorization&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Static routing&lt;/li&gt;
&lt;li&gt;Server-driven UI&lt;/li&gt;
&lt;li&gt;A granular permission array (instead of roles)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All three are different, but they share one root idea&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;FE isn’t about security. It’s about UX.&lt;/strong&gt; Every decision still depends on the server.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Most apps run just fine on this idea.
But the trouble starts once real users and real data pile up.&lt;/p&gt;
&lt;p&gt;So I left 3 questions at the end of that post.
Now I’m back to actually dig into them.&lt;/p&gt;
&lt;p&gt;A user is filling in the third field of a form.
Meanwhile, the admin quietly revokes their access.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This person has no idea yet.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The frontend doesn’t decide who’s authorized.
It’s just a mirror of the server’s decision.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;These three questions are really just one, in disguise.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;When the mirror shakes.
When the mirror breaks.
Can you still stand up straight? Haha.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/permission-mirror.BAEpjXFm_ZksoAT.jpeg&quot; alt=&quot;An illustrated standing mirror reflects a server rack in a quiet room.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1400&quot; height=&quot;781&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The frontend mirrors the server’s permission decisions.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;q1-what-if-permission-gets-revoked-mid-action&quot;&gt;Q1. What if permission gets revoked mid-action?&lt;/h2&gt;
&lt;p&gt;The user keeps typing. The admin has already pulled their access.
The server won’t save it. That’s the server’s job.&lt;/p&gt;
&lt;p&gt;The real problem comes next.
The screen just keeps smiling, like nothing happened.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How do you get a real-time update?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You don’t want to be knocking on the server the whole time.&lt;/p&gt;
&lt;p&gt;This is where &lt;strong&gt;SSE (Server-Sent Events)&lt;/strong&gt; comes in.&lt;/p&gt;
&lt;p&gt;All you really need is one signal: “&lt;strong&gt;Something changed.&lt;/strong&gt;”
You don’t need the full data, just the alert.&lt;/p&gt;
&lt;p&gt;So… what if that signal gets lost while your connection drops?
No worries.&lt;/p&gt;
&lt;p&gt;You just send the ID of the last signal you got, and the server fills in the rest.&lt;/p&gt;
&lt;h2 id=&quot;q2-what-do-you-trust-when-the-network-drops&quot;&gt;Q2. What do you trust when the network drops?&lt;/h2&gt;
&lt;p&gt;When you’re offline, you can’t just ask the server again.&lt;/p&gt;
&lt;p&gt;So… should you trust the last permission you saw?
Trusting it feels risky. What if it’s already outdated?&lt;/p&gt;
&lt;p&gt;The answer is &lt;strong&gt;optimistic UI&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;You trust the last known permission.
Let the user keep working, and queue up whatever they do.&lt;/p&gt;
&lt;p&gt;Why is this safe?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Because you can check everything again once you’re back online.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The permission you’re trusting isn’t a real decision.
It’s just a temporary kindness.&lt;/p&gt;
&lt;p&gt;It’s fine if it’s stale, because the client was never the one making the final call anyway.&lt;/p&gt;
&lt;p&gt;So where does the unsynced work go?&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Throwing it away is the easy answer.
It’s also the wrong one.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/offline-outbox.DeJxQeyP_FmSop.jpeg&quot; alt=&quot;An illustrated open mailbox holds several letters beside a quiet residential street.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1400&quot; height=&quot;781&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;An outbox keeps work waiting until it can be sent.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Think of a mailbox. You don’t throw away a letter just because the post office is closed. You drop it in, and it goes out once the office opens again.&lt;/p&gt;
&lt;p&gt;Same idea here. You stack the actions in an outbox and send them out in order once you’re back online.&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;pending → syncing → (synced | rejected)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;q3-what-about-i-can-only-edit-my-own-posts&quot;&gt;Q3. What about “I can only edit my own posts”?&lt;/h2&gt;
&lt;p&gt;This one’s the sneaky trap.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;edit:posts&lt;/code&gt; tells you the user can edit posts.
It doesn’t tell you whether they can edit this post.&lt;/p&gt;
&lt;p&gt;So the check quietly turns into&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; canEdit&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; can&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&amp;#39;edit:posts&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; post&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;.authorId &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; currentUser&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;.id;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looks fine at first. The problem is, this one line gets copy-pasted across every component.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;That’s the mirror trying to make its own decisions.&lt;/p&gt;
&lt;p&gt;The moment FE tries to reimplement the rule, FE has to be updated every time the policy changes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;So instead of shipping the rule, ship the decision.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The server calculates “Can this user do this?” per resource and sends it down with the data&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  &amp;quot;id&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;post_123&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  &amp;quot;title&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;…&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  &amp;quot;_permissions&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&amp;quot;canEdit&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; &amp;quot;canDelete&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; false&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;// FE just reads the decision&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; canEdit&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; post&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;_permissions&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;.canEdit;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, even if the policy changes, your FE code doesn’t change a single line. The server just sends a different value.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(Quick tip: if you’re rendering a list of posts, batch these checks instead of asking one by one.)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yeah, this value can go stale too. But that’s not a new problem.&lt;/p&gt;
&lt;p&gt;The same two answers from Q1 and Q2 apply here&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;catch the signal and re-check, or trust it optimistically and re-verify later.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;so-one-rule-to-rule-them-all&quot;&gt;So, one rule to rule them all&lt;/h2&gt;
&lt;p&gt;All three questions really fold into one.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Permission changes in real time
→ catch the signal, ask again, but don’t wreck the user’s work.&lt;/li&gt;
&lt;li&gt;The network drops
→ trust the last permission for a bit, then double-check with the server once you’re back.&lt;/li&gt;
&lt;li&gt;Permission depends on the resource
→ don’t copy the rule over, just render the decision.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;A mirror doesn’t make up answers.
It just reflects.&lt;/strong&gt;&lt;/p&gt;</content:encoded><author>Charlie</author></item><item><title>5 Minutes into Frontend RBAC</title><link>https://hellocharlie.blog/posts/tech/5-minutes-into-frontend-rbac/</link><guid isPermaLink="true">https://hellocharlie.blog/posts/tech/5-minutes-into-frontend-rbac/</guid><description>Roles, permissions, and where the easy answers run out.</description><pubDate>Sat, 05 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When it comes to permissions (RBAC) on the frontend, are you just hiding a button with &lt;code&gt;if (role === &amp;#39;ADMIN&amp;#39;)&lt;/code&gt; and calling it done?&lt;/p&gt;
&lt;p&gt;It works. Until it doesn’t. So before the patterns, the one rule most tutorials skip:&lt;/p&gt;
&lt;aside class=&quot;callout notice&quot;&gt;&lt;span class=&quot;callout__icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg viewBox=&quot;0 0 24 24&quot; width=&quot;18&quot; height=&quot;18&quot;&gt;&lt;circle cx=&quot;12&quot; cy=&quot;12&quot; r=&quot;8.5&quot;&gt;&lt;/circle&gt;&lt;path d=&quot;M12 10.75v5&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M12 7.75h.01&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;div class=&quot;callout__content&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;&lt;strong&gt;Frontend permission control is not security. It’s UX.&lt;/strong&gt;&lt;/p&gt;&lt;div class=&quot;callout__body&quot;&gt;&lt;p&gt;Anything you check in the browser can be bypassed—in the browser devtools, a replayed request, or
a direct API call. The frontend only decides what to show. What’s allowed is decided by the
backend. Always: every protected route, every mutation, enforced on the server.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;That’s your Single Source of Truth (SSOT). Keep that boundary clean and everything below is about experience. Blur it, and you’ve shipped a lock with the key taped to the door.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/frontend-rbac-patterns.C8AidZwb_1Na9x2.png&quot; alt=&quot;Trust boundary between the frontend browser, which decides what to show and can be bypassed in devtools, and the backend server, which decides what is allowed and is the single source of truth.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1400&quot; height=&quot;647&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The frontend decides what to show; the backend decides what’s allowed.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now, the three patterns.&lt;/p&gt;
&lt;h2 id=&quot;approach-1-static-route-mapping-config-driven&quot;&gt;Approach 1. Static Route Mapping (Config-Driven)&lt;/h2&gt;
&lt;p&gt;The simplest approach maps roles to routes.&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ADMIN  -&amp;gt; Dashboard, Users&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;EDITOR -&amp;gt; Dashboard&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;VIEWER -&amp;gt; Dashboard (read only)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works perfectly on day one. Then the requirements change.&lt;/p&gt;
&lt;p&gt;A role needs access to part of a page. A new edge case fits none of your existing roles. Marketing wants editors to see one admin-only widget.&lt;/p&gt;
&lt;p&gt;Now the role checks don’t live in one config file. They scatter across components, routes, menus, and feature flags.&lt;/p&gt;
&lt;p&gt;The coupling grows quietly and you only notice the day you add one new role and break four screens you forgot existed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; small apps with few, stable roles.&lt;br/&gt;
&lt;strong&gt;Painful when:&lt;/strong&gt; roles multiply or permissions get granular.&lt;/p&gt;
&lt;h2 id=&quot;approach-2-server-driven-navigation-backend-driven-ui&quot;&gt;Approach 2. Server-Driven Navigation (Backend-Driven UI)&lt;/h2&gt;
&lt;p&gt;Here, the server decides. Instead of hardcoding menus, the frontend renders whatever the backend sends down—the navigation, the visible features, the allowed actions.&lt;/p&gt;
&lt;p&gt;This shines in B2B SaaS, where every customer has a different plan and different permissions. You can’t hardcode that.&lt;/p&gt;
&lt;p&gt;The trade-off is timing: a permission-aware UI can’t fully render until the payload arrives. But that’s not the blank screen a lot of write-ups make it out to be. Your shell and skeletons paint immediately, and caching the last known set lets returning users see their real UI right away.&lt;/p&gt;
&lt;p&gt;A trade-off you manage, not a wall you hit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; multi-tenant products with per-customer permissions.&lt;br/&gt;
&lt;strong&gt;Painful when:&lt;/strong&gt; you treat the permission fetch as blocking instead of progressive.&lt;/p&gt;
&lt;h2 id=&quot;approach-3-granular-permissions&quot;&gt;Approach 3. Granular Permissions&lt;/h2&gt;
&lt;p&gt;The third approach stops shipping roles to the frontend and ships a flat array of permissions instead.&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&amp;quot;view:dashboard&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;edit:users&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;delete:comments&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now components don’t care who you are. They ask one question:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Do I have this permission?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;Can&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; permission&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&amp;quot;edit:users&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;  &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;EditUserButton&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;Can&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is decoupling. The component knows nothing about admins, editors, or viewers. Only a single capability. Drop it anywhere, test it in isolation, reuse it without rewiring a role matrix.&lt;/p&gt;
&lt;p&gt;It’s still RBAC. The roles don’t disappear. They move.&lt;/p&gt;
&lt;p&gt;The backend keeps them, maps each one to a set of permissions, and sends down the resolved flat array. Roles stay a backend concern; permissions become the language the frontend speaks.&lt;/p&gt;
&lt;p&gt;One honest cost: those strings multiply and aren’t type-checked. Typo &lt;code&gt;&amp;quot;edit:users&amp;quot;&lt;/code&gt; and nothing complains until runtime. Govern them with a central registry or generated types before they drift.&lt;/p&gt;
&lt;h2 id=&quot;before-you-go-a-few-things-worth-thinking-about&quot;&gt;Before You Go: A Few Things Worth Thinking About&lt;/h2&gt;
&lt;p&gt;These three patterns will carry most apps a long way. But the moment your product has real users and real data, a few questions show up that none of them answer cleanly.&lt;/p&gt;
&lt;p&gt;I won’t solve them here. The answers deserve their own article, and honestly, the thinking is the valuable part. So let’s just sit with them.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What happens when a permission is revoked mid-action?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A user is three fields into a form when an admin pulls their access. The backend will reject the save—fine, that’s the SSOT doing its job. But should the UI let them keep typing into a dead end? How does the frontend even find out in real time, without hammering the server? And once it knows, how do you take access away without yanking half-finished work off the screen?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What do you trust when the network drops?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Offline-first apps can’t refetch permissions on demand. Do you trust the last permission set you saw and risk acting on stale rules? And if the user makes a change you can’t sync, where does that work go? Throwing it away is the easy answer. It’s also the wrong one.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What about “I can only edit my own posts”?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is the sneaky one. &lt;code&gt;edit:posts&lt;/code&gt; tells you the user can edit posts. Not whether they can edit this post. The check quietly becomes &lt;code&gt;can(&amp;quot;edit:posts&amp;quot;) &amp;amp;&amp;amp; post.authorId === currentUser.id&lt;/code&gt;. The moment your permissions depend on the resource and not just the user, plain RBAC starts to creak, and you’re standing at the doorway of a different model entirely.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;None of these has a single right answer. They have trade-offs, which is exactly what makes them worth thinking through before you hit them in production.&lt;/p&gt;
&lt;p&gt;So I’ll leave you with the question instead of the answer:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;How would you handle each of these in your own app?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(That third one, in particular, is where I’d like to pick things up next time.)&lt;/p&gt;</content:encoded><author>Charlie</author></item><item><title>[In Melbourne] Free Coffee Cupping: Five Senses ‘Get It India’</title><link>https://hellocharlie.blog/posts/coffee/five-senses-get-it-india-melbourne-2026/</link><guid isPermaLink="true">https://hellocharlie.blog/posts/coffee/five-senses-get-it-india-melbourne-2026/</guid><description>A free Indian coffee cupping in West Melbourne on 24 September 2026. Check the time, venue, programme and Eventbrite booking details.</description><pubDate>Mon, 07 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Five Senses’ &lt;strong&gt;Get It India&lt;/strong&gt; cupping is a free West Melbourne session on Thursday 24 September 2026. The organiser describes it as an Indian coffee programme with a tasting of coffees made using different processing methods. The event has not taken place yet, so the details here come from the published listing. &lt;a href=&quot;https://www.eventbrite.com.au/e/mel-curated-cupping-get-it-india-tickets-1997801946401&quot;&gt;Event listing&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/coffee-cupping-illustration.DH6t5Ym3_Ze4JUq.png&quot; alt=&quot;AI-generated illustration of coffee cupping bowls, spoons and roasted coffee beans.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1536&quot; height=&quot;1024&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;AI-generated representative image of coffee cupping.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;event-details&quot;&gt;Event details&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;When:&lt;/strong&gt; Thursday 24 September 2026, 5:30–7:30 pm, Melbourne local time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Where:&lt;/strong&gt; Five Senses Barista Academy, 300 Rosslyn Street, West Melbourne VIC 3003.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Price:&lt;/strong&gt; Free (A$0).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Booking:&lt;/strong&gt; &lt;a href=&quot;https://www.eventbrite.com.au/e/mel-curated-cupping-get-it-india-tickets-1997801946401&quot;&gt;Reserve a spot through Eventbrite&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Five Senses lists the same address on its &lt;a href=&quot;https://fivesenses.com.au/pages/contact&quot;&gt;academy contact page&lt;/a&gt;. Recheck Eventbrite before you book or travel, because ticket availability and event details can change.&lt;/p&gt;
&lt;h2 id=&quot;what-the-session-covers&quot;&gt;What the session covers&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.eventbrite.com.au/e/mel-curated-cupping-get-it-india-tickets-1997801946401&quot;&gt;Eventbrite listing&lt;/a&gt; says the evening covers Indian coffee’s history and growing conditions, with Veer Attikan and Gungegiri estates named in the programme. It also says the tasting will explore innovative processing methods.&lt;/p&gt;
&lt;h2 id=&quot;if-cupping-is-new-to-you&quot;&gt;If cupping is new to you&lt;/h2&gt;
&lt;p&gt;Five Senses’ &lt;a href=&quot;https://fivesenses.com.au/blogs/news/cupping-coffee-at-home&quot;&gt;cupping guide&lt;/a&gt; describes cupping as a way to compare coffee samples side by side for flavour, acidity, body and aftertaste.&lt;/p&gt;
&lt;p&gt;For your own notes, start with a plain comparison: which cup feels fuller, or which flavour stays with you longer? Write down what you notice in ordinary words. You could also ask which estate each sample comes from and how it was processed. These are tasting prompts, not promises about how this session will be run.&lt;/p&gt;
&lt;p&gt;The listing does not explicitly confirm beginner suitability. If this would be your first cupping, ask Five Senses whether the session suits newcomers and whether you need to bring anything. Check any access requirements with the venue before booking.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Details checked: 7 September 2026.&lt;/strong&gt; For the latest ticket information, use the &lt;a href=&quot;https://www.eventbrite.com.au/e/mel-curated-cupping-get-it-india-tickets-1997801946401&quot;&gt;original event page&lt;/a&gt;.&lt;/p&gt;</content:encoded><author>Charlie</author></item><item><title>[In Gold Coast] Whale watching with Sea World Cruises</title><link>https://hellocharlie.blog/posts/life/gold-coast-whale-watching/</link><guid isPermaLink="true">https://hellocharlie.blog/posts/life/gold-coast-whale-watching/</guid><description>A photo diary of a Gold Coast whale-watching trip with Sea World Cruises, from the boarding pontoon to whales surfacing beside the coastal skyline.</description><pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I joined Sea World Whale Watch on the Gold Coast on &lt;strong&gt;11 September 2026, at 3 pm&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We got to see both a mother whale and her calf! I felt a little seasick, but the photos and video below capture what we saw out on the water.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/whale-skyline.ApK1rXfA_Z2fkhjn.jpg&quot; alt=&quot;A whale rising out of the sea with Gold Coast apartment towers behind it&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1080&quot; height=&quot;1440&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Whale watching with the city still in view. Photos and video supplied by Charlie.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;finding-the-boat&quot;&gt;Finding the boat&lt;/h2&gt;
&lt;p&gt;The blue Sea World Cruises sign makes the terminal easy to recognise. In the photo below, the entrance is beside the Sea World ferry-stop sign.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/terminal.TRD6HmeA_Z1rqCtL.jpg&quot; alt=&quot;Sea World Cruises terminal entrance beside the Sea World ferry-stop sign&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1080&quot; height=&quot;1440&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The terminal entrance.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The blue-and-white boat is tied up alongside the pontoon. There are open viewing areas on the upper decks, and you can see passengers already waiting on board.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/boat.C7LfI21E_Z1XWBgX.jpg&quot; alt=&quot;Blue-and-white Sea World Cruises boat moored beside the boarding pontoon&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1080&quot; height=&quot;1440&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The boat at the pontoon before heading out.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/boarding.TimDUcFj_Z16XnIS.jpg&quot; alt=&quot;Passengers boarding the Sea World Cruises boat via the gangway&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1080&quot; height=&quot;1440&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A closer look at the boarding area and upper decks.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;whales-above-the-waves&quot;&gt;Whales above the waves&lt;/h2&gt;
&lt;p&gt;Out at sea, the photos show choppy water and whitecaps.&lt;/p&gt;
&lt;p&gt;One catches a whale almost upright above the surface, surrounded by a patch of white water. The beachfront towers are visible behind the whale in the other photo.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/whale-surfacing.B7ScfLRz_Z2efMhs.jpg&quot; alt=&quot;A whale rising almost vertically above choppy water with white spray around it&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1080&quot; height=&quot;1440&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A whale above the surface, with spray hanging over the water.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The short video catches a different moment: a tail lifts above the surface and disappears, followed later by a whale’s back coming into view. The coastline stays in the background throughout the clip.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://hellocharlie.blog/media/gold-coast-whale-watching/whale-watching.mp4&quot;&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/whale-tail.CXHqBDFZ_ZRoUqW.jpg&quot; alt=&quot;A whale’s tail lifted above the water with the coast behind it&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;720&quot; height=&quot;1280&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href=&quot;https://hellocharlie.blog/media/gold-coast-whale-watching/whale-watching.mp4&quot;&gt;Watch the 15-second whale-watching video&lt;/a&gt;. Opens as a video with playback controls and the original sound.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;a-little-seasickness-and-a-jacket-helps&quot;&gt;A little seasickness, and a jacket helps&lt;/h2&gt;
&lt;p&gt;I felt a little seasick during the trip.&lt;/p&gt;
&lt;p&gt;Before departure, seasickness tablets were being sold on board for &lt;strong&gt;A$4&lt;/strong&gt;. That was the price on my visit. Check with the operator for current availability and pricing.&lt;/p&gt;
&lt;p&gt;It can feel a little cold out at sea, so bring a warm layer even if you feel comfortable on shore.&lt;/p&gt;
&lt;h2 id=&quot;before-you-book&quot;&gt;Before you book&lt;/h2&gt;
&lt;p&gt;These are moments from this trip. The photos aren’t a promise of what another sailing will see.&lt;/p&gt;
&lt;p&gt;Check the departure point and arrival instructions on your own booking, and leave room in your plans for any changes the operator announces.&lt;/p&gt;
&lt;p&gt;Sea World Cruises asks guests to arrive about &lt;strong&gt;30 minutes before departure&lt;/strong&gt; and allow &lt;strong&gt;up to 2.5 hours&lt;/strong&gt; for whale watching.&lt;/p&gt;
&lt;p&gt;Its packing advice includes enclosed flat shoes and a warm jacket. Check the &lt;a href=&quot;https://seaworldcruises.com.au/faq/&quot;&gt;operator’s FAQ&lt;/a&gt; when planning your visit.&lt;/p&gt;
&lt;p&gt;The whale-watching departure point is &lt;strong&gt;Sea World Cruises Terminal, Seaworld Drive, Main Beach&lt;/strong&gt;, beside the theme park’s main entrance.&lt;/p&gt;
&lt;p&gt;Use the &lt;a href=&quot;https://seaworldcruises.com.au/getting-here/&quot;&gt;official directions&lt;/a&gt; to plan your journey, and check the &lt;a href=&quot;https://seaworldcruises.com.au/accessibility/sea-world-cruises-terminal/&quot;&gt;terminal guide&lt;/a&gt; if you need access information.&lt;/p&gt;</content:encoded><author>Charlie</author></item><item><title>We Already Have LLMs. Why Does Jev Exist?</title><link>https://hellocharlie.blog/posts/tech/jev-typesafe-structured-decisions/</link><guid isPermaLink="true">https://hellocharlie.blog/posts/tech/jev-typesafe-structured-decisions/</guid><description>Jev answers predefined questions with choices, scores and probabilities. Here is how that differs from a structured-output LLM.</description><pubDate>Thu, 17 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/decision-flow.CBP9aocm_Iwb5g.webp&quot; alt=&quot;Abstract illustration of an incoming message branching into constrained decisions and a human review path.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1440&quot; height=&quot;960&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;AI-generated conceptual illustration. This is not a Jev product screenshot.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://typesafe.ai/blog/introducing-system-one-models-and-jev&quot;&gt;Jev&lt;/a&gt; is a decision model from TypeSafe. The developer defines the questions. Jev returns a choice, a score or a yes probability that application code can use. Modern LLMs can do the same jobs, but Jev is designed around this narrower role.&lt;/p&gt;
&lt;h2 id=&quot;route-a-refund-request&quot;&gt;Route a refund request&lt;/h2&gt;
&lt;p&gt;Suppose a customer writes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I was charged twice and I want my money back.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A support classifier only needs to decide which team should receive the request.&lt;/p&gt;
&lt;p&gt;The developer defines the possible answers first, such as &lt;code&gt;refund&lt;/code&gt;, &lt;code&gt;explain_charge&lt;/code&gt; and &lt;code&gt;other&lt;/code&gt;. Jev selects one. Ordinary code handles the next action.&lt;/p&gt;
&lt;p&gt;The object and &lt;code&gt;0.96&lt;/code&gt; below are invented for explanation. They are not the result of a live Jev call or a TypeSafe SDK shape. The value does not mean 96 per cent accuracy.&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;// Hypothetical application adapter, not a TypeSafe SDK signature.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; supportDecision&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;  category&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;#39;refund&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;  confidence&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; 0.96&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;supportDecision&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;.category &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;#39;refund&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; supportDecision&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;.confidence &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; 0.9&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;  routeToRefundTeam&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;  sendToHumanReview&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The AI classifies the request. It does not issue the refund or choose the &lt;code&gt;0.9&lt;/code&gt; cutoff. Application code decides whether to automate the action or send it to a person. The cutoff is also illustrative and needs testing on the application’s own data.&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;unstructured input&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        ↓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;bounded AI decision&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        ↓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;probabilities and confidence&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        ↓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ordinary if/else code&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;how-this-differs-from-a-structured-output-llm&quot;&gt;How this differs from a structured-output LLM&lt;/h2&gt;
&lt;p&gt;Current LLM APIs also support JSON schemas, tool calls and constrained output. OpenAI’s &lt;a href=&quot;https://developers.openai.com/api/docs/guides/structured-outputs&quot;&gt;Structured Outputs guide&lt;/a&gt; says supported models can follow schemas containing basic types and nested objects.&lt;/p&gt;
&lt;p&gt;Older prompt-only integrations could produce malformed JSON, such as an invalid trailing comma. Current structured-output features greatly reduce that problem. The output may still be wrong in meaning, and a model may refuse the request.&lt;/p&gt;
&lt;p&gt;Jev’s output range is narrower. Its public API offers three decision types: Choice, Score and Noul. For these decisions, it also returns probabilities for the available options.&lt;/p&gt;
&lt;p class=&quot;table-hint&quot; id=&quot;comparison-hint-jev-and-a-modern-structured-output-llm-cover-different-ranges-of-work&quot;&gt;Scroll horizontally to view every column on a small screen.&lt;/p&gt;&lt;div class=&quot;table-scroll&quot; role=&quot;region&quot; aria-label=&quot;Jev and a modern structured-output LLM cover different ranges of work&quot; aria-describedby=&quot;comparison-hint-jev-and-a-modern-structured-output-llm-cover-different-ranges-of-work&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;&lt;caption&gt;Jev and a modern structured-output LLM cover different ranges of work&lt;/caption&gt;&lt;thead&gt;&lt;tr&gt;&lt;th scope=&quot;col&quot;&gt;Question&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;Jev&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;Structured-output LLM&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Output range&lt;/th&gt;&lt;td&gt;Choice, Score or Noul&lt;/td&gt;&lt;td&gt;A supported JSON Schema&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Free strings and nested objects&lt;/th&gt;&lt;td&gt;Cannot generate them directly&lt;/td&gt;&lt;td&gt;Supported&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Probabilities for alternative answers&lt;/th&gt;&lt;td&gt;Returned for bounded Choice and Score decisions&lt;/td&gt;&lt;td&gt;Usually not part of schema conformance itself&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Generate an explanation or reply&lt;/th&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Can be correct in shape but wrong in meaning&lt;/th&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;A structured-output LLM covers more ground when the application needs a flexible response, complicated reasoning, conversation or information whose possible values cannot be listed in advance. Jev may be more direct when the valid answers are known and their probabilities change what the application does next.&lt;/p&gt;
&lt;h2 id=&quot;the-three-answers-jev-can-return&quot;&gt;The three answers Jev can return&lt;/h2&gt;
&lt;p&gt;TypeSafe’s &lt;a href=&quot;https://docs.typesafe.ai/api&quot;&gt;public HTTP API&lt;/a&gt; receives the content to evaluate as &lt;code&gt;state&lt;/code&gt;, along with a model name and questions. &lt;code&gt;state&lt;/code&gt; can be a string, object or array. The documented model name is &lt;code&gt;jev-latest&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Each question uses one of three formats:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Choice&lt;/strong&gt; selects one label from a list supplied by the developer. It returns the selected label, a probability for every label and confidence. The &lt;a href=&quot;https://docs.typesafe.ai/primitives/choice&quot;&gt;Choice documentation&lt;/a&gt; allows up to 255 labels.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Score&lt;/strong&gt; evaluates the input against ordered levels described by the developer. The &lt;a href=&quot;https://docs.typesafe.ai/primitives/score&quot;&gt;Score documentation&lt;/a&gt; says it returns a probability for each level, a probability-weighted score, descriptions of the levels and confidence. The weighted result can be fractional even when the levels use integers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Noul&lt;/strong&gt; answers a yes-or-no question with a number from 0 to 1. That number is the probability of “yes”. According to the &lt;a href=&quot;https://docs.typesafe.ai/primitives/noul&quot;&gt;Noul documentation&lt;/a&gt;, it does not have a separate confidence field.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Jev does not offer a general “fill any JSON Schema” output. It cannot directly generate a free string, an arbitrary nested object or an unrestricted number.&lt;/p&gt;
&lt;p&gt;TypeSafe’s &lt;a href=&quot;https://docs.typesafe.ai/cookbooks/date_extraction_cookbook&quot;&gt;date extraction example&lt;/a&gt; follows the same boundary. Jev chooses date parts, then ordinary code checks the result.&lt;/p&gt;
&lt;h2 id=&quot;high-confidence-can-still-be-wrong&quot;&gt;High confidence can still be wrong&lt;/h2&gt;
&lt;p&gt;A Choice result contains a probability for every option. If the options are &lt;code&gt;refund&lt;/code&gt;, &lt;code&gt;explain_charge&lt;/code&gt; and &lt;code&gt;other&lt;/code&gt;, the response includes three probabilities. Together, those values form a probability distribution.&lt;/p&gt;
&lt;p&gt;Choice and Score also return &lt;code&gt;confidence&lt;/code&gt;. TypeSafe’s &lt;a href=&quot;https://docs.typesafe.ai/confidence&quot;&gt;confidence guide&lt;/a&gt; describes it as a summary of how clearly the probabilities concentrate on one option.&lt;/p&gt;
&lt;p&gt;The fields answer different questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Confidence is not the same value as the highest option probability.&lt;/li&gt;
&lt;li&gt;Confidence is not the probability that the answer is correct.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A high-confidence answer can still be wrong. The &lt;code&gt;0.96&lt;/code&gt; above and &lt;code&gt;0.98&lt;/code&gt; below are invented confidence values, not measurements.&lt;/p&gt;
&lt;p&gt;Whether predicted probabilities match real outcomes is assessed across groups. If predictions given probability 0.8 are well calibrated, about 80 per cent of that group should be correct. It gives no certainty about one prediction. The &lt;a href=&quot;https://scikit-learn.org/stable/modules/calibration.html&quot;&gt;scikit-learn calibration guide&lt;/a&gt; explains the same group-level distinction.&lt;/p&gt;
&lt;p&gt;TypeSafe advises teams to test automation cutoffs on their own data. In the material reviewed for this article, I did not find public evidence showing how well Jev’s probabilities match outcomes in different domains.&lt;/p&gt;
&lt;p&gt;TypeSafe says Jev cannot hallucinate. Its benchmark description also says the reported zero per cent hallucination or type-error figure is not empirical. An accepted response stays within the declared type and labels, yet Jev can still select a valid answer that is wrong.&lt;/p&gt;
&lt;h2 id=&quot;a-transaction-classification-example&quot;&gt;A transaction-classification example&lt;/h2&gt;
&lt;p&gt;Consider this bank transaction description:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;UBER *TRIP HELP.UBER.COM AU&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An application could define five categories:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;transport | food | accommodation | transfer | unknown&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result and &lt;code&gt;0.98&lt;/code&gt; below are invented. They are not a live Jev result or an SDK call shape. High confidence does not guarantee a correct category.&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;// Hypothetical application adapter, not a TypeSafe SDK signature.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; transactionDecision&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;  input&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;#39;UBER *TRIP HELP.UBER.COM AU&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;  category&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;#39;transport&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;  confidence&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; 0.98&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;transactionDecision&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;.confidence &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; 0.9&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;  saveAutomatically&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;transactionDecision&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;.category);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;  sendToHumanReview&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A real financial application would need to consider the cost of a wrong category, review capacity and error rates for different transaction types. The automation cutoff may need to change with the risk of the decision.&lt;/p&gt;
&lt;h2 id=&quot;what-system-one-model-means&quot;&gt;What “System One Model” means&lt;/h2&gt;
&lt;p&gt;TypeSafe calls Jev the first &lt;a href=&quot;https://docs.typesafe.ai/concepts/system-one&quot;&gt;System One Model&lt;/a&gt;. Its launch post says Jev outputs all probabilities in parallel instead of generating one token after another. TypeSafe also says it uses a parallel sampler and Reinforcement Learning for Calibrated Decisions, or RLCD. Its &lt;a href=&quot;https://docs.typesafe.ai/introduction/machine-learning-primer&quot;&gt;machine-learning primer&lt;/a&gt; describes Jev as returning decisions and probabilities instead of text.&lt;/p&gt;
&lt;p&gt;These are vendor descriptions. In the material reviewed for this article, I did not find a paper, model card, weights, model size, input-length limit or training-data description. There was also not enough information to determine whether the internal network is a Transformer or another design. “Non-autoregressive” alone does not reveal that architecture.&lt;/p&gt;
&lt;h2 id=&quot;documented-and-proposed-uses&quot;&gt;Documented and proposed uses&lt;/h2&gt;
&lt;p&gt;Documented examples need to be separated from vendor proposals and hypothetical uses. An example in the documentation does not establish production accuracy or throughput.&lt;/p&gt;
&lt;p class=&quot;table-hint&quot; id=&quot;comparison-hint-status-of-ten-possible-jev-workloads-as-verified-on-16-september-2026&quot;&gt;Scroll horizontally to view every column on a small screen.&lt;/p&gt;&lt;div class=&quot;table-scroll&quot; role=&quot;region&quot; aria-label=&quot;Status of ten possible Jev workloads as verified on 16 September 2026&quot; aria-describedby=&quot;comparison-hint-status-of-ten-possible-jev-workloads-as-verified-on-16-september-2026&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;&lt;caption&gt;Status of ten possible Jev workloads as verified on 16 September 2026&lt;/caption&gt;&lt;thead&gt;&lt;tr&gt;&lt;th scope=&quot;col&quot;&gt;Use case&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;Status and boundary&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Customer-support routing&lt;/th&gt;&lt;td&gt;Documented in TypeSafe’s intent-routing pattern and customer-service evaluation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Transaction classification&lt;/th&gt;&lt;td&gt;Hypothetical. It fits Choice, but the bank example above is not a TypeSafe demo&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Fraud-risk categorisation&lt;/th&gt;&lt;td&gt;Vendor-proposed. The use-case map describes fraud indicators and risk scoring. Domain testing is required&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Content moderation&lt;/th&gt;&lt;td&gt;Documented in TypeSafe’s Choice consistency example&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Lead scoring&lt;/th&gt;&lt;td&gt;Vendor-proposed in the use-case map. This is not validated product performance&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Intent classification&lt;/th&gt;&lt;td&gt;Documented in TypeSafe’s intent-routing pattern&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Document classification&lt;/th&gt;&lt;td&gt;Documented in TypeSafe’s hierarchical-classification example&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;LLM output verification&lt;/th&gt;&lt;td&gt;Documented in TypeSafe’s citation-checking and LLM-guardrail examples&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Workflow routing&lt;/th&gt;&lt;td&gt;Documented through TypeSafe intent routing and confidence-based branching&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;High-volume background processing&lt;/th&gt;&lt;td&gt;Vendor-proposed in the use-case map. Production throughput is unverified&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The official examples include &lt;a href=&quot;https://docs.typesafe.ai/patterns/intent-routing&quot;&gt;intent routing&lt;/a&gt;, &lt;a href=&quot;https://docs.typesafe.ai/cookbooks/consistency_choice_cookbook&quot;&gt;moderation&lt;/a&gt;, &lt;a href=&quot;https://docs.typesafe.ai/cookbooks/hierarchical_classification&quot;&gt;hierarchical classification&lt;/a&gt;, &lt;a href=&quot;https://docs.typesafe.ai/cookbooks/citation_check&quot;&gt;citation checking&lt;/a&gt; and &lt;a href=&quot;https://docs.typesafe.ai/cookbooks/llm_guardrails&quot;&gt;LLM guardrails&lt;/a&gt;. TypeSafe’s &lt;a href=&quot;https://docs.typesafe.ai/concepts/use-case-map&quot;&gt;use-case map&lt;/a&gt; proposes additional applications.&lt;/p&gt;
&lt;h2 id=&quot;what-to-check-before-adopting-it&quot;&gt;What to check before adopting it&lt;/h2&gt;
&lt;p&gt;A team can compare Jev with a smaller structured-output model, a conventional classifier, embeddings or explicit rules. Scikit-learn’s &lt;a href=&quot;https://scikit-learn.org/stable/auto_examples/text/plot_document_classification_20newsgroups.html&quot;&gt;text-classification example&lt;/a&gt; shows a conventional approach. &lt;a href=&quot;https://developers.openai.com/api/docs/guides/embeddings&quot;&gt;Embeddings&lt;/a&gt; can support similarity search and vector-based classification. The comparison should include model calls, retries, human review, data preparation and operating cost.&lt;/p&gt;
&lt;p&gt;TypeSafe’s &lt;a href=&quot;https://evals.typesafe.ai/&quot;&gt;evaluation site&lt;/a&gt; claims low cost and latency. TypeSafe wrote the cases, and the reference probabilities were model-generated. The launch post also acknowledges possible bias from case design and routing. In the official and open-source material reviewed, I did not find an independently reproduced benchmark.&lt;/p&gt;
&lt;p&gt;As of 16 September 2026, Jev is an early-access service that requires an API key. The product and API documentation reviewed did not establish numeric rate limits, a service-level agreement, data-retention terms, deployment options, a maximum &lt;code&gt;state&lt;/code&gt; size or an input-length limit.&lt;/p&gt;
&lt;p&gt;Jev may fit when the valid answers are known before the request and their probabilities change the next action. It does not fit summarisation, conversation, code generation or extraction where the possible answers cannot be listed. Longer reasoning tasks must be split into smaller questions and joined again in application code, which adds development work.&lt;/p&gt;
&lt;p&gt;Before adoption, compare Jev, a structured-output LLM, a smaller model, a conventional classifier, embeddings and rules on the same labelled cases. Measure the error rate, human-review load and cost, then prepare a fallback for failures.&lt;/p&gt;</content:encoded><author>Charlie</author></item><item><title>멜버른 단추·의류 부자재 샵, Fitzroy Jimmy Buttons</title><link>https://hellocharlie.blog/ko/posts/life/jimmy-buttons-fitzroy/</link><guid isPermaLink="true">https://hellocharlie.blog/ko/posts/life/jimmy-buttons-fitzroy/</guid><description>Fitzroy에서 빈티지 소품이나 의류 부자재를 찾는다면? Jimmy Buttons &amp; Haberdashery를 가보세요! 단추부터 지퍼, 버클, 레이스까지!</description><pubDate>Tue, 08 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;남들과 같은 옷에 변화를 주고 싶거나,
현재 입고 있는 옷의 단추가 떨어졌거나,
의류 리폼을 하기 위해 부자재를 찾고 있다면,
Jimmy Buttons &amp;amp; Haberdashery 요기를 들러보세요.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/buttons.D8ms_Tg9_1JS1qQ.jpg&quot; alt=&quot;진열통마다 서로 다른 모양의 단추&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1080&quot; height=&quot;1440&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;진열통 끝에 붙은 단추를 보며 모양을 비교할 수 있어요!&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;단추-샵-in-fitzroy&quot;&gt;단추 샵 in Fitzroy&lt;/h2&gt;
&lt;p&gt;매장 주소: 375 George Street, Fitzroy VIC 3065 &lt;a href=&quot;https://www.jimmybuttons.com.au/&quot;&gt;Jimmy Buttons 공식 홈페이지&lt;/a&gt;에서 주소와 매장 안내를 자세히 볼 수 있습니다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/fitzroy-street.DSrcBI63_HKbOs.jpg&quot; alt=&quot;트램 선로와 전선이 보이는 핏즈로이 거리&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1080&quot; height=&quot;1440&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;가게로 가는 길&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;입구는 파란 벽에 흰 문이 있는 모습이에요.
다른 가게들에 비해 안쪽으로 깊게 들어가 있어서 눈에 안 띌지도 몰라요!&lt;/p&gt;
&lt;p&gt;문에 ENTRANCE라고 적혀 있고, 위에 OPEN이라고 쓰여 있어요!
가게 문이 닫혀 있거나 열리지 않은 것 같아도 열려 있답니다!!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/entrance.CRjuH2va_Z1Fk3Ol.jpg&quot; alt=&quot;파란 벽 사이로 ENTRANCE 글씨와 여러 단추가 붙어 있는 흰색 출입문&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1080&quot; height=&quot;1440&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;단추로 꾸며진 문&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;길을 찾을 때는 &lt;a href=&quot;https://www.google.com/maps/search/?api=1&amp;amp;query=Jimmy+Buttons+375+George+Street+Fitzroy&quot;&gt;지도에서 Jimmy Buttons 위치 보기&lt;/a&gt;를 이용하세요.
출발 위치와 시간에 따라 대중교통 경로가 달라지니, 방문할 때의 길찾기 결과를 확인하는 게 좋습니다 :)&lt;/p&gt;
&lt;h2 id=&quot;모양을-하나씩-비교하게-되는-단추-진열대&quot;&gt;모양을 하나씩 비교하게 되는 단추 진열대&lt;/h2&gt;
&lt;p&gt;단추는 길쭉한 진열통 끝에 샘플을 붙여 놨어요!
통 끝에 붙은 단추는 저마다 모양이 달라요. 표면에 무늬가 있는 것부터 반짝이는 금색·은색 단추까지, 단추가 정~~~말 많아요.&lt;/p&gt;
&lt;p&gt;그리고, 색상별로 단추가 모여 있으니 옷에 달 단추를 찾는다면 기존 단추나 옷 사진을 가져가서 비교해 보세요.
색뿐 아니라 크기와 옷에 다는 방식도 함께 보면 고를 때 도움이 됩니다!&lt;/p&gt;
&lt;h2 id=&quot;지퍼와-버클-레이스도-가득해요&quot;&gt;지퍼와 버클, 레이스도 가득해요&lt;/h2&gt;
&lt;p&gt;단추 외에 의류 부자재도 다양하답니다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/zips.D16uPtpg_p7JRm.jpg&quot; alt=&quot;길이와 색이 다른 지퍼 견본을 붙인 흰 상자들이 쌓인 선반&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1080&quot; height=&quot;1440&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/buckles.PPWNkWHe_B98Dj.jpg&quot; alt=&quot;색색의 바탕에 여러 크기의 버클과 D자 고리가 나뉘어 진열된 벽&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1080&quot; height=&quot;1440&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/lace.BqnMqDF8_Z25f7Ll.jpg&quot; alt=&quot;벽면에 여러 줄로 걸린 흰색과 검은색 레이스 및 장식용 테이프 롤&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1080&quot; height=&quot;1440&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/beads.S2gey9sk_1e2nvU.jpg&quot; alt=&quot;투명 봉투에 담긴 비즈와 작은 장식 재료들이 걸린 진열대&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1080&quot; height=&quot;1440&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;방문-전에-확인할-것&quot;&gt;방문 전에 확인할 것&lt;/h2&gt;
&lt;p&gt;특정 부자재가 목적이라면 크기와 색, 필요한 수량을 정리해서 매장에 먼저 물어보면 친절히 도와주십니다!
사진에 보이는 제품이 방문일에도 있는지는 장담드릴 수 없습니다 😅&lt;/p&gt;
&lt;p&gt;공식 홈페이지 영업시간&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;월요일부터 금요일 / 오전 9시~오후 4시&lt;/li&gt;
&lt;li&gt;토요일 / 오전 10시~오후 3시&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;영업시간과 휴무일은 방문 전에 &lt;a href=&quot;https://www.jimmybuttons.com.au/&quot;&gt;공식 홈페이지&lt;/a&gt;에서 다시 확인해 보시는 걸 추천드려요!&lt;/p&gt;</content:encoded><author>Charlie</author></item><item><title>TypeScript 7 Is “10x Faster.”</title><link>https://hellocharlie.blog/posts/tech/typescript-7-benchmark/</link><guid isPermaLink="true">https://hellocharlie.blog/posts/tech/typescript-7-benchmark/</guid><description>So I Built a Benchmark to test TypeScript 7’s “10x faster” claim.</description><pubDate>Mon, 31 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Project: &lt;a href=&quot;https://github.com/yesimnoh/ts7-benchmark&quot;&gt;https://github.com/yesimnoh/ts7-benchmark&lt;/a&gt;&lt;br/&gt;
Live dashboard: &lt;a href=&quot;https://yesimnoh.github.io/ts7-benchmark/&quot;&gt;https://yesimnoh.github.io/ts7-benchmark/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/benchmark-dashboard.Bj6me0HF_2w4J0G.webp&quot; alt=&quot;TypeScript 7 benchmark dashboard comparing TypeScript 6 and 7 check times across four fixtures, with a 4.1x peak speed-up on the real-world fixture.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1400&quot; height=&quot;964&quot;&gt;&lt;/p&gt;
&lt;p&gt;TypeScript 7 is here.&lt;/p&gt;
&lt;p&gt;And the number everyone remembers is 10x faster.&lt;/p&gt;
&lt;p&gt;That is a big claim.&lt;/p&gt;
&lt;p&gt;So, naturally, I wanted to make a small project and bother it with numbers. I built a benchmark that runs TypeScript 6 and TypeScript 7 against the same code, records the time and memory, and publishes the results to a live dashboard.&lt;/p&gt;
&lt;aside class=&quot;callout notice&quot;&gt;&lt;span class=&quot;callout__icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg viewBox=&quot;0 0 24 24&quot; width=&quot;18&quot; height=&quot;18&quot;&gt;&lt;circle cx=&quot;12&quot; cy=&quot;12&quot; r=&quot;8.5&quot;&gt;&lt;/circle&gt;&lt;path d=&quot;M12 10.75v5&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M12 7.75h.01&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;div class=&quot;callout__content&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;&lt;strong&gt;The result?&lt;/strong&gt;&lt;/p&gt;&lt;div class=&quot;callout__body&quot;&gt;&lt;p&gt;TypeScript 7 was not 10x faster in my test. It was roughly 3–4x faster. That is not a
contradiction. It is a measurement boundary.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;h2 id=&quot;first-what-changed-in-typescript-7&quot;&gt;First, what changed in TypeScript 7?&lt;/h2&gt;
&lt;p&gt;TypeScript 6 and earlier ran on a compiler written in TypeScript and executed through JavaScript.&lt;/p&gt;
&lt;p&gt;TypeScript 7 moves the compiler and language service to a native implementation written in Go.&lt;/p&gt;
&lt;p&gt;This gives the compiler two important advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;native execution&lt;/li&gt;
&lt;li&gt;shared-memory parallelism.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;a href=&quot;https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/&quot;&gt;official TypeScript 7 announcement&lt;/a&gt; reports major full-build improvements across large projects, with examples ranging from about 7.7x to 11.9x faster.&lt;/p&gt;
&lt;p&gt;But “build performance” contains more than one thing.&lt;/p&gt;
&lt;p&gt;There is parsing. There is type-checking. There is emitting output. There may also be multiple projects in a dependency graph.&lt;/p&gt;
&lt;p&gt;My benchmark measures only one part:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tsc --noEmit&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;No JavaScript output. No bundling. No incremental build. No editor startup. Just type-checking.&lt;/p&gt;
&lt;p&gt;That boundary matters.&lt;/p&gt;
&lt;h2 id=&quot;the-benchmark-setup&quot;&gt;The benchmark setup&lt;/h2&gt;
&lt;p&gt;I wanted the project to answer more than “Which command finished first?”&lt;/p&gt;
&lt;p&gt;So I gave it four fixtures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Small synthetic: about 500 lines;&lt;/li&gt;
&lt;li&gt;Medium synthetic: about 15,000 lines;&lt;/li&gt;
&lt;li&gt;Large synthetic: about 120,000 lines;&lt;/li&gt;
&lt;li&gt;Real-world: the open-source validation library Zod, pinned to a specific commit.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The synthetic fixtures generate valid interfaces and generic functions at controlled sizes. They make the workload repeatable.&lt;/p&gt;
&lt;p&gt;But synthetic code is still synthetic code. Real applications contain deeper generics, stranger inference paths, and decisions someone made at 4:57 p.m. on a Friday.&lt;/p&gt;
&lt;p&gt;That is why I included Zod as well.&lt;/p&gt;
&lt;p&gt;For each fixture, the benchmark runs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript 6 once as the baseline;&lt;/li&gt;
&lt;li&gt;TypeScript 7 with &lt;code&gt;--checkers 1&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;TypeScript 7 with &lt;code&gt;--checkers 4&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;TypeScript 7 with &lt;code&gt;--checkers 8&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each timing uses two warmup runs followed by &lt;code&gt;hyperfine&lt;/code&gt;’s timed runs. Peak memory comes from GNU &lt;code&gt;time -v&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The current results were produced with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript 6.0.3;&lt;/li&gt;
&lt;li&gt;TypeScript 7.0.2;&lt;/li&gt;
&lt;li&gt;a GitHub Actions Ubuntu runner;&lt;/li&gt;
&lt;li&gt;4 CPU cores;&lt;/li&gt;
&lt;li&gt;about 16 GB of memory.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The machine is part of the result, not a footnote.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hellocharlie.blog/_astro/benchmark-detailed-results.CzPjZlZG_Xidg1.webp&quot; alt=&quot;Detailed benchmark results listing mean time, variation, and peak memory for TypeScript 6 and TypeScript 7 checker configurations.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1400&quot; height=&quot;1159&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;typescript-7-was-34x-faster&quot;&gt;TypeScript 7 was 3–4x faster&lt;/h2&gt;
&lt;p&gt;Using TypeScript 7 with four checkers, the current mean results look like this:&lt;/p&gt;
&lt;p class=&quot;table-hint&quot; id=&quot;comparison-hint-mean-no-emit-type-checking-time-reported-in-the-benchmark&quot;&gt;Scroll horizontally to view every column on a small screen.&lt;/p&gt;&lt;div class=&quot;table-scroll&quot; role=&quot;region&quot; aria-label=&quot;Mean no-emit type-checking time reported in the benchmark&quot; aria-describedby=&quot;comparison-hint-mean-no-emit-type-checking-time-reported-in-the-benchmark&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;&lt;caption&gt;Mean no-emit type-checking time reported in the benchmark&lt;/caption&gt;&lt;thead&gt;&lt;tr&gt;&lt;th scope=&quot;col&quot;&gt;Fixture&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;TypeScript 6&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;TypeScript 7&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;Speed-up&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Small synthetic (~500 lines)&lt;/th&gt;&lt;td&gt;1.10 s&lt;/td&gt;&lt;td&gt;0.33 s&lt;/td&gt;&lt;td&gt;3.3x&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Medium synthetic (~15,000 lines)&lt;/th&gt;&lt;td&gt;1.25 s&lt;/td&gt;&lt;td&gt;0.38 s&lt;/td&gt;&lt;td&gt;3.3x&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Large synthetic (~120,000 lines)&lt;/th&gt;&lt;td&gt;2.27 s&lt;/td&gt;&lt;td&gt;0.64 s&lt;/td&gt;&lt;td&gt;3.5x&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Zod pinned commit&lt;/th&gt;&lt;td&gt;8.05 s&lt;/td&gt;&lt;td&gt;1.98 s&lt;/td&gt;&lt;td&gt;4.1x&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The synthetic fixtures stayed in a fairly narrow range.&lt;/p&gt;
&lt;p&gt;Zod benefited more.&lt;/p&gt;
&lt;p&gt;That makes sense. Zod relies heavily on complex generic types, so the type checker has more meaningful work to distribute. The important result is not that every codebase becomes exactly four times faster.&lt;/p&gt;
&lt;p&gt;It is that the gain changes with the shape of the code.&lt;/p&gt;
&lt;p&gt;Benchmarks enjoy context almost as much as developers do.&lt;/p&gt;
&lt;h2 id=&quot;then-why-does-microsoft-report-around-10x&quot;&gt;Then why does Microsoft report around 10x?&lt;/h2&gt;
&lt;p&gt;Because we measured different things.&lt;/p&gt;
&lt;p&gt;The official figures cover full builds on large real-world repositories. My test isolates type-checking with &lt;code&gt;--noEmit&lt;/code&gt;, uses one real-world fixture, and runs on a four-core CI machine.&lt;/p&gt;
&lt;p&gt;So these two statements can both be true:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript 7 can make full builds around 10x faster on some large projects.&lt;/li&gt;
&lt;li&gt;Type-checking alone was around 3–4x faster in my benchmark.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A benchmark number without its workload is just a confident-looking decimal.&lt;/p&gt;
&lt;p&gt;The more useful question is not:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Is TypeScript 7 really 10x faster?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Faster at what, on which project, and on what machine?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;--checkers-where-the-result-became-interesting&quot;&gt;&lt;code&gt;--checkers&lt;/code&gt;: where the result became interesting&lt;/h2&gt;
&lt;p&gt;TypeScript 7 introduces the &lt;code&gt;--checkers&lt;/code&gt; option.&lt;/p&gt;
&lt;p&gt;It controls how many checker workers can process the program in parallel. The default is four.&lt;/p&gt;
&lt;p&gt;Before TypeScript 7, type-checking was effectively one worker doing the job alone.&lt;/p&gt;
&lt;p&gt;Now several workers can share it.&lt;/p&gt;
&lt;p&gt;At least, until the hardware runs out of chairs.&lt;/p&gt;
&lt;p&gt;The Zod results show this clearly:&lt;/p&gt;
&lt;p class=&quot;table-hint&quot; id=&quot;comparison-hint-zod-result-by-typescript-7-checker-count&quot;&gt;Scroll horizontally to view every column on a small screen.&lt;/p&gt;&lt;div class=&quot;table-scroll&quot; role=&quot;region&quot; aria-label=&quot;Zod result by TypeScript 7 checker count&quot; aria-describedby=&quot;comparison-hint-zod-result-by-typescript-7-checker-count&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;&lt;caption&gt;Zod result by TypeScript 7 checker count&lt;/caption&gt;&lt;thead&gt;&lt;tr&gt;&lt;th scope=&quot;col&quot;&gt;Configuration&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;Mean time&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;TypeScript 7 — 1 checker&lt;/th&gt;&lt;td&gt;2.62 s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;TypeScript 7 — 4 checkers&lt;/th&gt;&lt;td&gt;1.98 s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;TypeScript 7 — 8 checkers&lt;/th&gt;&lt;td&gt;2.24 s&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Moving from one checker to four helped.&lt;/p&gt;
&lt;p&gt;Moving from four to eight did not.&lt;/p&gt;
&lt;p&gt;The runner only has four CPU cores. Eight checker workers cannot all execute useful work at the same time, so the extra parallelism adds coordination and memory overhead instead of speed.&lt;/p&gt;
&lt;p&gt;More workers do not automatically mean more performance.&lt;/p&gt;
&lt;p&gt;Parallelism still has to negotiate with the machine underneath it.&lt;/p&gt;
&lt;p&gt;On a runner with eight or more cores, the ceiling may move. That is a future benchmark I would like to add.&lt;/p&gt;
&lt;h2 id=&quot;speed-has-a-memory-bill&quot;&gt;Speed has a memory bill&lt;/h2&gt;
&lt;p&gt;The checker count also changes memory usage.&lt;/p&gt;
&lt;p&gt;For Zod, TypeScript 7 used approximately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;734 MB with one checker;&lt;/li&gt;
&lt;li&gt;1,002 MB with four checkers;&lt;/li&gt;
&lt;li&gt;1,222 MB with eight checkers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TypeScript 6 used about 1,206 MB in the same fixture.&lt;/p&gt;
&lt;p&gt;So one checker used much less memory, while eight checkers slightly exceeded the TypeScript 6 baseline without improving the execution time.&lt;/p&gt;
&lt;p&gt;This is the trade-off. More checkers can reduce waiting time when enough cores are available. They also need more memory.&lt;/p&gt;
&lt;p&gt;For a developer laptop, four may be a reasonable default.&lt;/p&gt;
&lt;p&gt;For a memory-constrained CI runner, one may be the better deal.&lt;/p&gt;
&lt;p&gt;The fastest setting is not always the cheapest setting.&lt;/p&gt;
&lt;h2 id=&quot;making-the-benchmark-reproducible&quot;&gt;Making the benchmark reproducible&lt;/h2&gt;
&lt;p&gt;I did not want this to become one screenshot from one successful run.&lt;/p&gt;
&lt;p&gt;The repository pins both compiler versions and the exact Zod commit. It stores the raw timing and memory results, then merges them into a single &lt;code&gt;summary.json&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;A scheduled GitHub Actions workflow runs the benchmark every week. It:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;records the runner specification;&lt;/li&gt;
&lt;li&gt;installs both compilers;&lt;/li&gt;
&lt;li&gt;regenerates the synthetic fixtures;&lt;/li&gt;
&lt;li&gt;runs every fixture and checker combination;&lt;/li&gt;
&lt;li&gt;commits the new results;&lt;/li&gt;
&lt;li&gt;deploys the dashboard to GitHub Pages.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The numbers can change.&lt;/p&gt;
&lt;p&gt;The method should not quietly change with them.&lt;/p&gt;
&lt;h2 id=&quot;what-this-benchmark-does-not-prove&quot;&gt;What this benchmark does not prove&lt;/h2&gt;
&lt;p&gt;There are still clear limitations.&lt;/p&gt;
&lt;p&gt;GitHub Actions runners are shared virtual machines, so absolute timings contain noise. The ratios and trends are more useful than one millisecond value.&lt;/p&gt;
&lt;p&gt;The synthetic fixtures stress code volume, but they cannot reproduce the type patterns of every application.&lt;/p&gt;
&lt;p&gt;There is only one real-world project in the current suite. And again, this benchmark measures type-checking only. It says nothing directly about emit time, incremental builds, bundling, or editor responsiveness.&lt;/p&gt;
&lt;p&gt;That is not a reason to ignore the result.&lt;/p&gt;
&lt;p&gt;It is a reason to label it correctly.&lt;/p&gt;
&lt;h2 id=&quot;so-is-typescript-7-faster&quot;&gt;So, is TypeScript 7 faster?&lt;/h2&gt;
&lt;p&gt;Yes.&lt;/p&gt;
&lt;p&gt;Clearly.&lt;/p&gt;
&lt;p&gt;In this benchmark, TypeScript 7 reduced type-checking time by roughly three to four times. On the real-world Zod fixture, four checkers produced the best result on a four-core runner.&lt;/p&gt;
&lt;p&gt;The famous 10x number was not wrong.&lt;/p&gt;
&lt;p&gt;It was answering a broader question.&lt;/p&gt;
&lt;p&gt;My benchmark asked a smaller one:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If I isolate type-checking, how much faster is TypeScript 7 here?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The answer was 3–4x.&lt;/p&gt;
&lt;p&gt;Not as loud as 10x.&lt;/p&gt;
&lt;p&gt;Still fast enough to change the developer experience. And now there is a dashboard that can keep checking.&lt;/p&gt;
&lt;p&gt;Official TypeScript 7 announcement: &lt;a href=&quot;https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/&quot;&gt;https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/&lt;/a&gt;&lt;/p&gt;</content:encoded><author>Charlie</author></item></channel></rss>