<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Jose Flores | Writing</title><description>Writing on frontend architecture, statecharts, and the actor model.</description><link>https://0xjcf.com/</link><language>en</language><item><title>Lifecycle Boundaries in Actor and State Machine Architecture</title><link>https://0xjcf.com/writing/lifecycle-is-the-real-boundary/</link><guid isPermaLink="true">https://0xjcf.com/writing/lifecycle-is-the-real-boundary/</guid><description>How moving browser navigation beyond an Ignite effect revealed the lifecycle owner for observation, commits, failures, and cleanup.</description><pubDate>Thu, 30 Jul 2026 00:00:04 GMT</pubDate><content:encoded>&lt;figure&gt;&lt;img src=&quot;https://0xjcf.com/assets/images/articles/lifecycle-is-the-real-boundary-v1.jpg&quot; alt=&quot;A charcoal enclosure surrounds a red architectural structure and a path that circulates through one opening.&quot; width=&quot;1734&quot; height=&quot;907&quot; /&gt;&lt;/figure&gt;&lt;p&gt;The functional core gave the router one place to decide which route the application accepts.&lt;/p&gt;
&lt;p&gt;I still had to connect that decision to the browser.&lt;/p&gt;
&lt;p&gt;I was using a newer Navigation API iteration of the beta router examples to test that boundary against native navigation.&lt;/p&gt;
&lt;p&gt;My first version used an Ignite effect. When the accepted route changed, the effect called the browser’s Navigation API. That preserved an important boundary: the resolver decided the route without importing &lt;code&gt;window&lt;/code&gt;, and the effect handled the outward update.&lt;/p&gt;
&lt;p&gt;For a single rendered element, that approach felt natural.&lt;/p&gt;
&lt;p&gt;The problem became clearer when I followed everything else the router had to do. It needed to read the initial path, observe browser navigation, receive requests from several surfaces, commit accepted paths, record failures, and remove the observer when it stopped.&lt;/p&gt;
&lt;p&gt;The route element was only one projection of that behavior. It was not the lifetime owner of the router.&lt;/p&gt;
&lt;h2 id=&quot;the-effect-solved-direction-not-ownership&quot;&gt;The effect solved direction, not ownership&lt;/h2&gt;
&lt;p&gt;The effect established the direction of the dependency. Router state changed first, then browser navigation followed.&lt;/p&gt;
&lt;p&gt;It did not answer which running part of the system owned the browser relationship.&lt;/p&gt;
&lt;p&gt;An earlier version started the shared router actor in one module and registered the browser observer beside it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; const&lt;/span&gt;&lt;span&gt; routerActor&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; createActor&lt;/span&gt;&lt;span&gt;(routerMachine, {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  input: { path: &lt;/span&gt;&lt;span&gt;currentPath&lt;/span&gt;&lt;span&gt;() },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}).&lt;/span&gt;&lt;span&gt;start&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;onPopState&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span&gt;path&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  routerActor.&lt;/span&gt;&lt;span&gt;send&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    type: &lt;/span&gt;&lt;span&gt;&quot;POPSTATE&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    path,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Ignite effect handled the opposite direction by writing accepted paths back to browser history.&lt;/p&gt;
&lt;p&gt;Each piece worked, but their lifetimes did not line up.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;onPopState&lt;/code&gt; returned an unsubscribe function, and this call discarded it. Stopping the actor did not remove the observer. The Ignite effect belonged to a route element even though the shared actor and browser observer could outlive that element.&lt;/p&gt;
&lt;p&gt;The nested router made this harder to ignore. Parent navigation, documentation sections, and settings panels all projected the same router source. If one of those elements connected or disconnected, it should not decide whether the application router was still observing the browser.&lt;/p&gt;
&lt;p&gt;The question that helped me move forward was not, “Where should this code live?”&lt;/p&gt;
&lt;p&gt;It was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Which parts of this behavior must start and stop together?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The router state, browser observation, accepted-path commits, failure handling, and cleanup described one running lifecycle. They needed an owner whose lifetime contained all of them.&lt;/p&gt;
&lt;h2 id=&quot;removing-host-made-the-next-boundary-visible&quot;&gt;Removing &lt;code&gt;host&lt;/code&gt; made the next boundary visible&lt;/h2&gt;
&lt;p&gt;The earlier API change also mattered here.&lt;/p&gt;
&lt;p&gt;Once &lt;code&gt;host&lt;/code&gt; was removed from &lt;code&gt;igniteCore&lt;/code&gt; command and effect callbacks on the v3 beta line, the custom element could no longer act as an implicit path to browser capabilities. Commands received explicit application inputs. Effects had to use explicitly provided capabilities instead of discovering them through the element.&lt;/p&gt;
&lt;p&gt;That was intentional, but it raised another design question: how should the router receive navigation without turning Ignite Element into a browser integration framework?&lt;/p&gt;
&lt;p&gt;I considered introducing another Ignite abstraction, something like a driver or &lt;code&gt;igniteEnvironment&lt;/code&gt;. The idea was to give effects and sources a standard place to find browser, storage, network, or other environment capabilities.&lt;/p&gt;
&lt;p&gt;The more I worked through it, the less it fit.&lt;/p&gt;
&lt;p&gt;Ignite Element already has a behavioral input: the source. That source may be an XState machine or actor, a Redux store, a MobX object, or another supported runtime. Adding a second behavior runtime to &lt;code&gt;igniteCore&lt;/code&gt; would create more configuration while making Ignite Element responsible for how every state library provisions external capabilities.&lt;/p&gt;
&lt;p&gt;XState already has its own composition tools, including machine provisioning with &lt;code&gt;.provide()&lt;/code&gt;. Other sources have their own construction patterns. Ignite Element does not need to replace them.&lt;/p&gt;
&lt;p&gt;The direction we landed on was simpler: provide the environment capability while constructing the source, then give the resulting source to &lt;code&gt;igniteCore&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For the router, that capability became &lt;code&gt;NavigationPort&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-source-owns-navigation-over-time&quot;&gt;The source owns navigation over time&lt;/h2&gt;
&lt;p&gt;The navigation port answers three questions the source has about its environment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What is the current path?&lt;/li&gt;
&lt;li&gt;How can the router observe external path changes?&lt;/li&gt;
&lt;li&gt;How can it commit a path the application accepted?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The browser implementation uses &lt;code&gt;window.navigation&lt;/code&gt;. A memory implementation provides the same capability for headless tests. Neither implementation owns route matching, authentication redirects, or nested route policy.&lt;/p&gt;
&lt;p&gt;Those decisions remain in the project resolver.&lt;/p&gt;
&lt;p&gt;The source uses the port to assemble the running behavior. Its observer is expressed as XState callback actor logic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;observeNavigation&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;fromCallback&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;RouterEvent&lt;/span&gt;&lt;span&gt;&amp;gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ({ &lt;/span&gt;&lt;span&gt;sendBack&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    navigation.&lt;/span&gt;&lt;span&gt;observe&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span&gt;path&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      sendBack&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        type: &lt;/span&gt;&lt;span&gt;&quot;NAVIGATION_OBSERVED&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        path,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      }),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;navigation.observe&lt;/code&gt; returns its cleanup function. Because the router invokes this callback actor at its root, observation begins when the router starts and cleanup runs when the router stops.&lt;/p&gt;
&lt;p&gt;That is the lifecycle boundary I was missing in the first version.&lt;/p&gt;
&lt;figure&gt;&lt;div&gt;&lt;img src=&quot;https://0xjcf.com/assets/diagrams/1023d1fa2f7f586b51fd.svg&quot; alt=&quot;Architecture diagram with: Browser navigation; NavigationPort; Source-owned observer; Router actor; Route resolver.&quot; width=&quot;1387&quot; height=&quot;102&quot; /&gt;&lt;/div&gt;&lt;figcaption&gt;Architecture diagram · Scroll horizontally to explore.&lt;a href=&quot;https://0xjcf.com/assets/diagrams/1023d1fa2f7f586b51fd.svg&quot;&gt;Open full size &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;The Ignite effect was useful because it showed that the browser update belonged outside the deterministic resolver. Moving navigation under the source answered the next question: the router’s running owner, not the rendered element, should coordinate that update with observation and cleanup.&lt;/p&gt;
&lt;p&gt;Ignite Element remains the projection boundary. It receives the source, derives the view, exposes commands, and renders components. The source owns behavior that must remain coherent even when a particular projection disconnects.&lt;/p&gt;
&lt;h2 id=&quot;the-environment-can-change-without-rebuilding-the-router&quot;&gt;The environment can change without rebuilding the router&lt;/h2&gt;
&lt;p&gt;Once navigation was supplied to the source, the same router behavior could run with browser and memory implementations:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; browserSource&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; createRouterSource&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  navigation: &lt;/span&gt;&lt;span&gt;createBrowserNavigation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    resolveBrowserNavigation&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; memory&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; createMemoryNavigation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; headlessSource&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; createRouterSource&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  navigation: memory.port,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was important for dogfooding Ignite Element. The &lt;a href=&quot;https://0xjcf.github.io/ignite-element/api/headless-runtime/&quot;&gt;headless runtime&lt;/a&gt; should be able to exercise the same router behavior without fabricating a custom element or pretending to be a browser.&lt;/p&gt;
&lt;p&gt;The memory implementation can report an externally observed path. It can also record paths the router asks it to commit. The browser implementation translates those operations into the native Navigation API.&lt;/p&gt;
&lt;p&gt;Changing the implementation does not move route policy. It also does not move the observer lifetime. Both remain under the same source.&lt;/p&gt;
&lt;p&gt;This is where ports became useful in a way that felt less abstract to me. The port is not another layer added for ceremony. It is the application-facing shape of a capability the source needs in more than one environment.&lt;/p&gt;
&lt;p&gt;The adapter answers, “How does navigation work here?” The resolver answers, “Which route does this application accept?” The actor keeps those answers coherent as events arrive over time.&lt;/p&gt;
&lt;h2 id=&quot;the-current-commit-protocol-has-a-limit&quot;&gt;The current commit protocol has a limit&lt;/h2&gt;
&lt;p&gt;The browser’s Navigation API exposes more lifecycle detail than the current router port preserves. A call to &lt;code&gt;navigation.navigate()&lt;/code&gt; provides separate &lt;code&gt;committed&lt;/code&gt; and &lt;code&gt;finished&lt;/code&gt; promises. One represents the URL and history entry changing. The other represents the navigation finishing, including intercepted work.&lt;/p&gt;
&lt;p&gt;Our current &lt;code&gt;NavigationPort.commit()&lt;/code&gt; returns &lt;code&gt;Promise&amp;lt;void&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The router resolves the accepted route, updates its context, and asks the port to commit the path. If the promise rejects, the source records the failure. If it resolves, the source does not receive a separate application fact.&lt;/p&gt;
&lt;p&gt;That means router context currently represents the route accepted by application policy. It does not prove that the environment confirmed every part of the navigation lifecycle.&lt;/p&gt;
&lt;p&gt;I think this is an important limit to keep visible without designing a larger protocol before the example needs one.&lt;/p&gt;
&lt;p&gt;If a future application needs to distinguish accepted, browser-committed, and fully-finished navigation, the port and actor protocol will need to preserve the milestone that matters. We would also have to decide what happens when a second request arrives before the first one finishes and whether abandoned work can be cancelled.&lt;/p&gt;
&lt;p&gt;The current dogfooding projects do not answer those questions. They demonstrate optimistic context updates, commit rejection handling, source-owned observation, and cleanup.&lt;/p&gt;
&lt;p&gt;That is enough evidence for the boundary we changed. It is not evidence for a pending-route or confirmed-navigation protocol we have not implemented.&lt;/p&gt;
&lt;h2 id=&quot;why-the-actor-owns-this-behavior&quot;&gt;Why the actor owns this behavior&lt;/h2&gt;
&lt;p&gt;The resolver can remain a function because its work ends when it returns an accepted route.&lt;/p&gt;
&lt;p&gt;The router cannot end there. It remembers state, receives requests from multiple navigation surfaces, observes the browser while running, commits accepted paths, records failures, and releases its observer when it stops.&lt;/p&gt;
&lt;p&gt;I use an actor because those responsibilities already form a protocol over time. The meaning of a request depends on current route and authentication state. Observation can arrive between application requests. Cleanup has to occur even if no route element remains connected.&lt;/p&gt;
&lt;p&gt;That does not mean every router needs XState. An imperative controller with &lt;code&gt;start()&lt;/code&gt;, &lt;code&gt;navigate()&lt;/code&gt;, and &lt;code&gt;stop()&lt;/code&gt; could own the same lifecycle in a smaller application.&lt;/p&gt;
&lt;p&gt;The architectural requirement is narrower: the behavior that must remain coherent from start to stop needs one owner with the right lifetime.&lt;/p&gt;
&lt;p&gt;For Ignite Element, this also protects the library boundary. &lt;code&gt;igniteCore&lt;/code&gt; does not need &lt;code&gt;host&lt;/code&gt; in commands or effects, and it does not need a new environment runtime. It receives a source whose behavior and capabilities were already composed using the tools native to that source.&lt;/p&gt;
&lt;p&gt;That feels closer to the framework-agnostic design I want. Ignite Element projects behavior without quietly becoming its lifecycle authority.&lt;/p&gt;
&lt;h2 id=&quot;next-in-the-series&quot;&gt;Next in the series&lt;/h2&gt;
&lt;p&gt;The router source now depends on a navigation capability without depending on &lt;code&gt;window.navigation&lt;/code&gt; directly. The browser and memory implementations can change while route policy and lifecycle ownership remain in the same place.&lt;/p&gt;
&lt;p&gt;The next article examines that capability boundary more closely: what belongs in a port, what belongs in an adapter, and how external values become application meaning without moving policy into the integration.&lt;/p&gt;
&lt;p&gt;If you want to follow this work in the project itself, the &lt;a href=&quot;https://0xjcf.github.io/ignite-element/&quot;&gt;Ignite Element v3 beta documentation&lt;/a&gt; describes the current runtime model, and the &lt;a href=&quot;https://github.com/0xjcf/ignite-element/tree/beta&quot;&gt;beta branch on GitHub&lt;/a&gt; contains the source and dogfooding examples behind this series.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://0xjcf.com/writing/lifecycle-is-the-real-boundary/&quot;&gt;Read on 0xjcf.com&lt;/a&gt;&lt;/p&gt;</content:encoded><dc:creator>Jose Flores</dc:creator><media:content url="https://0xjcf.com/assets/images/articles/lifecycle-is-the-real-boundary-v1.jpg" medium="image" type="image/jpeg" width="1734" height="907"/></item><item><title>Functional Core Architecture: Where Deterministic Decisions Belong</title><link>https://0xjcf.com/writing/functional-core/</link><guid isPermaLink="true">https://0xjcf.com/writing/functional-core/</guid><description>How building an Ignite Element router with the browser&apos;s Navigation API led me to separate semantic commands, deterministic route policy, and environment effects.</description><pubDate>Thu, 30 Jul 2026 00:00:03 GMT</pubDate><content:encoded>&lt;figure&gt;&lt;img src=&quot;https://0xjcf.com/assets/images/articles/functional-core-v1.jpg&quot; alt=&quot;A precise red geometric core sits in a clear space, separated from a ring of irregular charcoal forms.&quot; width=&quot;1732&quot; height=&quot;908&quot; /&gt;&lt;/figure&gt;&lt;p&gt;The previous article ended with a semantic navigation request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;NAVIGATE_REQUESTED&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;to&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;/dashboard&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That gave the router a stable message. A link, a test, or another interface could request the same destination without sending a click event or reconstructing browser markup.&lt;/p&gt;
&lt;p&gt;I thought the next step would be straightforward. I wanted to build the router around the browser’s &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API&quot;&gt;Navigation API&lt;/a&gt;, which is now available as &lt;code&gt;window.navigation&lt;/code&gt;. It gives applications one place to observe, intercept, and initiate navigation without rebuilding everything around &lt;code&gt;popstate&lt;/code&gt; and the History API.&lt;/p&gt;
&lt;p&gt;It seemed like a good project for dogfooding the &lt;a href=&quot;https://0xjcf.github.io/ignite-element/&quot;&gt;Ignite Element v3 beta&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The beta already contained SPA and nested-router examples built around the History API. I used the Navigation API iteration described here to revisit the same route-policy and lifecycle questions.&lt;/p&gt;
&lt;p&gt;What I did not know was where the browser’s responsibility should end.&lt;/p&gt;
&lt;p&gt;The Navigation API could tell me which destination was requested. It could update browser history and report how navigation progressed. It could not decide whether &lt;code&gt;/dashboard&lt;/code&gt; was a route this application recognized or whether a signed-out user should be redirected to &lt;code&gt;/login&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I had started with one routing problem. The native API made me see at least two:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What route does the application accept?&lt;/li&gt;
&lt;li&gt;How does the current environment observe and commit that route?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first question is a decision. The second is a capability and a lifecycle.&lt;/p&gt;
&lt;h2 id=&quot;the-first-bridge-was-an-ignite-effect&quot;&gt;The first bridge was an Ignite effect&lt;/h2&gt;
&lt;p&gt;My first instinct was to keep the browser update close to Ignite Element. A command would carry the request to the router actor, and an Ignite effect would observe the resulting snapshot and update the Navigation API.&lt;/p&gt;
&lt;p&gt;A reduced version of that direction looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; browserNavigation&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; window.navigation;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; routerCore&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; igniteCore&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  source: routerActor,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  commands&lt;/span&gt;&lt;span&gt;: ({ &lt;/span&gt;&lt;span&gt;actor&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; ({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    navigate&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;to&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      actor.&lt;/span&gt;&lt;span&gt;send&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        type: &lt;/span&gt;&lt;span&gt;&quot;NAVIGATE_REQUESTED&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        to,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  effects&lt;/span&gt;&lt;span&gt;: ({ &lt;/span&gt;&lt;span&gt;snapshot&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;prevSnapshot&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; nextPath&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; snapshot.context.route.path;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; previousPath&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; prevSnapshot.context.route.path;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (nextPath &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; previousPath) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      void&lt;/span&gt;&lt;span&gt; browserNavigation.&lt;/span&gt;&lt;span&gt;navigate&lt;/span&gt;&lt;span&gt;(nextPath).finished;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was a reasonable place to begin. The command expressed intent. The effect synchronized accepted router state with the browser. The machine did not import &lt;code&gt;window&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It also exposed a problem in Ignite Element’s API.&lt;/p&gt;
&lt;p&gt;At the time, command and effect callbacks received the custom element as &lt;code&gt;host&lt;/code&gt;. That made it possible for a command to discover intent from &lt;code&gt;host.dataset&lt;/code&gt;, which was the problem the &lt;code&gt;startModule&lt;/code&gt; example exposed earlier in the series. It also gave effects a convenient path back into the element and its browser environment.&lt;/p&gt;
&lt;p&gt;The router made the broader issue easier to see. A navigation effect did not need a rendered element. It needed a navigation capability. Giving it &lt;code&gt;host&lt;/code&gt; encouraged the component to become a service locator for whichever browser details the effect wanted to reach.&lt;/p&gt;
&lt;p&gt;That pressure led me to remove &lt;code&gt;host&lt;/code&gt; from the &lt;code&gt;igniteCore&lt;/code&gt; command and effect callbacks on the v3 beta line. This was not only a preference in the router example. It became part of the beta API.&lt;/p&gt;
&lt;p&gt;The custom element still owns rendering and browser interaction at its boundary. What changed is that core callbacks no longer receive the element simply because one happens to exist. A command must receive the application value it needs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;commands&lt;/span&gt;&lt;span&gt;: ({ &lt;/span&gt;&lt;span&gt;actor&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; ({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  navigate&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;to&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    actor.&lt;/span&gt;&lt;span&gt;send&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      type: &lt;/span&gt;&lt;span&gt;&quot;NAVIGATE_REQUESTED&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      to,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://0xjcf.github.io/ignite-element/&quot;&gt;v3 beta documentation&lt;/a&gt; introduces the current runtime, and the &lt;a href=&quot;https://github.com/0xjcf/ignite-element/tree/beta&quot;&gt;beta branch on GitHub&lt;/a&gt; contains the implementation and dogfooding projects as they evolve.&lt;/p&gt;
&lt;p&gt;An effect that needs an environment capability must receive or close over that capability explicitly. It should not discover the environment by reaching sideways through the rendered host.&lt;/p&gt;
&lt;p&gt;Removing &lt;code&gt;host&lt;/code&gt; did not solve routing by itself. It made the unanswered responsibility visible.&lt;/p&gt;
&lt;p&gt;The effect knew which accepted path to send to the browser because router state already contained one. Something still had to decide which path belonged in that state.&lt;/p&gt;
&lt;h2 id=&quot;the-browser-request-was-not-the-application-decision&quot;&gt;The browser request was not the application decision&lt;/h2&gt;
&lt;p&gt;Consider an unauthenticated request for &lt;code&gt;/dashboard&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The browser can report &lt;code&gt;/dashboard&lt;/code&gt; as the destination. The semantic command can carry that path to the router. Neither one has enough authority to say whether the application accepts it.&lt;/p&gt;
&lt;p&gt;That answer depends on application facts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which routes exist?&lt;/li&gt;
&lt;li&gt;Which route matched the requested path?&lt;/li&gt;
&lt;li&gt;Does it require authentication?&lt;/li&gt;
&lt;li&gt;Is the current user authenticated?&lt;/li&gt;
&lt;li&gt;Should the application redirect to &lt;code&gt;/login&lt;/code&gt;?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I initially described this as “policy equals decisions.” That is close, but it helps to be more specific. Policy is the rule that chooses an answer from the current state and facts. The state transition records that answer. The running actor then carries the behavior forward over time.&lt;/p&gt;
&lt;p&gt;For the &lt;a href=&quot;https://github.com/0xjcf/ignite-element/tree/beta/examples/apps/spa-router&quot;&gt;SPA router dogfooding project&lt;/a&gt;, the smallest useful policy became a pure function named &lt;code&gt;resolveNavigation&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; requiresAuth&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; RouteName&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; boolean&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  routes.&lt;/span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span&gt;route&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; route.name &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; name)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ?.requiresAuth &lt;/span&gt;&lt;span&gt;??&lt;/span&gt;&lt;span&gt; false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; const&lt;/span&gt;&lt;span&gt; resolveNavigation&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  toPath&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  authed&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; boolean&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Resolved&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; match&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; matchRoute&lt;/span&gt;&lt;span&gt;(toPath);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;requiresAuth&lt;/span&gt;&lt;span&gt;(match.name) &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; !&lt;/span&gt;&lt;span&gt;authed) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; login&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; matchRoute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;LOGIN_PATH&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      path: login.path,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      route: login.name,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      params: login.params,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      redirected: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    };&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    path: match.path,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    route: match.name,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    params: match.params,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    redirected: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  };&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The route table, dynamic path matching, and authentication redirect are application policy in this project. &lt;code&gt;resolveNavigation&lt;/code&gt; does not observe &lt;code&gt;window.navigation&lt;/code&gt;, inspect an element, or commit browser history. It receives the requested path and authentication fact, then returns the route the application accepts.&lt;/p&gt;
&lt;p&gt;That gives the decision a small test surface:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  resolveNavigation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/dashboard&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;).route,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;login&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  resolveNavigation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/dashboard&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;).route,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;dashboard&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  resolveNavigation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/missing&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;).route,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;not-found&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These tests do not prove that the browser navigated. They prove that the same path and authentication state produce the same application answer.&lt;/p&gt;
&lt;p&gt;That is what made the phrase functional core useful to me. I am not using it to mean that the whole application must adopt functional programming. I mean the deterministic part of the system that owns an application decision.&lt;/p&gt;
&lt;p&gt;The effect can perform an outward update. The Navigation API can perform browser navigation. Neither one should quietly create route policy while doing that work.&lt;/p&gt;
&lt;h2 id=&quot;the-nested-router-tested-the-same-boundary-again&quot;&gt;The nested router tested the same boundary again&lt;/h2&gt;
&lt;p&gt;The SPA example gave me a clean resolver, but I wondered whether the boundary would hold once navigation was divided across parent and child surfaces.&lt;/p&gt;
&lt;p&gt;That became the &lt;a href=&quot;https://github.com/0xjcf/ignite-element/tree/beta/examples/apps/nested-child-router&quot;&gt;nested child-router dogfooding project&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The name can make it sound like a child router runs inside a parent router. That is not how the example works. One application-owned XState router source is shared by the parent and child outlets. Parent navigation, documentation sections, and settings panels expose commands scoped to the choices available in each surface, but all of those requests reach the same router actor.&lt;/p&gt;
&lt;p&gt;The nested resolver owns a different route policy. Instead of applying authentication redirects, it maps a root-relative path into the &lt;code&gt;parent&lt;/code&gt;, &lt;code&gt;child&lt;/code&gt;, and &lt;code&gt;label&lt;/code&gt; the outlets need.&lt;/p&gt;
&lt;p&gt;The important result was not that both projects shared one generic resolver. They do not. Each project owns a stable, closed route policy for the behavior it is testing.&lt;/p&gt;
&lt;p&gt;What they share is the responsibility split:&lt;/p&gt;
&lt;figure&gt;&lt;div&gt;&lt;img src=&quot;https://0xjcf.com/assets/diagrams/86d6e75cb069ddc920b7.svg&quot; alt=&quot;Architecture diagram with: Link, test, or scoped navigation surface; Semantic navigation command; Router actor; Deterministic resolver; Environment effect; Navigation API.&quot; width=&quot;1405&quot; height=&quot;144&quot; /&gt;&lt;/div&gt;&lt;figcaption&gt;Architecture diagram · Scroll horizontally to explore.&lt;a href=&quot;https://0xjcf.com/assets/diagrams/86d6e75cb069ddc920b7.svg&quot;&gt;Open full size &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Different surfaces may offer different commands. Different projects may apply different route rules. The route decision still has one application authority.&lt;/p&gt;
&lt;p&gt;This answered one of the questions I had while working through ports and policies. A port does not make the decision deterministic. The resolver does that. A port gives the running behavior a provider-neutral way to reach the environment after the decision is made.&lt;/p&gt;
&lt;h2 id=&quot;what-the-functional-core-changed&quot;&gt;What the functional core changed&lt;/h2&gt;
&lt;p&gt;Before this project, it was easy for me to think about routing as one feature. The application receives a path and the browser moves.&lt;/p&gt;
&lt;p&gt;Dogfooding the Navigation API forced me to slow that sentence down.&lt;/p&gt;
&lt;p&gt;The interface captures a destination. The command carries the request. The resolver applies route policy. The actor records the accepted route. An environment capability performs the browser work.&lt;/p&gt;
&lt;p&gt;Those steps can live close together in a small implementation. They still own different decisions.&lt;/p&gt;
&lt;p&gt;This also explains why removing &lt;code&gt;host&lt;/code&gt; from &lt;code&gt;igniteCore&lt;/code&gt; mattered. The change was not only about making headless tests easier. It prevented command and effect callbacks from treating the current custom element as the source of application intent or environment capability.&lt;/p&gt;
&lt;p&gt;The current router source still uses an optimistic commit. It updates router context with the accepted route, asks its navigation capability to commit the path, and records a failure if that promise rejects. A successful commit does not return an application fact, and the source does not keep separate pending and confirmed routes.&lt;/p&gt;
&lt;p&gt;I do not want to describe a stronger protocol than the example implements. The resolver gives us a deterministic application decision. It does not prove that the browser and application state have been reconciled after every commit.&lt;/p&gt;
&lt;p&gt;That limit led to the next question.&lt;/p&gt;
&lt;p&gt;The Ignite effect could update the browser after router state changed, but the router also needed to observe browser navigation, own the subscription, handle commit failures, and clean everything up when it stopped. Those responsibilities did not belong to one route decision, and tying them to whichever element rendered the route gave them the wrong lifetime.&lt;/p&gt;
&lt;p&gt;The next post, &lt;a href=&quot;https://0xjcf.com/writing/lifecycle-is-the-real-boundary/&quot;&gt;&lt;em&gt;Lifecycle Boundaries in Actor and State Machine Architecture&lt;/em&gt;&lt;/a&gt;, follows the router from the Ignite effect into the running source that owns observation, commits, failure, and cleanup.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://0xjcf.com/writing/functional-core/&quot;&gt;Read on 0xjcf.com&lt;/a&gt;&lt;/p&gt;</content:encoded><dc:creator>Jose Flores</dc:creator><media:content url="https://0xjcf.com/assets/images/articles/functional-core-v1.jpg" medium="image" type="image/jpeg" width="1732" height="908"/></item><item><title>Semantic Commands: Translating User Intent into Application Behavior</title><link>https://0xjcf.com/writing/narrative-to-semantic-command/</link><guid isPermaLink="true">https://0xjcf.com/writing/narrative-to-semantic-command/</guid><description>How to translate a product narrative into a precise application request without carrying interface details into behavior.</description><pubDate>Thu, 30 Jul 2026 00:00:02 GMT</pubDate><content:encoded>&lt;figure&gt;&lt;img src=&quot;https://0xjcf.com/assets/images/articles/narrative-to-semantic-command-v1.jpg&quot; alt=&quot;Loose charcoal ribbons converge through a red gate into a single precisely folded token.&quot; width=&quot;1734&quot; height=&quot;907&quot; /&gt;&lt;/figure&gt;&lt;p&gt;A product narrative is not yet a runtime contract.&lt;/p&gt;
&lt;p&gt;Consider the narrative from the previous article:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A developer wants to start a selected module while exercising the application from different interfaces. They should be able to identify the module directly instead of reconstructing how one browser view stores that value.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The paragraph preserves the person, their desired progress, and the presentation detail we want to keep outside the application contract.&lt;/p&gt;
&lt;p&gt;But an actor cannot treat that paragraph as a deterministic protocol.&lt;/p&gt;
&lt;p&gt;At the other extreme, a browser can give us a &lt;code&gt;MouseEvent&lt;/code&gt;. That object can cross a function boundary, but it says almost nothing about the application request. It contains coordinates, targets, and browser state while leaving the behavior to reverse-engineer what the click meant.&lt;/p&gt;
&lt;p&gt;The narrative has meaning but no protocol. The gesture has data but no application meaning.&lt;/p&gt;
&lt;p&gt;The translation between them is where intent either survives or begins to leak.&lt;/p&gt;
&lt;p&gt;This is where I use a semantic command: a request named in the application’s vocabulary that carries the information its authority needs to consider it.&lt;/p&gt;
&lt;h2 id=&quot;the-interface-sees-a-gesture&quot;&gt;The interface sees a gesture&lt;/h2&gt;
&lt;p&gt;In a browser, the interaction may begin with a click. In a test, it may begin with a direct function call. Another interface might use a keyboard shortcut, an HTTP request, or an agent action.&lt;/p&gt;
&lt;p&gt;Those are delivery mechanisms. They are not the application intent.&lt;/p&gt;
&lt;p&gt;If the command accepts a browser event, the interface becomes part of the application contract:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;startModule&lt;/span&gt;&lt;span&gt;(event: MouseEvent);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The behavior now has to discover what the click meant. It may inspect &lt;code&gt;event.currentTarget&lt;/code&gt;, read a data attribute, or depend on a particular markup structure. A headless test must recreate enough of that interface to make the request.&lt;/p&gt;
&lt;p&gt;The semantic command names the operation directly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;startModule&lt;/span&gt;&lt;span&gt;(moduleId: string);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The browser binding can obtain the selected ID from its current view and pass it in. A headless test can use the same contract without creating a &lt;code&gt;MouseEvent&lt;/code&gt; or an element.&lt;/p&gt;
&lt;p&gt;This is not about removing context indiscriminately. It is about separating the context behavior needs from the mechanism that happened to capture it.&lt;/p&gt;
&lt;p&gt;The behavior needs to know which module was requested. It does not need the click coordinates, target element, or markup structure.&lt;/p&gt;
&lt;p&gt;The delivery boundary should translate the interaction. It should not ask the application to reverse-engineer the interface.&lt;/p&gt;
&lt;h2 id=&quot;a-command-is-a-compression-boundary&quot;&gt;A command is a compression boundary&lt;/h2&gt;
&lt;p&gt;I think of the semantic command as a form of deliberate compression.&lt;/p&gt;
&lt;p&gt;The product narrative contains more context than the running behavior needs. The interface interaction contains more delivery detail than the application should know. The command preserves the smallest useful meaning between them.&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Form&lt;/th&gt;&lt;th&gt;What it preserves&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Product narrative&lt;/td&gt;&lt;td&gt;Person, context, desired progress, and product intent&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Interface interaction&lt;/td&gt;&lt;td&gt;How one interface observed or captured the request&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Semantic command&lt;/td&gt;&lt;td&gt;The application request and the values behavior needs to consider it&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fact&lt;/td&gt;&lt;td&gt;What the application or external work determined actually happened&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Compression is useful only if we preserve the right information.&lt;/p&gt;
&lt;p&gt;If we discard &lt;code&gt;moduleId&lt;/code&gt;, the behavior cannot identify the requested module. If we carry the entire &lt;code&gt;MouseEvent&lt;/code&gt;, the application inherits browser details it does not need. If we announce that the module started before authoritative behavior accepts the request, we have compressed a possibility into a false fact.&lt;/p&gt;
&lt;p&gt;The goal is not the smallest payload. It is the smallest payload that preserves application meaning.&lt;/p&gt;
&lt;p&gt;In a larger system, that may include an idempotency key, correlation ID, initiating actor, or provenance the application needs for consistency and audit. Those values are not automatically delivery noise. The question is whether authoritative behavior needs them to interpret the request, not whether they came from outside the current function.&lt;/p&gt;
&lt;h2 id=&quot;carry-the-request-to-its-authority&quot;&gt;Carry the request to its authority&lt;/h2&gt;
&lt;p&gt;In the &lt;a href=&quot;https://0xjcf.github.io/ignite-element/&quot;&gt;Ignite Element v3 beta&lt;/a&gt;, the command forwards the request to the actor:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;commands&lt;/span&gt;&lt;span&gt;: ({ &lt;/span&gt;&lt;span&gt;actor&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; ({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  startModule&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;moduleId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    actor.&lt;/span&gt;&lt;span&gt;send&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      type: &lt;/span&gt;&lt;span&gt;&quot;START_MODULE&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      moduleId,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The beta API makes the same boundary explicit: command callbacks receive the actor, not the custom-element &lt;code&gt;host&lt;/code&gt;. A browser binding may still read from its element, but that translation happens before it calls the command.&lt;/p&gt;
&lt;p&gt;Two small translations happen:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The delivery interface turns its interaction into &lt;code&gt;startModule(moduleId)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The command function turns that request into &lt;code&gt;START_MODULE&lt;/code&gt; for the actor that owns the behavior.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The command does not decide whether the module may start. It does not update actor context directly or announce that the start succeeded. It carries the request to the part of the application allowed to interpret it.&lt;/p&gt;
&lt;figure&gt;&lt;div&gt;&lt;img src=&quot;https://0xjcf.com/assets/diagrams/2451d69789e06dfe7ae0.svg&quot; alt=&quot;Architecture diagram with: Product narrative, desired progress; Interface interaction, click / call / shortcut; Delivery binding, extracts moduleId; Semantic command, startModule(moduleId); Authoritative behavior, accepts or rejects.&quot; width=&quot;353&quot; height=&quot;424&quot; /&gt;&lt;/div&gt;&lt;figcaption&gt;Architecture diagram · Scroll horizontally to explore.&lt;a href=&quot;https://0xjcf.com/assets/diagrams/2451d69789e06dfe7ae0.svg&quot;&gt;Open full size &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;The narrative constrains what the command means, but it does not execute the interface. The binding translates the current interaction. The command then crosses into authoritative behavior without carrying the interface with it.&lt;/p&gt;
&lt;p&gt;That seam looks almost boring.&lt;/p&gt;
&lt;p&gt;That is a good sign. A command should be easy to describe: receive application intent and carry it to its owner.&lt;/p&gt;
&lt;h2 id=&quot;a-request-is-not-a-fact&quot;&gt;A request is not a fact&lt;/h2&gt;
&lt;p&gt;The message name often reveals what we think the sender has authority to claim.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;START_MODULE&lt;/code&gt; is a request. It says someone wants the behavior to consider starting a module.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MODULE_STARTED&lt;/code&gt; makes a different claim. It says the start already happened.&lt;/p&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Message&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;th&gt;Appropriate sender&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;START_MODULE&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Please consider starting this module&lt;/td&gt;&lt;td&gt;A caller allowed to request the behavior&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;MODULE_STARTED&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The module has successfully started&lt;/td&gt;&lt;td&gt;The behavior or work that observed completion&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;If an interface sends &lt;code&gt;MODULE_STARTED&lt;/code&gt; because someone clicked a button, it announces an outcome it does not own. The current state may reject the request. The work may fail. Another module may already be running.&lt;/p&gt;
&lt;p&gt;The actor may receive &lt;code&gt;START_MODULE&lt;/code&gt;, reject it, begin work, and later receive or produce &lt;code&gt;MODULE_STARTED&lt;/code&gt; after completion. Those messages describe different moments and require different authority.&lt;/p&gt;
&lt;p&gt;This is why naming every event in the past tense can be as misleading as naming every event as an imperative. The useful question is not grammatical:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What does the sender have the authority to claim?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;translation-can-fail-in-two-directions&quot;&gt;Translation can fail in two directions&lt;/h2&gt;
&lt;p&gt;A delivery boundary can translate too little.&lt;/p&gt;
&lt;p&gt;Passing a &lt;code&gt;MouseEvent&lt;/code&gt;, HTTP request, SDK object, or provider payload into behavior forces the application to understand an external contract. The command exists, but the delivery mechanism crossed with it.&lt;/p&gt;
&lt;p&gt;The boundary can also translate too much.&lt;/p&gt;
&lt;p&gt;If the command validates application policy, chooses an adapter, performs retries, changes state, and announces completion, it becomes a second behavior engine:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;startModule&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;moduleId&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;canStart&lt;/span&gt;&lt;span&gt;(moduleId)) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  await&lt;/span&gt;&lt;span&gt; retry&lt;/span&gt;&lt;span&gt;(() &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; api.&lt;/span&gt;&lt;span&gt;start&lt;/span&gt;&lt;span&gt;(moduleId));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  actor.&lt;/span&gt;&lt;span&gt;send&lt;/span&gt;&lt;span&gt;({ type: &lt;/span&gt;&lt;span&gt;&quot;MODULE_STARTED&quot;&lt;/span&gt;&lt;span&gt;, moduleId });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some boundary validation is normal. A route parameter may need decoding. A form value may need to become an explicit application value. Structurally invalid input may need to be rejected before it enters the application.&lt;/p&gt;
&lt;p&gt;But deciding whether a valid request is allowed now, which work should begin, and what a failure means belongs with authoritative behavior and policy.&lt;/p&gt;
&lt;p&gt;The narrow path is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;translate delivery details into application values&lt;/li&gt;
&lt;li&gt;preserve the context behavior actually needs&lt;/li&gt;
&lt;li&gt;carry the request without deciding its outcome&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Too little translation leaks the interface inward. Too much translation moves behavior outward.&lt;/p&gt;
&lt;h2 id=&quot;the-same-boundary-appears-in-navigation&quot;&gt;The same boundary appears in navigation&lt;/h2&gt;
&lt;p&gt;Navigation makes the next problem easier to see.&lt;/p&gt;
&lt;p&gt;A browser link produces a click. The application intent is to request a destination:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;commands&lt;/span&gt;&lt;span&gt;: ({ &lt;/span&gt;&lt;span&gt;actor&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; ({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  navigate&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;to&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    actor.&lt;/span&gt;&lt;span&gt;send&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      type: &lt;/span&gt;&lt;span&gt;&quot;NAVIGATE_REQUESTED&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      to,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;NAVIGATE_REQUESTED&lt;/code&gt; does not claim that the browser moved or that the destination is allowed. It asks the router’s behavior to interpret a destination under the current policy.&lt;/p&gt;
&lt;p&gt;The router still has to answer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does the path match a route?&lt;/li&gt;
&lt;li&gt;Does the route require authentication?&lt;/li&gt;
&lt;li&gt;Should the request redirect?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those answers should not depend on whether the request came from a link, a test, an HTTP route, or another delivery interface.&lt;/p&gt;
&lt;p&gt;The semantic command gives each caller the same vocabulary. It does not guarantee the same outcome, because the current state and policy still matter.&lt;/p&gt;
&lt;p&gt;That distinction is the reason the command remains a request.&lt;/p&gt;
&lt;h2 id=&quot;use-the-smallest-useful-vocabulary&quot;&gt;Use the smallest useful vocabulary&lt;/h2&gt;
&lt;p&gt;Semantic does not mean elaborate.&lt;/p&gt;
&lt;p&gt;We do not need a command class, command bus, or generic envelope for every request. A typed function and a typed actor event may be enough.&lt;/p&gt;
&lt;p&gt;I add structure when it protects a real distinction:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;interface interaction versus application intent&lt;/li&gt;
&lt;li&gt;request versus completed fact&lt;/li&gt;
&lt;li&gt;structural validation versus application policy&lt;/li&gt;
&lt;li&gt;caller responsibility versus behavior authority&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For &lt;code&gt;startModule&lt;/code&gt;, a string argument was enough to expose the hidden dependency. For navigation, &lt;code&gt;NAVIGATE_REQUESTED&lt;/code&gt; gives the router one place to interpret the request.&lt;/p&gt;
&lt;p&gt;The value is not the pattern name. It is that different callers can express the same intent without importing one interface’s vocabulary or creating another policy owner.&lt;/p&gt;
&lt;h2 id=&quot;a-semantic-command-check&quot;&gt;A semantic-command check&lt;/h2&gt;
&lt;p&gt;Before exposing a command, I ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Is it named after what the caller wants rather than how one interface delivers it?&lt;/li&gt;
&lt;li&gt;Does it carry every value authoritative behavior needs to consider the request?&lt;/li&gt;
&lt;li&gt;Can another caller use it without fabricating UI, transport, or provider objects?&lt;/li&gt;
&lt;li&gt;Does the sender have authority to make the claim expressed by the message?&lt;/li&gt;
&lt;li&gt;Does the boundary translate structure without deciding application policy?&lt;/li&gt;
&lt;li&gt;Is the command still understandable without knowing the current interface?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If those answers are clear, the command forms a useful seam between delivery and behavior.&lt;/p&gt;
&lt;h2 id=&quot;next-in-the-series&quot;&gt;Next in the series&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;NAVIGATE_REQUESTED&lt;/code&gt; gives the application a stable request, but a request is not yet a decision.&lt;/p&gt;
&lt;p&gt;Something still has to interpret the destination under the current routes, authentication state, and navigation policy. That interpretation should produce the same answer for the same state and facts, regardless of which interface delivered the request.&lt;/p&gt;
&lt;p&gt;The next post, &lt;a href=&quot;https://0xjcf.com/writing/functional-core/&quot;&gt;&lt;em&gt;Functional Core Architecture: Where Deterministic Decisions Belong&lt;/em&gt;&lt;/a&gt;, isolates that deterministic decision before we add browser history, observers, or asynchronous commits.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://0xjcf.com/writing/narrative-to-semantic-command/&quot;&gt;Read on 0xjcf.com&lt;/a&gt;&lt;/p&gt;</content:encoded><dc:creator>Jose Flores</dc:creator><media:content url="https://0xjcf.com/assets/images/articles/narrative-to-semantic-command-v1.jpg" medium="image" type="image/jpeg" width="1734" height="907"/></item><item><title>Product Framing Before Software Design: Deciding What to Build</title><link>https://0xjcf.com/writing/before-behavior-product-frame/</link><guid isPermaLink="true">https://0xjcf.com/writing/before-behavior-product-frame/</guid><description>How I use a product frame as the maintainer of Ignite Element, and how developers can apply the same questions to problems for their own clients and users.</description><pubDate>Thu, 30 Jul 2026 00:00:01 GMT</pubDate><content:encoded>&lt;figure&gt;&lt;img src=&quot;https://0xjcf.com/assets/images/articles/before-behavior-product-frame-v1.jpg&quot; alt=&quot;A red frame isolates a purposeful charcoal structure from scattered paper fragments.&quot; width=&quot;1733&quot; height=&quot;907&quot; /&gt;&lt;/figure&gt;&lt;p&gt;A clean boundary can protect the wrong behavior just as effectively as the right one.&lt;/p&gt;
&lt;p&gt;That is uncomfortable because architectural improvements produce satisfying evidence. The code becomes easier to test. Responsibilities become clearer. Two interfaces can use the same contract. We can point to the result and say, “This is better.”&lt;/p&gt;
&lt;p&gt;Sometimes it is.&lt;/p&gt;
&lt;p&gt;But better architecture only tells us that the implementation matches our decision more clearly. It does not tell us whether the decision was worth making.&lt;/p&gt;
&lt;p&gt;I am approaching this from my role as the maintainer of &lt;a href=&quot;https://0xjcf.github.io/ignite-element/&quot;&gt;Ignite Element&lt;/a&gt;. The developer using the library is my direct user. That developer may be building for a client, an internal product team, or the people who eventually use the application.&lt;/p&gt;
&lt;p&gt;I ran into this after cleaning up the &lt;code&gt;startModule&lt;/code&gt; command from the previous article:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;startModule&lt;/span&gt;&lt;span&gt;(moduleId);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The caller now supplied the module ID directly. A browser binding and a headless test could express the same intent without recreating an HTML element.&lt;/p&gt;
&lt;p&gt;That fixed a real ownership problem.&lt;/p&gt;
&lt;p&gt;It did not prove that starting a module was the right behavior to build.&lt;/p&gt;
&lt;h2 id=&quot;the-refactor-answered-one-question&quot;&gt;The refactor answered one question&lt;/h2&gt;
&lt;p&gt;The headless test gave us useful evidence. It had to construct an element and attach &lt;code&gt;data-module-id&lt;/code&gt; before it could say which module should start. The test was recreating presentation markup to communicate application intent.&lt;/p&gt;
&lt;p&gt;Changing the command answered a narrow question:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Can more than one interface express the same request without depending on one browser view?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We could verify the answer with tests. Both callers passed a module ID, sent the same event, and reached the same behavior.&lt;/p&gt;
&lt;p&gt;That is implementation evidence.&lt;/p&gt;
&lt;p&gt;It does not prove that developers understand the API faster, make fewer mistakes, or prefer the new contract. It does not tell us whether starting a module solves the most important problem in their workflow. Those are different claims, and they require different evidence.&lt;/p&gt;
&lt;p&gt;This distinction is easy to lose when implementation is inexpensive. We can move from an idea to a polished component before we have written down the problem we think the component solves.&lt;/p&gt;
&lt;p&gt;The code begins to look settled while the product decision still rests on assumptions.&lt;/p&gt;
&lt;h2 id=&quot;start-with-progress-not-a-command&quot;&gt;Start with progress, not a command&lt;/h2&gt;
&lt;p&gt;“The system needs a start command” sounds like a requirement, but it already assumes the solution.&lt;/p&gt;
&lt;p&gt;Before behavior, I want to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Who is trying to make progress?&lt;/li&gt;
&lt;li&gt;What are they trying to accomplish?&lt;/li&gt;
&lt;li&gt;What makes that difficult today?&lt;/li&gt;
&lt;li&gt;What would be different if we helped?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this example, the frame begins at the boundary between Ignite Element and the developer using it. I am trying to understand how that developer exercises component behavior through the different environments the library supports. During normal development, they might use a rendered browser control. In a test, they might use the &lt;a href=&quot;https://0xjcf.github.io/ignite-element/api/headless-runtime/&quot;&gt;headless runtime&lt;/a&gt;. In both cases, they wanted to start a particular module and observe what the application did.&lt;/p&gt;
&lt;p&gt;The difficulty I actually observed was narrower: the headless test had to fabricate DOM state before it could make that request.&lt;/p&gt;
&lt;p&gt;That is enough to investigate. We do not need to inflate one awkward test into a universal claim about every developer, test, or component system. We only need to describe what we know precisely enough to decide what to try next.&lt;/p&gt;
&lt;p&gt;I have found it useful to capture that understanding in a small product frame.&lt;/p&gt;
&lt;h2 id=&quot;a-product-frame-makes-assumptions-visible&quot;&gt;A product frame makes assumptions visible&lt;/h2&gt;
&lt;p&gt;The frame is not a requirements document. It is a compact record of the problem as we currently understand it, including the places where we may be wrong.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;startModule&lt;/code&gt;, I would write:&lt;/p&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Part&lt;/th&gt;&lt;th&gt;Current statement&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Person and context&lt;/td&gt;&lt;td&gt;A developer using Ignite Element to invoke module behavior from a browser binding or headless test&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Desired progress&lt;/td&gt;&lt;td&gt;Start a chosen module and inspect the resulting behavior&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Observed difficulty&lt;/td&gt;&lt;td&gt;The headless test had to fabricate DOM state to provide the module ID&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Known constraint&lt;/td&gt;&lt;td&gt;Starting a module requires an explicit module ID&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Hypothesis&lt;/td&gt;&lt;td&gt;A presentation-independent command will make the behavior easier to exercise from more than one interface&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Outcome signal&lt;/td&gt;&lt;td&gt;Developers can express the operation without learning or recreating an unrelated presentation contract&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The person, desired progress, difficulty, and constraint come from the workflow we encountered. The hypothesis describes the change we believe will help. The outcome signal tells us what improvement we expect to observe.&lt;/p&gt;
&lt;p&gt;Those lines should not quietly trade places.&lt;/p&gt;
&lt;p&gt;A hypothesis does not become a fact because the implementation works. An outcome signal is not “the pull request merged.” If we cannot distinguish what we observed from what we hope will improve, the frame only gives uncertainty a more official-looking format.&lt;/p&gt;
&lt;p&gt;The point is not to sound certain. The point is to make the uncertainty inspectable.&lt;/p&gt;
&lt;h2 id=&quot;the-frame-can-repeat-at-each-layer&quot;&gt;The frame can repeat at each layer&lt;/h2&gt;
&lt;p&gt;The developer is my direct user in this example, but they are not necessarily the last person affected by the design. They may use Ignite Element to build an application for a client, a product team, or an end user with a different problem.&lt;/p&gt;
&lt;p&gt;The same product frame can be applied again at each layer:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Frame owner&lt;/th&gt;&lt;th&gt;Person in focus&lt;/th&gt;&lt;th&gt;Progress being investigated&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Ignite Element maintainer&lt;/td&gt;&lt;td&gt;Developer using the library&lt;/td&gt;&lt;td&gt;Express and test the same application behavior across supported environments&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Application developer or team&lt;/td&gt;&lt;td&gt;Client, stakeholder, or operator&lt;/td&gt;&lt;td&gt;Represent the workflow and rules the application needs to support&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Product team or client&lt;/td&gt;&lt;td&gt;End user&lt;/td&gt;&lt;td&gt;Complete a real task with less friction&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The chain will differ by product. The important part is that evidence from one layer does not automatically validate the next. A passing headless test gives me evidence about the Ignite Element contract. It does not prove that a client’s workflow is correct or that an end user benefits from the behavior.&lt;/p&gt;
&lt;p&gt;Each layer needs its own person, desired progress, observed difficulty, hypothesis, and outcome signal. The questions repeat. The answers do not.&lt;/p&gt;
&lt;h2 id=&quot;correct-behavior-and-helpful-behavior-need-different-evidence&quot;&gt;Correct behavior and helpful behavior need different evidence&lt;/h2&gt;
&lt;p&gt;Architecture and product discovery validate different things.&lt;/p&gt;

















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Question&lt;/th&gt;&lt;th&gt;Useful evidence&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Did we implement the behavior we agreed to?&lt;/td&gt;&lt;td&gt;Transition tests, traces, contracts, and observable state&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Did the behavior help with the problem we described?&lt;/td&gt;&lt;td&gt;Observation, feedback, usage, and outcome signals&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Passing the first test does not answer the second.&lt;/p&gt;
&lt;p&gt;A state machine can enforce a workflow perfectly while the workflow remains confusing. A provider-neutral adapter can isolate an integration while the application still models the provider’s process instead of the user’s progress. A polished interface can make the wrong next step easier to click.&lt;/p&gt;
&lt;p&gt;None of those are failures of architecture. They are reminders that architecture protects a decision from implementation churn. It does not transform that decision into product truth.&lt;/p&gt;
&lt;p&gt;This is why the frame belongs before behavior. It keeps the reason for the behavior close enough that we can still question it.&lt;/p&gt;
&lt;h2 id=&quot;turn-the-frame-into-a-narrative&quot;&gt;Turn the frame into a narrative&lt;/h2&gt;
&lt;p&gt;The frame gives us the pieces of the problem, but it does not yet describe the interaction.&lt;/p&gt;
&lt;p&gt;For this example, the narrative might be:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A developer wants to start a selected module while exercising the application from different interfaces. They should be able to identify the module directly instead of reconstructing how one browser view stores that value.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That sentence gives us more direction than “add a reusable command.” It identifies who initiates the interaction, which value represents their intent, and which presentation detail should remain outside the application contract.&lt;/p&gt;
&lt;p&gt;It also leaves the implementation open.&lt;/p&gt;
&lt;p&gt;The narrative does not require XState, Ignite Element, a DOM attribute, or a particular test helper. Those choices belong later, after we agree on the progress the behavior should support.&lt;/p&gt;
&lt;p&gt;This is where product framing begins to constrain architecture without designing it. The narrative gives us a reason to reject a contract that forces every caller to impersonate the current UI, but it does not prescribe the final command, event, or actor.&lt;/p&gt;
&lt;h2 id=&quot;the-frame-guides-behavior-but-does-not-govern-it&quot;&gt;The frame guides behavior but does not govern it&lt;/h2&gt;
&lt;p&gt;There is an important boundary here.&lt;/p&gt;
&lt;p&gt;The product frame explains why we are considering a behavior. It is not a second runtime authority.&lt;/p&gt;
&lt;p&gt;Once a team accepts the behavior, the application still needs one place that decides whether a request is allowed now, how state changes, which work begins, and what completion or failure means. Those decisions belong to authoritative behavior and policy.&lt;/p&gt;
&lt;p&gt;The reverse is also true. A state machine can enforce an accepted rule, but it cannot tell us whether the original product frame was sound.&lt;/p&gt;
&lt;p&gt;The frame may change because we learn something new. The runtime should not reinterpret policy every time a product assumption changes in a document. We use new evidence to make a deliberate product decision, then change the authoritative behavior through the same explicit process as any other rule.&lt;/p&gt;
&lt;p&gt;Keeping these responsibilities separate prevents two mistakes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;treating implementation correctness as product validation&lt;/li&gt;
&lt;li&gt;treating a product narrative as executable policy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The frame gives behavior a reason. The behavior gives the running system authority.&lt;/p&gt;
&lt;h2 id=&quot;keep-it-small-enough-to-change&quot;&gt;Keep it small enough to change&lt;/h2&gt;
&lt;p&gt;This should not become a ceremony before every function.&lt;/p&gt;
&lt;p&gt;I use a product frame when a change introduces or alters visible behavior, commits the application to a policy, or creates a boundary that other parts of the system will depend on.&lt;/p&gt;
&lt;p&gt;For many changes, six short lines are enough:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Person and context:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Desired progress:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Observed difficulty:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Known constraint:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Hypothesis:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Outcome signal:&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The most valuable line is often Hypothesis. It records what we expect to improve without presenting that expectation as a settled fact.&lt;/p&gt;
&lt;p&gt;That makes later learning less defensive. If evidence contradicts the hypothesis, we can change the product decision without pretending the implementation was defective. The code may have done exactly what we asked. We simply learned that we should ask for something different.&lt;/p&gt;
&lt;p&gt;A useful frame should make change easier, not make the original idea harder to question.&lt;/p&gt;
&lt;h2 id=&quot;a-product-frame-check&quot;&gt;A product-frame check&lt;/h2&gt;
&lt;p&gt;Before modeling behavior, I ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which layer of the product am I framing?&lt;/li&gt;
&lt;li&gt;Can I describe the person and their desired progress without naming the implementation?&lt;/li&gt;
&lt;li&gt;Which evidence belongs to this layer?&lt;/li&gt;
&lt;li&gt;Which part of the difficulty have we actually observed?&lt;/li&gt;
&lt;li&gt;Which part are we assuming?&lt;/li&gt;
&lt;li&gt;What outcome would support the hypothesis?&lt;/li&gt;
&lt;li&gt;What result would make us reconsider it?&lt;/li&gt;
&lt;li&gt;Does the proposed request express the person’s intent or a detail of the current interface?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For &lt;code&gt;startModule&lt;/code&gt;, the improvement was not merely that we found a cleaner function signature. The signature was useful because it matched the intent described by the product frame.&lt;/p&gt;
&lt;p&gt;That still does not give every caller permission to mutate state. “Start this module” is a request. The application needs a smaller semantic command that can cross into authoritative behavior and be accepted or rejected according to its rules.&lt;/p&gt;
&lt;h2 id=&quot;next-in-the-series&quot;&gt;Next in the series&lt;/h2&gt;
&lt;p&gt;The product frame gives us the context. The narrative describes the interaction. Neither one is a message the application can execute.&lt;/p&gt;
&lt;p&gt;The next post, &lt;a href=&quot;https://0xjcf.com/writing/narrative-to-semantic-command/&quot;&gt;&lt;em&gt;Semantic Commands: Translating User Intent into Application Behavior&lt;/em&gt;&lt;/a&gt;, follows that translation and asks what the application should receive when a person clicks a button, a test invokes a function, or another interface requests the same behavior.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://0xjcf.com/writing/before-behavior-product-frame/&quot;&gt;Read on 0xjcf.com&lt;/a&gt;&lt;/p&gt;</content:encoded><dc:creator>Jose Flores</dc:creator><media:content url="https://0xjcf.com/assets/images/articles/before-behavior-product-frame-v1.jpg" medium="image" type="image/jpeg" width="1733" height="907"/></item><item><title>AI-Generated Code and Software Architecture: Why Ownership Matters</title><link>https://0xjcf.com/writing/when-code-becomes-cheap/</link><guid isPermaLink="true">https://0xjcf.com/writing/when-code-becomes-cheap/</guid><description>How AI-assisted development shifts the difficult work from producing code to deciding which part of the system should own each decision.</description><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;figure&gt;&lt;img src=&quot;https://0xjcf.com/assets/images/articles/when-code-becomes-cheap-v1.jpg&quot; alt=&quot;Three architectural enclosures organize charcoal blocks, each anchored by one red keystone.&quot; width=&quot;1733&quot; height=&quot;907&quot; /&gt;&lt;/figure&gt;&lt;p&gt;There was a time when writing the code was the hard part.&lt;/p&gt;
&lt;p&gt;You had to remember the syntax, learn an unfamiliar API, and wire asynchronous logic carefully enough that it would survive timing, failure, and retries. I do not miss callback hell, but that friction did limit how quickly a vague decision could become part of a system.&lt;/p&gt;
&lt;p&gt;Today, I can describe a feature and receive a plausible implementation in seconds. It may include loading states, error handling, retries, and tests. Sometimes it is shockingly good.&lt;/p&gt;
&lt;p&gt;But I started noticing a different kind of failure.&lt;/p&gt;
&lt;p&gt;The logic looked correct, yet the system still felt unstable. A small change required knowledge from several files. Tests passed, but the code felt risky to touch. Replacing an integration changed behavior that should have had nothing to do with the provider.&lt;/p&gt;
&lt;p&gt;The problem was not syntax. It was not even the tool.&lt;/p&gt;
&lt;p&gt;It was ownership.&lt;/p&gt;
&lt;h2 id=&quot;the-bottleneck-moved&quot;&gt;The bottleneck moved&lt;/h2&gt;
&lt;p&gt;AI is very good at completing the local story.&lt;/p&gt;
&lt;p&gt;Ask it to load some data and it must decide where loading state lives, who calls the API, how errors are translated, and whether a retry belongs in the component, machine, or request helper. If those decisions are missing from the request, the implementation still has to put them somewhere.&lt;/p&gt;
&lt;p&gt;Usually, it chooses the nearest convenient place.&lt;/p&gt;
&lt;p&gt;That does not make the generated code inherently bad. A human working quickly will make the same choice. The difference is that we can now repeat the choice across a codebase much faster than we can inspect its consequences.&lt;/p&gt;
&lt;p&gt;AI did not create unclear responsibility. It made unclear responsibility cheaper to multiply.&lt;/p&gt;
&lt;p&gt;This is why I think the bottleneck moved. Producing a possible implementation is becoming easier. Deciding which part of the system is allowed to know, choose, mutate, start, stop, retry, and clean up is still design work.&lt;/p&gt;
&lt;p&gt;That is the architectural work I care about.&lt;/p&gt;
&lt;h2 id=&quot;a-small-command-with-a-hidden-decision&quot;&gt;A small command with a hidden decision&lt;/h2&gt;
&lt;p&gt;I ran into this while dogfooding &lt;a href=&quot;https://0xjcf.github.io/ignite-element/&quot;&gt;Ignite Element&lt;/a&gt;, a component runtime that projects actor state into Web Components.&lt;/p&gt;
&lt;p&gt;As the maintainer, I often build small projects to stress-test the API against realistic behavior. These projects are not client case studies. They give me a place to see whether Ignite Element’s contracts still make sense when the same behavior is exercised through a rendered interface and a headless runtime.&lt;/p&gt;
&lt;p&gt;One of those projects included a screen that lists several modules. Each module has a Start button, an XState actor tracks which one is running, and Ignite Element renders the resulting state.&lt;/p&gt;
&lt;p&gt;The first version of the command discovered the module ID from the rendered element:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;commands&lt;/span&gt;&lt;span&gt;: ({ &lt;/span&gt;&lt;span&gt;actor&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;host&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; ({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  startModule&lt;/span&gt;&lt;span&gt;: () &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    actor.&lt;/span&gt;&lt;span&gt;send&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      type: &lt;/span&gt;&lt;span&gt;&quot;START_MODULE&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      moduleId: host.dataset.moduleId &lt;/span&gt;&lt;span&gt;??&lt;/span&gt;&lt;span&gt; &quot;missing&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing about this looked alarming. It worked in the browser because the element contained the expected &lt;code&gt;data-module-id&lt;/code&gt;. The &lt;code&gt;&quot;missing&quot;&lt;/code&gt; fallback even looked defensive.&lt;/p&gt;
&lt;p&gt;As part of that stress test, I tried to exercise the same behavior through the headless runtime.&lt;/p&gt;
&lt;p&gt;The test did not need a DOM element. It only needed to say which module should start. But the command contract forced the test to create an element and attach data to it before it could express that intent.&lt;/p&gt;
&lt;p&gt;That awkward setup was useful evidence.&lt;/p&gt;
&lt;p&gt;When a test has to impersonate an unrelated boundary, the production contract may be giving that boundary too much responsibility.&lt;/p&gt;
&lt;p&gt;Starting a module requires a module ID whether the request comes from a button, a test, a keyboard shortcut, or another interface. Reading &lt;code&gt;dataset&lt;/code&gt; is only one way a browser view might find that value. It is not part of the application intent.&lt;/p&gt;
&lt;p&gt;The command became:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;commands&lt;/span&gt;&lt;span&gt;: ({ &lt;/span&gt;&lt;span&gt;actor&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; ({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  startModule&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;moduleId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    actor.&lt;/span&gt;&lt;span&gt;send&lt;/span&gt;&lt;span&gt;({ type: &lt;/span&gt;&lt;span&gt;&quot;START_MODULE&quot;&lt;/span&gt;&lt;span&gt;, moduleId });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important change is not the extra parameter. It is the decision that disappeared from the command.&lt;/p&gt;
&lt;p&gt;The caller now supplies &lt;code&gt;moduleId&lt;/code&gt;. A browser binding may read it from the current view. A headless test can pass it directly. The command carries the request into the actor, and the machine defines whether that request is allowed in the current state.&lt;/p&gt;
&lt;p&gt;That callback shape also became part of the Ignite Element v3 beta. The beta removes &lt;code&gt;host&lt;/code&gt; from command and effect callbacks, so a callback no longer receives the rendered element as an implicit dependency. Commands receive explicit application inputs, while effects receive or close over the capabilities they need.&lt;/p&gt;
&lt;p&gt;The tool sees a missing value and fills it. The architecture decides who was allowed to provide that value in the first place.&lt;/p&gt;
&lt;figure&gt;&lt;div&gt;&lt;img src=&quot;https://0xjcf.com/assets/diagrams/cee0cc5b3d60dab4e409.svg&quot; alt=&quot;Architecture diagram with: Caller, button binding / headless test / other interface; Ignite command, startModule(moduleId); XState actor, handles START_MODULE; Ignite projection, snapshot to rendered view.&quot; width=&quot;355&quot; height=&quot;424&quot; /&gt;&lt;/div&gt;&lt;figcaption&gt;Architecture diagram · Scroll horizontally to explore.&lt;a href=&quot;https://0xjcf.com/assets/diagrams/cee0cc5b3d60dab4e409.svg&quot;&gt;Open full size &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;the-logic-was-correct-the-boundary-was-not&quot;&gt;The logic was correct. The boundary was not&lt;/h2&gt;
&lt;p&gt;The original command worked. That is what made the problem easy to miss.&lt;/p&gt;
&lt;p&gt;Architecture problems rarely announce themselves with a syntax error. They appear later as knowledge that spreads farther than the change should require.&lt;/p&gt;
&lt;p&gt;In this example, the ownership problem is concrete. The command knew how one browser view stored &lt;code&gt;moduleId&lt;/code&gt;, and the headless test exposed that dependency by forcing us to recreate the view just to express the same intent.&lt;/p&gt;
&lt;p&gt;That gives us a more useful question to ask during review:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Does this part of the system know something because the behavior requires it, or only because one interface happens to provide it that way?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The behavior required a module ID. It did not require a &lt;code&gt;dataset&lt;/code&gt;. That detail belonged to the browser binding.&lt;/p&gt;
&lt;p&gt;Product policy should change when the product decision changes. An integration should change when the provider changes. Presentation should change when the experience changes. When those responsibilities are mixed, a change in one part travels through all three.&lt;/p&gt;
&lt;p&gt;That is often the feeling behind, “The code works, so why is this so difficult to change?”&lt;/p&gt;
&lt;h2 id=&quot;architecture-distributes-authority&quot;&gt;Architecture distributes authority&lt;/h2&gt;
&lt;p&gt;When I say architecture, I am not talking about folder names, diagrams, or a prescribed number of layers. Those things can record a decision, but they cannot make the decision for us.&lt;/p&gt;
&lt;p&gt;I am talking about the distribution of authority through a system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Who may interpret an intent?&lt;/li&gt;
&lt;li&gt;Who may decide whether it is valid now?&lt;/li&gt;
&lt;li&gt;Who may change state?&lt;/li&gt;
&lt;li&gt;Who may start external work?&lt;/li&gt;
&lt;li&gt;Who owns the result, failure, retry, and cleanup?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Responsibility and authority are related, but they are not the same.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Responsibility&lt;/strong&gt; is what a part must do. &lt;strong&gt;Authority&lt;/strong&gt; is what that part alone is allowed to decide.&lt;/p&gt;
&lt;p&gt;In the Ignite example, the caller supplies the module it wants to start. The command translates that request into an actor event. The machine defines the legal transition. The running actor owns the state and lifecycle. The view renders the result.&lt;/p&gt;
&lt;p&gt;The command is responsible for carrying intent, but it does not have authority to invent a transition. The view is responsible for presentation, but it does not have authority to mutate actor state. The actor owns the lifecycle, but it should not have to understand how a particular button encoded the request.&lt;/p&gt;
&lt;p&gt;Once those rules are explicit, generated code has something concrete to follow. We can review whether a new implementation respects an existing owner instead of judging each callback in isolation.&lt;/p&gt;
&lt;h2 id=&quot;why-state-machines-help-and-where-they-stop&quot;&gt;Why state machines help, and where they stop&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://stately.ai/docs/xstate&quot;&gt;XState&lt;/a&gt; is useful here because it makes part of this authority executable.&lt;/p&gt;
&lt;p&gt;States name the modes the system can be in. Events name the messages it can receive. Transitions say which changes are legal. When the machine runs, its actor owns the current state and processes those events over time.&lt;/p&gt;
&lt;p&gt;That gives us concrete questions during review:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does this event exist in the protocol?&lt;/li&gt;
&lt;li&gt;Is it allowed in the current state?&lt;/li&gt;
&lt;li&gt;Which transition handles it?&lt;/li&gt;
&lt;li&gt;Which actor owns the resulting lifecycle?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is an important caveat: making a contract executable does not make it a good contract.&lt;/p&gt;
&lt;p&gt;A machine can faithfully accept a &lt;code&gt;MouseEvent&lt;/code&gt; that should have remained in the UI. It can expose a provider payload that should have been translated at the boundary. It can divide one policy decision across several guards, actions, and adapters.&lt;/p&gt;
&lt;p&gt;Invoking external work from a state is not automatically the problem either. A machine may own when and why work begins while a supplied actor or adapter owns how one attempt is performed. The boundary becomes useful when those responsibilities are explicit and the result returns as a fact the application understands.&lt;/p&gt;
&lt;p&gt;XState helps us see the contract. It cannot choose the contract for us.&lt;/p&gt;
&lt;p&gt;It also does not mean every function needs a machine. A pure calculation or stateless transformation may already have a clear owner. Actors become useful when behavior changes over time and the state, work, results, and cleanup need to remain coherent.&lt;/p&gt;
&lt;h2 id=&quot;how-to-recognize-an-ownership-leak&quot;&gt;How to recognize an ownership leak&lt;/h2&gt;
&lt;p&gt;The leak usually appears as friction around a change:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A test must recreate a UI, provider, or runtime just to express application intent.&lt;/li&gt;
&lt;li&gt;The same decision appears in a component, handler, adapter, and state machine.&lt;/li&gt;
&lt;li&gt;Changing an API response forces unrelated behavior and presentation changes.&lt;/li&gt;
&lt;li&gt;Timing or message order changes the meaning of a result.&lt;/li&gt;
&lt;li&gt;The system works, but changing it safely requires someone who remembers its history.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these observations proves the boundary is wrong. They are signals that responsibility may be distributed more widely than the behavior requires.&lt;/p&gt;
&lt;p&gt;That is enough reason to pause and ask who actually owns the decision.&lt;/p&gt;
&lt;h2 id=&quot;a-responsibility-check-before-implementation&quot;&gt;A responsibility check before implementation&lt;/h2&gt;
&lt;p&gt;Before I ask for more code, I now try to answer a few questions in plain language:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Who is trying to make progress, and in what context?&lt;/li&gt;
&lt;li&gt;What observable result would tell us the behavior helped?&lt;/li&gt;
&lt;li&gt;What intent enters the system?&lt;/li&gt;
&lt;li&gt;Which part decides whether the request is valid now?&lt;/li&gt;
&lt;li&gt;Which part is allowed to change state?&lt;/li&gt;
&lt;li&gt;Which details belong only to the current UI, provider, or runtime?&lt;/li&gt;
&lt;li&gt;Could another caller express the same intent without pretending to be that interface?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then I check whether the code tells the same story.&lt;/p&gt;
&lt;p&gt;In the Ignite example, the answer is visible in the command signature and the event it sends. The caller provides &lt;code&gt;moduleId&lt;/code&gt;. The actor receives &lt;code&gt;START_MODULE&lt;/code&gt;. The machine defines the legal transition, and the running actor owns its execution over time.&lt;/p&gt;
&lt;p&gt;This does not mean every dependency needs an adapter or every decision needs another interface. A boundary is useful when it stops one part from making a decision that belongs somewhere else. If it does not protect a meaningful decision, the extra abstraction probably makes the system harder to understand.&lt;/p&gt;
&lt;h2 id=&quot;the-first-draft-is-cheap&quot;&gt;The first draft is cheap&lt;/h2&gt;
&lt;p&gt;Faster implementation is a real advantage. I can explore an idea, discover where it breaks, and discard it without losing a day.&lt;/p&gt;
&lt;p&gt;But the time saved by generation should create more room for judgment, not less.&lt;/p&gt;
&lt;p&gt;AI did not change how systems work. It changed how quickly an unnamed assumption can become a dependency.&lt;/p&gt;
&lt;p&gt;I do not think architecture matters more because AI writes worse code. It matters more because we can multiply ownership decisions faster than we notice them.&lt;/p&gt;
&lt;p&gt;The first draft is becoming cheap. A coherent answer to who owns what is not.&lt;/p&gt;
&lt;h2 id=&quot;next-in-the-series&quot;&gt;Next in the series&lt;/h2&gt;
&lt;p&gt;The project gave me a concrete behavior for stress-testing the Ignite Element API. Passing &lt;code&gt;moduleId&lt;/code&gt; directly made &lt;code&gt;startModule&lt;/code&gt; easier to exercise from both a browser binding and a headless test. That gives us evidence that the command no longer depends on one view’s markup.&lt;/p&gt;
&lt;p&gt;It does not prove that starting a module is the right feature for a client’s product, and that was not the question this project could answer. The immediate question was whether Ignite Element lets a developer express the same intent across supported environments without carrying one view’s markup into the command.&lt;/p&gt;
&lt;p&gt;Before we model a command, actor, or port, we need to understand who is trying to make progress, what is getting in their way, and what evidence would show that the behavior helped.&lt;/p&gt;
&lt;p&gt;That is where the next post, &lt;a href=&quot;https://0xjcf.com/writing/before-behavior-product-frame/&quot;&gt;&lt;em&gt;Product Framing Before Software Design: Deciding What to Build&lt;/em&gt;&lt;/a&gt;, begins.&lt;/p&gt;
&lt;p&gt;The examples in this series come from dogfooding Ignite Element. You can &lt;a href=&quot;https://0xjcf.github.io/ignite-element/&quot;&gt;read the v3 beta documentation&lt;/a&gt; or &lt;a href=&quot;https://github.com/0xjcf/ignite-element/tree/beta&quot;&gt;follow the implementation and examples on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://0xjcf.com/writing/when-code-becomes-cheap/&quot;&gt;Read on 0xjcf.com&lt;/a&gt;&lt;/p&gt;</content:encoded><dc:creator>Jose Flores</dc:creator><media:content url="https://0xjcf.com/assets/images/articles/when-code-becomes-cheap-v1.jpg" medium="image" type="image/jpeg" width="1733" height="907"/></item></channel></rss>