<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>CJS</title><description>Technical notes, personal essays, and field observations by Ngọc Cường.</description><link>https://cjs-crafts.com/</link><language>en-us</language><item><title>GPT-6 Astra Costs More. Use It Where It Compounds.</title><link>https://cjs-crafts.com/blog/gpt-6-astra-where-it-compounds</link><guid isPermaLink="true">https://cjs-crafts.com/blog/gpt-6-astra-where-it-compounds</guid><description>GPT-6 Astra costs more per token. Here is where larger, more capable workflows may still return more value—and how to measure that.</description><pubDate>Sat, 05 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;GPT-6 Astra arrives with the usual headline: OpenAI&apos;s most capable model yet. The more useful detail is less flattering. It is expensive.&lt;/p&gt;
&lt;p&gt;That changes how I think about it. Astra is not a model I would put behind every autocomplete, summary, or support reply. I would use it where a better decision, fewer retries, or one finished workflow can repay the premium.&lt;/p&gt;
&lt;p&gt;The question is not whether Astra is smarter. It is where that intelligence compounds.&lt;/p&gt;
&lt;h2&gt;The price is the first useful signal&lt;/h2&gt;
&lt;p&gt;For API users, Astra costs $10 per million input tokens and $50 per million output tokens. GPT-5.6 Sol costs $4 and $20. Terra costs $2 and $12.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input / 1M&lt;/th&gt;
&lt;th&gt;Output / 1M&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;GPT-6 Astra&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;td&gt;$50&lt;/td&gt;
&lt;td&gt;1.05M&lt;/td&gt;
&lt;td&gt;Hard end-to-end work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.6 Sol&lt;/td&gt;
&lt;td&gt;$4&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;1.05M&lt;/td&gt;
&lt;td&gt;Complex professional work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.6 Terra&lt;/td&gt;
&lt;td&gt;$2&lt;/td&gt;
&lt;td&gt;$12&lt;/td&gt;
&lt;td&gt;1.05M&lt;/td&gt;
&lt;td&gt;Cost-aware general workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Astra is 2.5 times the token price of Sol. It does not buy a larger context window; both support 1.05 million tokens and up to 128,000 output tokens. The premium is for execution quality, not raw capacity. These figures come from OpenAI&apos;s &lt;a href=&quot;https://developers.openai.com/api/docs/models/compare&quot;&gt;current model comparison&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That makes “cost per token” a weak buying metric. A cheaper run that needs three retries, two human rescues, and a final rewrite may cost more than one expensive run that lands cleanly.&lt;/p&gt;
&lt;p&gt;OpenAI &lt;a href=&quot;https://developers.openai.com/api/docs/guides/latest-model&quot;&gt;says Astra used fewer output tokens and reached a lower estimated API cost per task&lt;/a&gt; in several evaluations, despite its higher rate. That is a vendor result, not an independent guarantee, but it points toward the right unit of measurement: the completed job.&lt;/p&gt;
&lt;h2&gt;What actually changed&lt;/h2&gt;
&lt;p&gt;Astra is built for long, multi-step work across code, browsers, research, and professional software. The model supports computer use, web and file search, code execution, MCP, skills, structured outputs, and tool calling through the Responses API.&lt;/p&gt;
&lt;p&gt;The more interesting additions are operational. Async tool calls let the model continue useful work while a slow tool runs. Mid-turn steering lets a person correct direction without discarding completed work. Reasoning effort can also change during a conversation while the stable prompt prefix remains cacheable.&lt;/p&gt;
&lt;p&gt;Those features matter more than another impressive chat answer. They reduce the friction of giving an agent a real job, watching it work, and changing course without starting again. OpenAI documents the behaviour and limitations in its &lt;a href=&quot;https://developers.openai.com/api/docs/guides/latest-model&quot;&gt;Astra model guidance&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;My take: give Astra bigger units of work&lt;/h2&gt;
&lt;p&gt;Most teams still give strong models small tasks: write a function, summarise a PDF, or produce ten ideas. Astra looks better suited to a wider boundary.&lt;/p&gt;
&lt;p&gt;Give it the research question, source rules, spreadsheet template, final brief, and acceptance criteria. Give it the repository, the failing flow, the browser, and the tests. Let it carry context across the whole outcome.&lt;/p&gt;
&lt;p&gt;This does not mean removing human control. It means moving the checkpoint. Review the plan before consequential actions and review the deliverable at the end, rather than supervising every small step.&lt;/p&gt;
&lt;p&gt;For a US or UK product team, that could mean turning customer interviews, support tickets, and product data into a decision memo. For an agency, it could mean researching a client, drafting the campaign assets, and checking the final site. The value comes from fewer handoffs.&lt;/p&gt;
&lt;h2&gt;Measure one completed job&lt;/h2&gt;
&lt;p&gt;I would test Astra on 10 to 20 representative tasks before changing a production workflow. Run the same tasks with Sol or Terra and keep the human process as the baseline.&lt;/p&gt;
&lt;p&gt;Measure five things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;total model and tool cost;&lt;/li&gt;
&lt;li&gt;time from request to accepted result;&lt;/li&gt;
&lt;li&gt;number of retries;&lt;/li&gt;
&lt;li&gt;number of human interventions;&lt;/li&gt;
&lt;li&gt;defects found during review.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A simple internal measure is:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost per accepted result = model cost + tool cost + review time + rework time.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Also record the completion rate without rescue. That number often explains more than a benchmark score. If Astra costs 2.5 times more per token but cuts intervention and rework by more than half, it may be the cheaper system. If the result still needs the same review and correction, use the smaller model.&lt;/p&gt;
&lt;h2&gt;How I would get more from Astra&lt;/h2&gt;
&lt;p&gt;First, state the outcome and the boundary. Include the files, tools, permissions, constraints, and definition of done. Astra follows detailed instructions well, but OpenAI also notes that it is sensitive to conflicting guidance in skills and files such as &lt;code&gt;AGENTS.md&lt;/code&gt;. Clean context is part of model performance.&lt;/p&gt;
&lt;p&gt;Second, choose reasoning effort by task. Use &lt;code&gt;low&lt;/code&gt; for routine work and raise it only when the decision is hard enough to justify more latency and tokens. Astra supports &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;xhigh&lt;/code&gt;, and &lt;code&gt;max&lt;/code&gt;; it does not support &lt;code&gt;none&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Third, use the Responses API for tool-heavy work. Async calls and mid-turn steering are useful only when the surrounding application can preserve state, surface progress, and accept corrections safely.&lt;/p&gt;
&lt;p&gt;Fourth, keep stable instructions at the front so prompt caching can help. When changing reasoning during a conversation, OpenAI recommends a &lt;code&gt;configuration_update&lt;/code&gt; item instead of rewriting the request-level configuration.&lt;/p&gt;
&lt;p&gt;Finally, create a routing rule. Use Terra for volume, Sol for difficult but bounded work, and Astra for jobs where coherence across many steps has measurable value. Do not make the most expensive model the default because it is new.&lt;/p&gt;
&lt;h2&gt;The limit is still trust&lt;/h2&gt;
&lt;p&gt;Astra&apos;s stronger computer use makes permissions more important, not less. A capable agent with a vague brief can complete the wrong job faster.&lt;/p&gt;
&lt;p&gt;Keep approval gates for money, customer data, production changes, security-sensitive actions, and external communication. Log tool calls and preserve the evidence behind important conclusions. Better reasoning does not replace accountability.&lt;/p&gt;
&lt;p&gt;My short version is simple: buy Astra for finished work, not impressive turns. Start with the hardest workflow you can measure, compare it against Sol and your current human process, and keep it only where the total cost falls or the result clearly improves.&lt;/p&gt;
&lt;h2&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/models/gpt-6-astra&quot;&gt;GPT-6 Astra model&lt;/a&gt; — pricing, context and output limits, reasoning levels, supported tools and endpoints. Accessed 2026-09-05.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/models/compare&quot;&gt;Compare OpenAI models&lt;/a&gt; — Astra, GPT-5.6 Sol, and GPT-5.6 Terra positioning and API prices. Accessed 2026-09-05.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/latest-model&quot;&gt;Model guidance: GPT-6 Astra&lt;/a&gt; — async tool calling, mid-turn steering, prompt caching, instruction sensitivity, and reported cost-per-task behaviour. Accessed 2026-09-05.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>personal</category><category>release</category><category>research</category></item><item><title>Changing Rules Without Rebuilding the App</title><link>https://cjs-crafts.com/blog/changing-rules-without-rebuilding-the-app</link><guid isPermaLink="true">https://cjs-crafts.com/blog/changing-rules-without-rebuilding-the-app</guid><description>How to ship remotely managed business rules safely, especially when client releases pass through stores or platform review.</description><pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Some releases are expensive for reasons unrelated to code. Mobile stores and mini-app platforms add review time. Desktop clients may update slowly. Even on the web, a full deployment can be a large blast radius for changing one threshold.&lt;/p&gt;
&lt;p&gt;Remote rules can separate a business decision from the client release cycle. The useful phrase is &lt;em&gt;can separate&lt;/em&gt;. Doing it safely requires more than putting JSON behind an endpoint.&lt;/p&gt;
&lt;h2&gt;A small example&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { createRuleEngine, createRuleHelpers } from &quot;rule-engine-js&quot;;

type PromotionContext = {
  user: { age: number; verified: boolean };
  order: { total: number };
};

const engine = createRuleEngine();
const rules = createRuleHelpers&amp;lt;PromotionContext&amp;gt;();

const fallbackRule = rules.and(
  rules.gte(&quot;user.age&quot;, 18),
  rules.eq(&quot;user.verified&quot;, true),
  rules.gte(&quot;order.total&quot;, 50),
);

const result = engine.evaluateExpr(fallbackRule, context);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The rule is data, so a compatible replacement can be fetched without rebuilding the app. The client still needs a known-good fallback.&lt;/p&gt;
&lt;h2&gt;Treat the rule response as a contract&lt;/h2&gt;
&lt;p&gt;A remote payload should include more than an expression:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;id&quot;: &quot;promotion-eligibility&quot;,
  &quot;version&quot;: 17,
  &quot;schemaVersion&quot;: 1,
  &quot;effectiveAt&quot;: &quot;2026-09-04T00:00:00Z&quot;,
  &quot;expression&quot;: {
    &quot;and&quot;: [
      { &quot;gte&quot;: [&quot;user.age&quot;, 18] },
      { &quot;eq&quot;: [&quot;user.verified&quot;, true] },
      { &quot;gte&quot;: [&quot;order.total&quot;, 50] }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The client should reject unsupported schema versions, invalid operators, excessive complexity, and payloads outside their validity window. Cache only a validated rule.&lt;/p&gt;
&lt;p&gt;If fetching fails, choose the fallback based on risk:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;eligibility for a cosmetic promotion may fail closed or use the bundled rule;&lt;/li&gt;
&lt;li&gt;security and payment decisions should be enforced by the server;&lt;/li&gt;
&lt;li&gt;stale rules should have an explicit maximum age;&lt;/li&gt;
&lt;li&gt;a last-known-good rule should never be overwritten by an invalid response.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Roll out rules like releases&lt;/h2&gt;
&lt;p&gt;A rule change skips an app rebuild, but it is still a production change. Give it the same basics:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;validate against a schema;&lt;/li&gt;
&lt;li&gt;replay representative and boundary fixtures;&lt;/li&gt;
&lt;li&gt;evaluate in shadow mode when possible;&lt;/li&gt;
&lt;li&gt;activate for a small cohort;&lt;/li&gt;
&lt;li&gt;watch decision rates and errors by version;&lt;/li&gt;
&lt;li&gt;keep one-click rollback.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A/B tests should assign a stable cohort on the server and record both the experiment and rule versions. Otherwise support teams cannot explain why two users received different results.&lt;/p&gt;
&lt;h2&gt;Keep the authority clear&lt;/h2&gt;
&lt;p&gt;Client evaluation is useful for immediate UI feedback and offline behavior. It is not proof that an operation is allowed. A modified client can ignore the rule entirely.&lt;/p&gt;
&lt;p&gt;For discounts, access control, fraud checks, or regulated decisions, send the same versioned context to the server and make the authoritative decision there. The client can be optimistic; the backend must be correct.&lt;/p&gt;
&lt;p&gt;rule-engine-js v1.0.7 provides typed rule helpers, safe nested path resolution, caching, metrics, and stateful evaluation. Those features solve evaluation. Distribution, approval, compatibility, and rollback remain application concerns.&lt;/p&gt;
&lt;p&gt;Sources: &lt;a href=&quot;https://github.com/crafts69guy/rule-engine-js&quot;&gt;rule-engine-js repository&lt;/a&gt;, &lt;a href=&quot;https://www.npmjs.com/package/rule-engine-js&quot;&gt;npm package&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>javascript</category><category>typescript</category><category>production</category></item><item><title>My Current Neovim Workflow: Snacks, Blink, and Profiles</title><link>https://cjs-crafts.com/blog/current-neovim-workflow-snacks-blink-profiles</link><guid isPermaLink="true">https://cjs-crafts.com/blog/current-neovim-workflow-snacks-blink-profiles</guid><description>A tour of the Neovim workflow I actually use today: focused profiles, Snacks navigation, Blink completion, and explicit tooling.</description><pubDate>Wed, 27 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Editor configurations age quickly. Plugin names change, defaults move, and the workflow that looked good in a screenshot may not survive daily use.&lt;/p&gt;
&lt;p&gt;This is the shape of my current Neovim setup, verified against the &lt;code&gt;production&lt;/code&gt; branch of my dotfiles in September 2026.&lt;/p&gt;
&lt;h2&gt;Pick a language profile&lt;/h2&gt;
&lt;p&gt;The first decision is not a colorscheme. It is the kind of project I am opening.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;:Profile&lt;/code&gt; command selects &lt;code&gt;core&lt;/code&gt;, &lt;code&gt;web&lt;/code&gt;, &lt;code&gt;go&lt;/code&gt;, or &lt;code&gt;rust&lt;/code&gt;. Core covers Lua, shell, JSON, YAML, and Markdown. Other profiles enable their own LazyVim extras and use separate Mason and Treesitter data directories.&lt;/p&gt;
&lt;p&gt;That keeps the editor&apos;s base consistent while preventing every machine from accumulating every tool.&lt;/p&gt;
&lt;h2&gt;Navigate with Snacks&lt;/h2&gt;
&lt;p&gt;Snacks now owns the high-frequency paths:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;leader&amp;gt;e&lt;/code&gt; opens the explorer;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;leader&amp;gt;ff&lt;/code&gt; finds files from the project root;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;leader&amp;gt;fg&lt;/code&gt; searches Git-tracked files;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;;r&lt;/code&gt; greps from the current working directory;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\\&lt;/code&gt; opens the buffer picker;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gd&lt;/code&gt; and &lt;code&gt;gr&lt;/code&gt; use LSP definition and reference pickers;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;C-/&amp;gt;&lt;/code&gt; toggles a terminal.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Neo-tree is disabled, and Telescope is no longer the center of the setup. One picker system means fewer overlapping mappings and a more consistent preview interface.&lt;/p&gt;
&lt;h2&gt;Complete with Blink&lt;/h2&gt;
&lt;p&gt;Blink combines LSP, paths, snippets, buffer words, and tmux sources. Signature help and documentation use bordered windows; &lt;code&gt;&amp;lt;C-y&amp;gt;&lt;/code&gt; accepts the selected item.&lt;/p&gt;
&lt;p&gt;AI completion is optional rather than fused into the core completion path. The &lt;code&gt;ai.lua&lt;/code&gt; and &lt;code&gt;windsurf.lua&lt;/code&gt; specs can extend the setup, but editing and language intelligence still work without an AI provider.&lt;/p&gt;
&lt;p&gt;That is an important boundary. Completion should degrade gracefully when a network service is unavailable.&lt;/p&gt;
&lt;h2&gt;Keep language tools explicit&lt;/h2&gt;
&lt;p&gt;Mason installs shared tools such as Stylua, Luacheck, ShellCheck, and shfmt. Profile-specific servers are added only when relevant. LSP inlay hints are disabled globally, while Lua keeps a restrained set of parameter hints.&lt;/p&gt;
&lt;p&gt;Formatting is handled by Conform. Web formats come from LazyVim&apos;s Prettier extra; Lua uses Stylua. I prefer a clear formatter owner over an invisible chain of LSP fallbacks.&lt;/p&gt;
&lt;h2&gt;Small tools that survived&lt;/h2&gt;
&lt;p&gt;A few plugins continue to earn their place:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;inc-rename for live rename feedback;&lt;/li&gt;
&lt;li&gt;mini.bracketed for structured next/previous navigation;&lt;/li&gt;
&lt;li&gt;Dial for numbers, dates, booleans, semantic versions, and &lt;code&gt;let&lt;/code&gt;/&lt;code&gt;const&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Outline for a symbol view;&lt;/li&gt;
&lt;li&gt;debugprint for temporary logging with a cleanup command;&lt;/li&gt;
&lt;li&gt;nvim-highlight-colors for CSS and Tailwind color context.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The list matters less than the filter: each tool solves friction I can name.&lt;/p&gt;
&lt;p&gt;The full configuration lives in &lt;a href=&quot;https://github.com/crafts69guy/.dotfiles/tree/production/.config/nvim&quot;&gt;crafts69guy/.dotfiles&lt;/a&gt;. It will change again. This article records the decisions, not a promise that every plugin will remain forever.&lt;/p&gt;
</content:encoded><category>learning</category><category>personal</category><category>tool</category></item><item><title>Keeping a LazyVim Configuration Personal and Maintainable</title><link>https://cjs-crafts.com/blog/keeping-lazyvim-personal-and-maintainable</link><guid isPermaLink="true">https://cjs-crafts.com/blog/keeping-lazyvim-personal-and-maintainable</guid><description>A maintainable way to customize LazyVim with small overrides, language profiles, a shared theme, and version-controlled decisions.</description><pubDate>Sun, 24 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The easy part of customizing Neovim is adding a plugin. The difficult part is remembering six months later why it exists and which defaults it replaced.&lt;/p&gt;
&lt;p&gt;My current rule is simple: keep the distribution intact, keep overrides small, and give each piece one owner.&lt;/p&gt;
&lt;h2&gt;Prefer extension over replacement&lt;/h2&gt;
&lt;p&gt;LazyVim already supplies plugin specifications, defaults, and language extras. Local files should describe the difference between that baseline and my preferences.&lt;/p&gt;
&lt;p&gt;For example, I use Snacks as the picker and explorer, so Neo-tree is explicitly disabled:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lua&quot;&gt;{
  &quot;neo-tree.nvim&quot;,
  enabled = false,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That small declaration is clearer than carrying a second explorer configuration that is never used.&lt;/p&gt;
&lt;p&gt;The same principle applies to formatting. LazyVim&apos;s Prettier extra handles common web file types; my Conform override adds Stylua for Lua rather than repeating the complete formatter table.&lt;/p&gt;
&lt;h2&gt;Use profiles for expensive language tooling&lt;/h2&gt;
&lt;p&gt;I work across web, Go, Rust, Markdown, and shell projects. Loading every language extra everywhere made startup state and Mason installations harder to reason about.&lt;/p&gt;
&lt;p&gt;The current configuration has four profiles:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lua&quot;&gt;local profiles = {
  core = { label = &quot;Core (Lua, shell, Markdown)&quot; },
  web = { label = &quot;Web (TypeScript, Tailwind, ESLint)&quot; },
  go = { label = &quot;Go&quot; },
  rust = { label = &quot;Rust&quot; },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;code&gt;:Profile&lt;/code&gt; command saves the selection and restarts Neovim after checking for modified buffers. Plugins are shared, while Mason and Treesitter data paths are isolated by profile. That keeps tooling focused without cloning the whole configuration.&lt;/p&gt;
&lt;p&gt;For one-off sessions, &lt;code&gt;NVIM_PROFILE&lt;/code&gt; can select a profile explicitly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fish&quot;&gt;env NVIM_PROFILE=rust nvim
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Let themes be infrastructure&lt;/h2&gt;
&lt;p&gt;The old setup hardcoded Solarized Osaka. The current one loads &lt;code&gt;crafts69guy/hue-nvim&lt;/code&gt; from the sibling &lt;code&gt;hue-theme&lt;/code&gt; repository when it is available.&lt;/p&gt;
&lt;p&gt;The active mood—&lt;code&gt;mua&lt;/code&gt;, &lt;code&gt;huong&lt;/code&gt;, or &lt;code&gt;cung&lt;/code&gt;—is stored outside Git under &lt;code&gt;~/.local/state/hue-theme/current&lt;/code&gt;. Switching colors does not dirty the dotfiles repository, and the same mood can be applied across Fish, Herdr, tmux, Ghostty, lazygit, and Neovim.&lt;/p&gt;
&lt;p&gt;That separation is useful: the dotfiles define integration, the theme repository defines generated colors, and local state chooses the current mood.&lt;/p&gt;
&lt;h2&gt;Make custom behavior searchable&lt;/h2&gt;
&lt;p&gt;Plugin files are grouped by responsibility, custom modules live under a personal namespace, and non-obvious decisions get comments. Keymaps include descriptions so WhichKey and picker views can explain them.&lt;/p&gt;
&lt;p&gt;When removing a plugin, search for commands, keymaps, dependencies, and module imports before deleting its spec. Configuration tends to leave small references behind.&lt;/p&gt;
&lt;p&gt;A personal setup should feel personal at the keyboard, but boring in Git. Small files, explicit ownership, and reproducible state make experimentation cheap without making maintenance endless.&lt;/p&gt;
&lt;p&gt;Source: &lt;a href=&quot;https://github.com/crafts69guy/.dotfiles&quot;&gt;current dotfiles&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>pattern</category><category>personal</category><category>tool</category></item><item><title>Why JSON-Serializable Rules Are Worth the Constraint</title><link>https://cjs-crafts.com/blog/why-json-serializable-rules-are-worth-the-constraint</link><guid isPermaLink="true">https://cjs-crafts.com/blog/why-json-serializable-rules-are-worth-the-constraint</guid><description>JSON-serializable rules trade arbitrary code for portability, auditability, and safer boundaries—if activation is treated like a release.</description><pubDate>Sun, 24 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Making a rule JSON-serializable sounds like an implementation detail. In practice, it changes the architecture.&lt;/p&gt;
&lt;p&gt;A JavaScript callback can close over local variables, call a service, read the clock, or mutate state. JSON cannot do any of that. It can only describe data. That limitation is the point.&lt;/p&gt;
&lt;h2&gt;What the constraint buys you&lt;/h2&gt;
&lt;p&gt;Consider a pricing eligibility rule:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;and&quot;: [
    { &quot;eq&quot;: [&quot;customer.tier&quot;, &quot;gold&quot;] },
    { &quot;gte&quot;: [&quot;order.total&quot;, 100] },
    { &quot;lt&quot;: [&quot;inventory.remaining&quot;, 10] }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because the rule contains no executable code, it can cross process boundaries. The same document can be stored, reviewed, cached, compared, and sent over an API.&lt;/p&gt;
&lt;p&gt;It also becomes possible to answer operational questions without reconstructing a deployment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which rule version produced this decision?&lt;/li&gt;
&lt;li&gt;What changed between version 12 and 13?&lt;/li&gt;
&lt;li&gt;Who approved the active version?&lt;/li&gt;
&lt;li&gt;Can we replay yesterday&apos;s input against both versions?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those capabilities do not come from JSON alone. JSON simply makes them practical.&lt;/p&gt;
&lt;h2&gt;What the constraint takes away&lt;/h2&gt;
&lt;p&gt;Serializable rules are less expressive than a programming language. That is healthy until teams try to rebuild a programming language inside the rule format.&lt;/p&gt;
&lt;p&gt;Custom operators are the escape hatch. Use them for stable domain concepts, not one campaign&apos;s special case. An operator such as &lt;code&gt;isBusinessDay&lt;/code&gt; may be reusable. An operator named after a temporary promotion is business data disguised as code.&lt;/p&gt;
&lt;p&gt;Rules should also be deterministic. Pass volatile values such as the current time, exchange rate, or feature assignment into the evaluation context. Hidden reads make replay and auditing unreliable.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const context = {
  customer,
  order,
  inventory,
  evaluatedAt: new Date().toISOString(),
  ruleVersion: 13,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Activation still needs engineering discipline&lt;/h2&gt;
&lt;p&gt;Moving a rule out of the application removes the application deployment. It does not remove risk.&lt;/p&gt;
&lt;p&gt;A production rule system needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;schema and operator validation;&lt;/li&gt;
&lt;li&gt;maximum depth and complexity limits;&lt;/li&gt;
&lt;li&gt;draft, review, and activation states;&lt;/li&gt;
&lt;li&gt;immutable versions with actor and timestamp;&lt;/li&gt;
&lt;li&gt;canary or shadow evaluation for sensitive changes;&lt;/li&gt;
&lt;li&gt;an immediate rollback path;&lt;/li&gt;
&lt;li&gt;metrics keyed by rule ID and version.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For authorization, fraud, lending, or pricing, the server remains authoritative. Client-side evaluation may improve feedback, but it must not become the security boundary.&lt;/p&gt;
&lt;h2&gt;Typed authoring helps&lt;/h2&gt;
&lt;p&gt;rule-engine-js v1.0.7 added typed path autocomplete:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type Context = {
  user: { age: number };
  order: { total: number };
};

const rules = createRuleHelpers&amp;lt;Context&amp;gt;();

rules.gte(&quot;user.age&quot;, 18);
rules.gt(&quot;order.total&quot;, 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The generic helpers catch many path and value mistakes while a rule is authored in TypeScript. Remotely supplied JSON still needs runtime validation.&lt;/p&gt;
&lt;p&gt;Serializable rules are worthwhile when portability and controlled change matter more than unrestricted expression. The constraint is not a workaround. It is the boundary that makes the system understandable.&lt;/p&gt;
&lt;p&gt;Sources: &lt;a href=&quot;https://github.com/crafts69guy/rule-engine-js&quot;&gt;repository&lt;/a&gt;, &lt;a href=&quot;https://github.com/crafts69guy/rule-engine-js/blob/production/CHANGELOG.md&quot;&gt;v1.0.7 changelog&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>javascript</category><category>typescript</category><category>production</category></item><item><title>Managing Dotfiles with Git and GNU Stow</title><link>https://cjs-crafts.com/blog/managing-dotfiles-with-git-and-gnu-stow</link><guid isPermaLink="true">https://cjs-crafts.com/blog/managing-dotfiles-with-git-and-gnu-stow</guid><description>A practical dotfiles workflow with Git and GNU Stow, including safe adoption, host-specific state, updates, and rollback.</description><pubDate>Sat, 23 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Dotfiles begin as a few hidden files in a home directory. Over time they become a record of how a machine works: shell behavior, editor settings, Git defaults, terminal themes, and small automation scripts.&lt;/p&gt;
&lt;p&gt;Git gives that history a home. GNU Stow turns files in a repository into symlinks at the paths applications expect.&lt;/p&gt;
&lt;h2&gt;The basic model&lt;/h2&gt;
&lt;p&gt;Suppose a repository contains:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;.dotfiles/
└── .config/
    ├── fish/
    └── nvim/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running Stow from the repository creates links under the target directory, normally the parent of the repository. The files stay versioned in &lt;code&gt;.dotfiles&lt;/code&gt;; applications read them through &lt;code&gt;~/.config&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My repository uses a single package rooted at &lt;code&gt;~/.dotfiles&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fish&quot;&gt;cd ~/.dotfiles
stow -Rv .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;-R&lt;/code&gt; option restows the package, and &lt;code&gt;-v&lt;/code&gt; shows what changes. Before applying an unfamiliar repository, use simulation mode:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fish&quot;&gt;stow -nRv .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read the output. Do not blindly replace existing configuration.&lt;/p&gt;
&lt;h2&gt;Adopt existing files carefully&lt;/h2&gt;
&lt;p&gt;If &lt;code&gt;~/.config/nvim&lt;/code&gt; already exists, Stow may report a conflict. Back it up first:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fish&quot;&gt;mv ~/.config/nvim ~/.config/nvim.before-stow
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then restow and compare the old files deliberately. GNU Stow also has an &lt;code&gt;--adopt&lt;/code&gt; option, but it moves existing files into the package. That is powerful and easy to misuse. Run it only with a clean Git worktree so the resulting diff shows exactly what was adopted.&lt;/p&gt;
&lt;h2&gt;Keep local state out of Git&lt;/h2&gt;
&lt;p&gt;A dotfiles repository should describe configuration, not every changing value on a machine.&lt;/p&gt;
&lt;p&gt;My current Hue theme switcher stores the active mood under &lt;code&gt;~/.local/state/hue-theme/current&lt;/code&gt;. Neovim language profile selection also lives in its state directory. Secrets, caches, plugin downloads, generated histories, and machine credentials stay outside the repository.&lt;/p&gt;
&lt;p&gt;For host differences, prefer small OS-specific files or environment variables over copying the entire configuration. The repository currently separates Fish configuration for macOS, Linux, and Windows.&lt;/p&gt;
&lt;h2&gt;Update as a transaction&lt;/h2&gt;
&lt;p&gt;A calm update loop looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fish&quot;&gt;cd ~/.dotfiles
git status --short
git pull --ff-only
stow -nRv .
stow -Rv .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check the worktree before pulling because local configuration is easy to edit accidentally through a symlink. Commit intentional changes first.&lt;/p&gt;
&lt;p&gt;My &lt;code&gt;stow_setup.sh&lt;/code&gt; also verifies shared agent rules, checks generated Hue integrations, rebuilds Bat&apos;s theme cache when needed, and configures Tide. Those steps are specific to this repository; plain Stow does not manage generated assets or application caches.&lt;/p&gt;
&lt;h2&gt;Rollback stays understandable&lt;/h2&gt;
&lt;p&gt;To remove the managed links without deleting the repository:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fish&quot;&gt;cd ~/.dotfiles
stow -Dv .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Git can restore an earlier configuration, while Stow can recreate its links. Keeping those two responsibilities separate is the reason the setup remains easy to reason about.&lt;/p&gt;
&lt;p&gt;Source: &lt;a href=&quot;https://github.com/crafts69guy/.dotfiles&quot;&gt;my current dotfiles&lt;/a&gt;, &lt;a href=&quot;https://www.gnu.org/software/stow/manual/stow.html&quot;&gt;GNU Stow manual&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>personal</category><category>production</category><category>tool</category></item><item><title>Neovim Without the Mystique</title><link>https://cjs-crafts.com/blog/neovim-without-the-mystique</link><guid isPermaLink="true">https://cjs-crafts.com/blog/neovim-without-the-mystique</guid><description>A calm introduction to Neovim: what it is, what to learn first, and how to avoid turning configuration into a second job.</description><pubDate>Sat, 23 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Neovim is a programmable text editor with Vim&apos;s editing model and a modern Lua-based configuration ecosystem. That description is accurate, but it can make the editor sound like a project you must finish before writing code.&lt;/p&gt;
&lt;p&gt;You do not need to build an editor from scratch. You need a small editing vocabulary, a way to inspect problems, and permission to leave the rest alone.&lt;/p&gt;
&lt;h2&gt;Learn the editing model first&lt;/h2&gt;
&lt;p&gt;Vim-style editing is modal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Normal mode is for navigation and commands.&lt;/li&gt;
&lt;li&gt;Insert mode is for entering text.&lt;/li&gt;
&lt;li&gt;Visual mode is for selecting text.&lt;/li&gt;
&lt;li&gt;Command-line mode is for editor commands.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first useful loop is small: open a file, move with &lt;code&gt;h&lt;/code&gt;, &lt;code&gt;j&lt;/code&gt;, &lt;code&gt;k&lt;/code&gt;, and &lt;code&gt;l&lt;/code&gt;, press &lt;code&gt;i&lt;/code&gt; to edit, press &lt;code&gt;Esc&lt;/code&gt; to return to Normal mode, then write with &lt;code&gt;:w&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Add motions as you need them. &lt;code&gt;w&lt;/code&gt; moves by word, &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;$&lt;/code&gt; move to line boundaries, &lt;code&gt;gg&lt;/code&gt; and &lt;code&gt;G&lt;/code&gt; move to the start and end of a file. Operators combine with motions: &lt;code&gt;dw&lt;/code&gt; deletes a word, while &lt;code&gt;ci&quot;&lt;/code&gt; changes text inside quotes.&lt;/p&gt;
&lt;p&gt;This composability matters more than memorizing a large keymap.&lt;/p&gt;
&lt;h2&gt;Start with a distribution if you want an IDE&lt;/h2&gt;
&lt;p&gt;A plain Neovim installation is intentionally sparse. A distribution such as &lt;a href=&quot;https://www.lazyvim.org/&quot;&gt;LazyVim&lt;/a&gt; provides sensible defaults for completion, language servers, formatting, search, and plugin management.&lt;/p&gt;
&lt;p&gt;That gives beginners a working environment and a real configuration to inspect. The trade-off is another layer of conventions. When a key behaves unexpectedly, check LazyVim&apos;s mapping before assuming Neovim itself is broken.&lt;/p&gt;
&lt;h2&gt;Keep the configuration boring&lt;/h2&gt;
&lt;p&gt;A healthy first configuration answers only a few questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which languages do I work in?&lt;/li&gt;
&lt;li&gt;Which formatter should run for each file type?&lt;/li&gt;
&lt;li&gt;How do I find files, search text, and inspect diagnostics?&lt;/li&gt;
&lt;li&gt;Which keymaps are important enough to customize?&lt;/li&gt;
&lt;li&gt;How can I reproduce this setup on another machine?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Add plugins to solve repeated friction, not to copy a screenshot. Every plugin creates another update and compatibility surface.&lt;/p&gt;
&lt;h2&gt;Know the repair tools&lt;/h2&gt;
&lt;p&gt;Three commands cover a surprising amount of debugging:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vim&quot;&gt;:checkhealth
:Lazy
:LspInfo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;:messages&lt;/code&gt; after an error and &lt;code&gt;:verbose map &amp;lt;key&amp;gt;&lt;/code&gt; to find who defined a mapping. Run Neovim without your configuration when isolating startup issues:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fish&quot;&gt;nvim --clean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The goal is not a perfect editor. It is an editor you can understand well enough to repair.&lt;/p&gt;
&lt;p&gt;My current setup still uses LazyVim, but it has changed repeatedly: Telescope and Neo-tree gave way to Snacks, Solarized Osaka gave way to a personal Hue theme, and language tooling is now selected through profiles. That evolution is normal. A configuration should follow the work, not preserve every early choice.&lt;/p&gt;
&lt;p&gt;Sources: &lt;a href=&quot;https://neovim.io/doc/&quot;&gt;Neovim documentation&lt;/a&gt;, &lt;a href=&quot;https://github.com/crafts69guy/.dotfiles&quot;&gt;my current dotfiles&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>learning</category><category>tool</category></item><item><title>A Practical LazyVim Setup for Web Development</title><link>https://cjs-crafts.com/blog/practical-lazyvim-setup-for-web-development</link><guid isPermaLink="true">https://cjs-crafts.com/blog/practical-lazyvim-setup-for-web-development</guid><description>Build a focused LazyVim environment for TypeScript work with explicit language tooling, formatting, and reproducible configuration.</description><pubDate>Sat, 23 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A useful Neovim setup is not the one with the most plugins. It is the one where editing, navigation, diagnostics, and formatting behave predictably.&lt;/p&gt;
&lt;p&gt;For web development, I start with LazyVim and add language support deliberately.&lt;/p&gt;
&lt;h2&gt;Install the foundation&lt;/h2&gt;
&lt;p&gt;On macOS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fish&quot;&gt;brew install neovim git fd ripgrep
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Confirm Neovim starts before adding configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fish&quot;&gt;nvim --version
nvim --clean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then follow the current &lt;a href=&quot;https://www.lazyvim.org/installation&quot;&gt;LazyVim installation guide&lt;/a&gt;. Keep your configuration in Git from the beginning. A broken experiment is much easier to undo when the previous state is a commit rather than a memory.&lt;/p&gt;
&lt;h2&gt;Enable only the language extras you need&lt;/h2&gt;
&lt;p&gt;My current configuration always loads JSON and Markdown support. A &lt;code&gt;web&lt;/code&gt; profile adds TypeScript, Tailwind CSS, Docker, Prisma, ESLint, and Prettier extras:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lua&quot;&gt;if profile.is(&quot;web&quot;) then
  vim.list_extend(spec, {
    { import = &quot;lazyvim.plugins.extras.lang.typescript&quot; },
    { import = &quot;lazyvim.plugins.extras.lang.tailwind&quot; },
    { import = &quot;lazyvim.plugins.extras.lang.docker&quot; },
    { import = &quot;lazyvim.plugins.extras.lang.prisma&quot; },
    { import = &quot;lazyvim.plugins.extras.linting.eslint&quot; },
    { import = &quot;lazyvim.plugins.extras.formatting.prettier&quot; },
  })
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is more maintainable than installing every language server on every machine. It also keeps the source of each default visible: LazyVim owns the baseline; local plugin files contain only deliberate overrides.&lt;/p&gt;
&lt;h2&gt;Separate responsibilities&lt;/h2&gt;
&lt;p&gt;I keep the setup split by concern:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;lua/
├── config/
│   ├── lazy.lua
│   ├── keymaps.lua
│   ├── options.lua
│   └── profile.lua
└── plugins/
    ├── coding.lua
    ├── conform.lua
    ├── lsp.lua
    ├── snacks.lua
    └── treesitter.lua
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Language servers provide diagnostics and code intelligence. Conform owns formatting. Snacks owns file search, grep, explorer, and terminal workflows. Blink provides completion. Clear ownership prevents two plugins from fighting over the same job.&lt;/p&gt;
&lt;h2&gt;Verify one layer at a time&lt;/h2&gt;
&lt;p&gt;After enabling a language:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;open a small real project;&lt;/li&gt;
&lt;li&gt;run &lt;code&gt;:checkhealth&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;inspect attached servers with &lt;code&gt;:LspInfo&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;confirm go-to-definition and diagnostics;&lt;/li&gt;
&lt;li&gt;format a deliberately messy file;&lt;/li&gt;
&lt;li&gt;restart once and repeat.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Do not debug completion, formatting, Treesitter, and LSP at the same time. Disable layers until the failure becomes obvious.&lt;/p&gt;
&lt;p&gt;The current configuration is available in &lt;a href=&quot;https://github.com/crafts69guy/.dotfiles/tree/production/.config/nvim&quot;&gt;my dotfiles&lt;/a&gt;. Copy ideas, not the whole directory. Your formatter choices, language mix, and keymap habits are the parts that should stay personal.&lt;/p&gt;
</content:encoded><category>learning</category><category>tool</category><category>typescript</category></item><item><title>Moving Business Rules Out of Application Code</title><link>https://cjs-crafts.com/blog/moving-business-rules-out-of-application-code</link><guid isPermaLink="true">https://cjs-crafts.com/blog/moving-business-rules-out-of-application-code</guid><description>A practical introduction to treating changing business decisions as data without turning your application into a configuration maze.</description><pubDate>Tue, 19 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A discount rule often begins as one readable &lt;code&gt;if&lt;/code&gt; statement. Then the exceptions arrive: customer tiers, regional limits, campaign windows, minimum totals, and one-off exclusions. The problem is not conditional logic itself. The problem is that a frequently changing business decision has become tangled with application flow.&lt;/p&gt;
&lt;p&gt;A rule engine gives that decision a boundary.&lt;/p&gt;
&lt;h2&gt;Start with the change rate&lt;/h2&gt;
&lt;p&gt;Do not move every condition into configuration. Code is still the clearest home for stable invariants and behavior tied closely to an algorithm.&lt;/p&gt;
&lt;p&gt;Rules become useful when a decision:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;changes more often than the surrounding application;&lt;/li&gt;
&lt;li&gt;needs an audit trail or explicit version;&lt;/li&gt;
&lt;li&gt;appears in several entry points;&lt;/li&gt;
&lt;li&gt;must be tested against many combinations of input;&lt;/li&gt;
&lt;li&gt;may eventually be managed outside a deployment.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is a small eligibility rule with &lt;a href=&quot;https://github.com/crafts69guy/rule-engine-js&quot;&gt;rule-engine-js&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { createRuleEngine, createRuleHelpers } from &quot;rule-engine-js&quot;;

type CheckoutContext = {
  customer: { tier: &quot;standard&quot; | &quot;gold&quot;; active: boolean };
  order: { total: number };
};

const engine = createRuleEngine();
const rules = createRuleHelpers&amp;lt;CheckoutContext&amp;gt;();

const canUseGoldDiscount = rules.and(
  rules.eq(&quot;customer.tier&quot;, &quot;gold&quot;),
  rules.eq(&quot;customer.active&quot;, true),
  rules.gte(&quot;order.total&quot;, 100),
);

const result = engine.evaluateExpr(canUseGoldDiscount, {
  customer: { tier: &quot;gold&quot;, active: true },
  order: { total: 140 },
});

console.log(result.success);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application still owns what happens next. The rule only answers a question.&lt;/p&gt;
&lt;h2&gt;Keep actions outside the rule&lt;/h2&gt;
&lt;p&gt;A useful separation is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;collect a trusted context;&lt;/li&gt;
&lt;li&gt;evaluate a rule;&lt;/li&gt;
&lt;li&gt;perform the action in application code.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That means a pricing rule can decide whether an order qualifies, but it should not charge a card or write directly to a database. Side effects stay in code where authentication, idempotency, retries, and observability are easier to enforce.&lt;/p&gt;
&lt;h2&gt;JSON is a transport format, not a governance model&lt;/h2&gt;
&lt;p&gt;A serializable rule can live in a file, database, or API response:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;and&quot;: [
    { &quot;eq&quot;: [&quot;customer.tier&quot;, &quot;gold&quot;] },
    { &quot;eq&quot;: [&quot;customer.active&quot;, true] },
    { &quot;gte&quot;: [&quot;order.total&quot;, 100] }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That flexibility is useful, but it also creates responsibility. Validate rule shape before activation. Limit rule depth and operator count. Version changes, record who approved them, and keep rollback simple. A bad configuration can break production just as effectively as bad code.&lt;/p&gt;
&lt;h2&gt;The real payoff&lt;/h2&gt;
&lt;p&gt;The main benefit is not fewer lines. It is a clearer ownership boundary: the engine handles evaluation, the rule describes a decision, and the application handles effects. Each part can change and be tested without hiding the others.&lt;/p&gt;
&lt;p&gt;As of v1.0.7, rule-engine-js includes typed path helpers, nested path resolution, validation helpers, state-change operators, and a stateful wrapper. Start with the plain engine. Add the advanced pieces only when the problem calls for them.&lt;/p&gt;
&lt;p&gt;Sources: &lt;a href=&quot;https://github.com/crafts69guy/rule-engine-js&quot;&gt;rule-engine-js repository&lt;/a&gt;, &lt;a href=&quot;https://www.npmjs.com/package/rule-engine-js&quot;&gt;npm package&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>javascript</category><category>typescript</category><category>rules</category></item><item><title>Refactoring Nested Conditions into Testable Rules</title><link>https://cjs-crafts.com/blog/refactoring-nested-conditions-into-testable-rules</link><guid isPermaLink="true">https://cjs-crafts.com/blog/refactoring-nested-conditions-into-testable-rules</guid><description>A small, repeatable refactoring for turning tangled access checks into named rules with explicit inputs and focused tests.</description><pubDate>Tue, 19 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The worst conditional in a codebase is rarely the longest one. It is the one nobody can change confidently.&lt;/p&gt;
&lt;p&gt;Access checks are a common example. A function starts with role checks, grows department exceptions, then absorbs account state and resource sensitivity. Shortening that function is helpful, but the real goal is to make the decision visible and testable.&lt;/p&gt;
&lt;h2&gt;Name the decision first&lt;/h2&gt;
&lt;p&gt;Before changing syntax, decide what the code is answering:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;May this user read this resource?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then define the smallest context that can answer it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type AccessContext = {
  user: {
    active: boolean;
    role: &quot;admin&quot; | &quot;member&quot;;
    department: string;
    permissions: string[];
  };
  resource: {
    department: string;
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This step exposes accidental dependencies. If a rule needs the entire request object, it is probably not isolated yet.&lt;/p&gt;
&lt;h2&gt;Express the rule&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { createRuleEngine, createRuleHelpers } from &quot;rule-engine-js&quot;;

const engine = createRuleEngine();
const rules = createRuleHelpers&amp;lt;AccessContext&amp;gt;();

const canReadResource = rules.and(
  rules.eq(&quot;user.active&quot;, true),
  rules.or(
    rules.eq(&quot;user.role&quot;, &quot;admin&quot;),
    rules.and(
      rules.field.equals(&quot;user.department&quot;, &quot;resource.department&quot;),
      rules.in(&quot;read&quot;, &quot;user.permissions&quot;),
    ),
  ),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The helper syntax produces a serializable rule. More importantly, the branches now read like the policy they implement.&lt;/p&gt;
&lt;p&gt;Evaluation returns a result object:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const decision = engine.evaluateExpr(canReadResource, context);

if (!decision.success) {
  throw new Error(&quot;Access denied&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keep the HTTP response, logging, and other side effects outside the rule.&lt;/p&gt;
&lt;h2&gt;Test the matrix, not the implementation&lt;/h2&gt;
&lt;p&gt;Rules invite table-driven tests. Each row describes a business case rather than a branch in the old function.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const cases = [
  [&quot;active admin&quot;, activeAdmin, true],
  [&quot;inactive admin&quot;, inactiveAdmin, false],
  [&quot;member in same department&quot;, sameDepartmentMember, true],
  [&quot;member without read permission&quot;, memberWithoutRead, false],
] as const;

for (const [name, context, expected] of cases) {
  test(name, () =&amp;gt; {
    expect(engine.evaluateExpr(canReadResource, context).success).toBe(
      expected,
    );
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add boundary cases: missing paths, empty permission arrays, unknown roles, and malformed remote rules. Those are usually more valuable than testing the helper calls themselves.&lt;/p&gt;
&lt;h2&gt;Refactor in two passes&lt;/h2&gt;
&lt;p&gt;First, preserve behavior. Put characterization tests around the existing function, introduce the rule, and compare both results against the same fixtures.&lt;/p&gt;
&lt;p&gt;Second, improve the policy. Remove obsolete exceptions, split unrelated decisions, and give each rule a stable name and version. Combining cleanup with migration makes regressions much harder to diagnose.&lt;/p&gt;
&lt;p&gt;A rule engine does not make complicated policy simple. It makes the complication explicit. That is enough to improve reviews, tests, and future changes.&lt;/p&gt;
&lt;p&gt;Source: &lt;a href=&quot;https://github.com/crafts69guy/rule-engine-js&quot;&gt;rule-engine-js v1.0.7&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>javascript</category><category>typescript</category><category>refactoring</category></item></channel></rss>