<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: loach</title>
    <description>The latest articles on DEV Community by loach (@loach2009).</description>
    <link>https://dev.to/loach2009</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4119133%2F9a7d0107-ed42-4ca8-a57a-d5b67333a4f5.png</url>
      <title>DEV Community: loach</title>
      <link>https://dev.to/loach2009</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/loach2009"/>
    <language>en</language>
    <item>
      <title>PC Engine CD BIOS Progress: 8 DEV Entries, 0 Verified Compatible APIs</title>
      <dc:creator>loach</dc:creator>
      <pubDate>Thu, 22 Oct 2026 03:26:53 +0000</pubDate>
      <link>https://dev.to/loach2009/pc-engine-cd-bios-progress-8-dev-entries-0-verified-compatible-apis-3cl8</link>
      <guid>https://dev.to/loach2009/pc-engine-cd-bios-progress-8-dev-entries-0-verified-compatible-apis-3cl8</guid>
      <description>&lt;p&gt;I'm working on &lt;a href="https://github.com/mao2009/oss-pce-cd-bios" rel="noopener noreferrer"&gt;oss-pce-cd-bios&lt;/a&gt;, an effort to build an open-source replacement for the PC Engine / TurboGrafx-CD System Card BIOS.&lt;/p&gt;

&lt;p&gt;My previous update focused on original HuC6280 routines and a synthetic CD-to-ADPCM DMA diagnostic. This update has a more visible milestone:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The development ROM now has eight implemented entry points out of 81 candidates, up from just one in the previous post.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's genuine implementation progress. But it is &lt;strong&gt;not&lt;/strong&gt; the same as eight compatible System Card services, and it doesn't mean any commercial CD game boots.&lt;/p&gt;

&lt;h2&gt;
  
  
  From standalone routines to actual DEV ROM entries
&lt;/h2&gt;

&lt;p&gt;The current 256 KiB development-only image has project-owned implementations installed at these candidate jump-table slots:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Slot&lt;/th&gt;
&lt;th&gt;Routine&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$1E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EX_GETVER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Development version response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$3C&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EX_BINBCD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Numeric conversion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$3D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EX_BCDBIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Numeric conversion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$3F&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MA_MUL8U&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unsigned 8-bit multiplication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$40&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MA_MUL8S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Signed 8-bit multiplication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$41&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MA_MUL16U&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unsigned 16-bit multiplication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$42&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MA_DIV16S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Signed 16-bit division&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$43&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MA_DIV16U&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unsigned 16-bit division&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The remaining &lt;strong&gt;73 slots are nonreturning stubs&lt;/strong&gt;, not functional API implementations.&lt;/p&gt;

&lt;p&gt;Previously, several math and conversion routines were tested only as independent components. They have now been wired into the development ROM's actual entry points and exercised under a project-defined calling convention with a pinned &lt;a href="https://github.com/drhelius/Geargrafx" rel="noopener noreferrer"&gt;Geargrafx&lt;/a&gt; core.&lt;/p&gt;

&lt;p&gt;The newest addition, &lt;a href="https://github.com/mao2009/oss-pce-cd-bios/pull/480" rel="noopener noreferrer"&gt;&lt;code&gt;MA_DIV16U&lt;/code&gt; in PR #480&lt;/a&gt;, checks quotient/remainder behavior, divide-by-zero handling, stack boundaries, caller interrupt/decimal flag combinations, and changes across the 8 KiB working RAM. Deliberately modified variants must be rejected for the &lt;em&gt;expected&lt;/em&gt; reason rather than merely producing any failure.&lt;/p&gt;

&lt;p&gt;The signed/unsigned arithmetic routines are original implementations, and their DEV-side contracts are being tested. They &lt;strong&gt;are not claims of compatibility with the proprietary reference System Card ABI&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 8/81 isn't a compatibility percentage
&lt;/h2&gt;

&lt;p&gt;The project now tracks separate measurements instead of merging different kinds of evidence into one progress bar:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Current main&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Project-defined DEV ROM entries implemented&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8/81&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remaining nonreturning stubs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;73/81&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reference BIOS ABIs independently VERIFIED&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/81&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Owner-approved DEV_CALLABLE entries&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reference BIOS JSR executions permitted by the current gate&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/81&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verified BIOS-mediated CD program boots&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This distinction is important. An entry can execute and return according to &lt;strong&gt;our&lt;/strong&gt; development contract without proving that a game can call it using the &lt;strong&gt;original&lt;/strong&gt; System Card's parameters, registers, flags, memory layout, or error behavior.&lt;/p&gt;

&lt;p&gt;In other words, the source code has advanced much faster than compatibility certification. The latter remains intentionally conservative.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/mao2009/oss-pce-cd-bios/blob/main/docs/status.md" rel="noopener noreferrer"&gt;current status ledger&lt;/a&gt; documents these independent axes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learning about the reference BIOS without executing it
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/mao2009/oss-pce-cd-bios/pull/472" rel="noopener noreferrer"&gt;static ABI investigation in PR #472&lt;/a&gt; added a local-only auditor and evidence ledger for seven reference BIOS entry points.&lt;/p&gt;

&lt;p&gt;Instead of jumping directly into proprietary firmware code, the audit records narrowly scoped static properties: control-flow shape, memory writes, bank-register operations, and hazards that would make an execution experiment unsafe.&lt;/p&gt;

&lt;p&gt;One important observation is that some reference math entry points are dispatchers that change memory page registers. An apparently simple multiplication API can therefore carry hardware-visible side effects that an independent arithmetic routine does not reproduce.&lt;/p&gt;

&lt;p&gt;A static safety classification is &lt;strong&gt;not&lt;/strong&gt; permission to run the reference code. The project still blocks all 81 reference JSR entry points. No proprietary BIOS bytes, extracted code, or commercial game content were published with this work.&lt;/p&gt;

&lt;h2&gt;
  
  
  A guard can detect trouble too late
&lt;/h2&gt;

&lt;p&gt;Another revealing experiment is &lt;a href="https://github.com/mao2009/oss-pce-cd-bios/pull/481" rel="noopener noreferrer"&gt;PR #481&lt;/a&gt;, which tested a synthetic HuC6280 caller/stack contract on a pinned, unmodified Geargrafx core.&lt;/p&gt;

&lt;p&gt;With the expected WRAM stack mapping, the test caller returned and its measured CPU state was preserved. With the stack mapped to I/O instead, the CPU's JSR and subsequent stack pushes changed VDC state &lt;strong&gt;before the guard detected the invalid mapping&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This demonstrates a subtle failure of the “check, then refuse” approach: checking after a side-effecting instruction cannot undo the side effect that already happened.&lt;/p&gt;

&lt;p&gt;The result doesn't prove a production-safe guard exists. Nor is the experiment a complete I/O bus trace or a real-hardware result. It provides a reproducible counterexample that the eventual safety design must address.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;As of &lt;strong&gt;October 22, 2026&lt;/strong&gt;, the goal is still a genuinely compatible and redistributable System Card implementation—not merely a development ROM with increasingly many working routines.&lt;/p&gt;

&lt;p&gt;The immediate work is to verify caller-visible ABI contracts safely, establish testable CD services such as &lt;code&gt;CD_RESET&lt;/code&gt; and &lt;code&gt;CD_READ&lt;/code&gt;, and eventually boot a &lt;strong&gt;self-authored disc through actual BIOS services&lt;/strong&gt;. Comparison against a second emulator and physical hardware will be needed before broader compatibility claims.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No retail game boot has been demonstrated. The reference ABI has zero VERIFIED entries. The release gate remains blocked (&lt;code&gt;release_eligible: false&lt;/code&gt;).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Going from one to eight implemented DEV entries is meaningful progress. Keeping that separate from “eight compatible BIOS APIs” is just as important.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/mao2009/oss-pce-cd-bios" rel="noopener noreferrer"&gt;https://github.com/mao2009/oss-pce-cd-bios&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is an independent project, unaffiliated with NEC, Hudson Soft, Konami, or other rights holders. It does not distribute proprietary BIOS images, commercial game images, or game-derived content.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>PC Engine CD BIOS Progress: Tested HuC6280 Routines and ADPCM DMA</title>
      <dc:creator>loach</dc:creator>
      <pubDate>Wed, 21 Oct 2026 03:48:30 +0000</pubDate>
      <link>https://dev.to/loach2009/pc-engine-cd-bios-progress-tested-huc6280-routines-and-adpcm-dma-2c64</link>
      <guid>https://dev.to/loach2009/pc-engine-cd-bios-progress-tested-huc6280-routines-and-adpcm-dma-2c64</guid>
      <description>&lt;p&gt;I'm building &lt;a href="https://github.com/mao2009/oss-pce-cd-bios" rel="noopener noreferrer"&gt;oss-pce-cd-bios&lt;/a&gt;, an open-source project aiming for a redistributable replacement for the PC Engine / TurboGrafx-CD System Card BIOS.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/loach2009/building-an-open-source-pc-engine-cd-bios-from-reading-sectors-to-running-code-from-ram-41ij"&gt;previous update&lt;/a&gt;, I described reading sectors from a self-authored CD image and executing a small synthetic HuC6280 payload from RAM. Those were isolated diagnostic experiments—not CD game boot.&lt;/p&gt;

&lt;p&gt;Since then, the focus has moved toward &lt;strong&gt;implementing original low-level routines and testing their actual effects in a pinned emulator core&lt;/strong&gt;. The results are useful, but it's important to keep the boundary between a tested routine and a compatible BIOS service explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven original routines exercised in the actual CPU core
&lt;/h2&gt;

&lt;p&gt;One tangible change is a set of independent HuC6280 assembly routines. The current opt-in real-core test target covers seven:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;EX_BINBCD&lt;/code&gt; and &lt;code&gt;EX_BCDBIN&lt;/code&gt; (numeric conversions)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MA_MUL8S&lt;/code&gt; and &lt;code&gt;MA_MUL8U&lt;/code&gt; (8-bit multiplication)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MA_MUL16U&lt;/code&gt; (16-by-16-bit unsigned multiplication)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MA_DIV16U&lt;/code&gt; and &lt;code&gt;MA_DIV16S&lt;/code&gt; (16-bit division)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't just Python models: the test harnesses execute original guest instructions with a pinned &lt;a href="https://github.com/drhelius/Geargrafx" rel="noopener noreferrer"&gt;Geargrafx&lt;/a&gt; core and check outcomes and relevant CPU state. For example, the unsigned 16-bit division work exercises edge cases and 65,536 generated operand pairs while checking quotient/remainder invariants, register/stack behavior, and deliberately broken variants.&lt;/p&gt;

&lt;p&gt;An additional &lt;code&gt;EX_RND&lt;/code&gt; implementation provides an original 16-bit LFSR-based random-number routine with its own tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;None of these standalone routines has been promoted to a caller-compatible System Card API.&lt;/strong&gt; Their local contracts are not automatically the reference BIOS calling conventions. In particular, they are not connected to the development ROM's API jump table.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/mao2009/oss-pce-cd-bios/pull/391" rel="noopener noreferrer"&gt;real-core integration change&lt;/a&gt; records the seven-routine target; the &lt;a href="https://github.com/mao2009/oss-pce-cd-bios/pull/386" rel="noopener noreferrer"&gt;division implementation&lt;/a&gt; is a concrete example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving a synthetic CD sector through ADPCM DMA
&lt;/h2&gt;

&lt;p&gt;The earlier CD read experiment transferred sector data into ordinary guest RAM. A newer experiment exercises a different hardware path.&lt;/p&gt;

&lt;p&gt;A self-authored HuC6280 guest issues SCSI &lt;code&gt;READ(6)&lt;/code&gt; through the emulated CD interface and arms ADPCM DMA via register &lt;code&gt;$180B&lt;/code&gt;, moving data into ADPCM RAM starting at &lt;code&gt;$0100&lt;/code&gt;. The host probe observes the resulting device state and memory; it does &lt;strong&gt;not&lt;/strong&gt; perform the transfer by directly calling a host-side ADPCM write method.&lt;/p&gt;

&lt;p&gt;The test includes both a successful transfer and a negative case where DMA is not armed. It also stores the raw observations, rechecks them against the source/build hashes and known toolchain/core versions, and verifies that twelve selected manipulations of the evidence are rejected.&lt;/p&gt;

&lt;p&gt;That extra evidence work matters: if a result can't be reproduced or its recorded inputs can't be bound to what actually ran, a green status is much less informative.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/mao2009/oss-pce-cd-bios/pull/406" rel="noopener noreferrer"&gt;ADPCM DMA experiment&lt;/a&gt; was merged on October 21.&lt;/p&gt;

&lt;p&gt;But the boundary remains clear: this is a &lt;strong&gt;device-layer transfer diagnostic&lt;/strong&gt;. It does not implement the System Card &lt;code&gt;AD_TRANS&lt;/code&gt; service, establish its ABI, or make the BIOS bootable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observing CD status and subcode-Q without claiming BIOS compatibility
&lt;/h2&gt;

&lt;p&gt;Two more lines of investigation now have real-core observations.&lt;/p&gt;

&lt;p&gt;For the CD status work, a test distinguishes a synthetic disc from no media and checks how cases such as an unrecognised disc and selection timeout are classified. For subcode-Q, the project decodes an observed &lt;code&gt;$DD&lt;/code&gt; response and checks track/time/status information at a particular synthetic-disc location.&lt;/p&gt;

&lt;p&gt;The real-core subcode-Q result is deliberately narrow: &lt;strong&gt;track 2 at LBA 66&lt;/strong&gt;, with playing, paused, and stopped states. Other track boundaries have host-side synthetic tests, but those cases have not all been measured in the actual emulator core.&lt;/p&gt;

&lt;p&gt;These investigations provide building blocks for future &lt;code&gt;CD_STAT&lt;/code&gt; and &lt;code&gt;CD_SUBQ&lt;/code&gt; work. They do &lt;strong&gt;not&lt;/strong&gt; mean their reference System Card entry points have been executed or reproduced.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://github.com/mao2009/oss-pce-cd-bios/pull/409" rel="noopener noreferrer"&gt;CD status diagnostic&lt;/a&gt; and &lt;a href="https://github.com/mao2009/oss-pce-cd-bios/pull/412" rel="noopener noreferrer"&gt;subcode-Q diagnostic&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the test boundary harder to misunderstand
&lt;/h2&gt;

&lt;p&gt;The default test workflow has also been clarified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make &lt;span class="nb"&gt;test&lt;/span&gt;            &lt;span class="c"&gt;# build and host-side checks&lt;/span&gt;
make test-real-core  &lt;span class="c"&gt;# required Geargrafx integration checks&lt;/span&gt;
make routine-real-core  &lt;span class="c"&gt;# opt-in standalone routine checks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The required CI path still runs the real-core checks; they weren't removed to make the host suite greener. The separate standalone-routine target remains opt-in rather than a required CI gate. Missing prerequisites and tests that haven't run must not be reported as passing observations.&lt;/p&gt;

&lt;p&gt;Another diagnostic now checks all 8 KiB of working RAM around a &lt;strong&gt;synthetic ROM-to-RAM handoff&lt;/strong&gt;, rejecting unexpected modifications. It does not read code from a CD or demonstrate the actual &lt;code&gt;CD_EXEC&lt;/code&gt; API.&lt;/p&gt;

&lt;h2&gt;
  
  
  So how many BIOS APIs actually work?
&lt;/h2&gt;

&lt;p&gt;The honest answer is still small.&lt;/p&gt;

&lt;p&gt;The development ROM has &lt;strong&gt;81 candidate API slots, and exactly one is callable as a development-only service: &lt;code&gt;EX_GETVER&lt;/code&gt;&lt;/strong&gt;. The other 80 slots remain nonreturning stubs. Adding independently tested arithmetic routines does &lt;em&gt;not&lt;/em&gt; change that number.&lt;/p&gt;

&lt;p&gt;The reference System Card ABI remains unverified. Callable &lt;code&gt;CD_RESET&lt;/code&gt;, &lt;code&gt;CD_READ&lt;/code&gt;, and &lt;code&gt;CD_EXEC&lt;/code&gt; services, BIOS-mediated homebrew CD boot, commercial-game compatibility, and a releasable firmware image are still missing. The release gate remains blocked.&lt;/p&gt;

&lt;p&gt;The distinction is the central point of this update:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A tested HuC6280 routine isn't necessarily a compatible BIOS API. A demonstrated DMA path isn't &lt;code&gt;AD_TRANS&lt;/code&gt;. An observed CD status response isn't a finished &lt;code&gt;CD_STAT&lt;/code&gt; service.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Still, these experiments move the project from preparing diagnostic scaffolding toward original, measured building blocks. That's the progress I want to report—without claiming a usable replacement BIOS before the evidence supports one.&lt;/p&gt;

&lt;p&gt;If you're interested in HuC6280 assembly, firmware replacement, CD-ROM² devices, or emulator-driven verification, technical feedback is welcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/mao2009/oss-pce-cd-bios" rel="noopener noreferrer"&gt;https://github.com/mao2009/oss-pce-cd-bios&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is an independent project, unaffiliated with NEC, Hudson Soft, Konami, or other rights holders. It does not distribute proprietary BIOS images, commercial game images, or game-derived data.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>assembly</category>
      <category>gamedev</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building an Open-Source PC Engine CD BIOS: From Reading Sectors to Running Code from RAM</title>
      <dc:creator>loach</dc:creator>
      <pubDate>Fri, 16 Oct 2026 04:21:00 +0000</pubDate>
      <link>https://dev.to/loach2009/building-an-open-source-pc-engine-cd-bios-from-reading-sectors-to-running-code-from-ram-41ij</link>
      <guid>https://dev.to/loach2009/building-an-open-source-pc-engine-cd-bios-from-reading-sectors-to-running-code-from-ram-41ij</guid>
      <description>&lt;p&gt;I'm working on &lt;a href="https://github.com/mao2009/oss-pce-cd-bios" rel="noopener noreferrer"&gt;oss-pce-cd-bios&lt;/a&gt;, an open-source effort to create a redistributable replacement for the &lt;strong&gt;PC Engine / TurboGrafx-CD System Card BIOS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The initial long-term target is behavior compatible with Super System Card 3.0, using original code and third-party OSS only where its licensing and suitability have been reviewed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The project does not have a usable replacement BIOS yet.&lt;/strong&gt; No commercial CD title has been shown booting with it. But the development experiments have progressed from building diagnostic ROMs to reading a synthetic CD sector &lt;em&gt;and executing code loaded from it&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Here's what has been established so far—and what hasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting useful evidence from the real emulator core
&lt;/h2&gt;

&lt;p&gt;The PC Engine uses a HuC6280 CPU, bank-switched memory, and a separate CD-ROM interface. A replacement System Card needs more than a ROM file with the expected size: it needs correct startup behavior, bank mapping, calling conventions, and device interactions.&lt;/p&gt;

&lt;p&gt;I'm using a pinned build of &lt;a href="https://github.com/drhelius/Geargrafx" rel="noopener noreferrer"&gt;Geargrafx&lt;/a&gt; as the first instrumented execution environment.&lt;/p&gt;

&lt;p&gt;The repository now builds original diagnostic ROMs and checks actual CPU execution, including program-counter transitions, MPR (memory page register) state, stack behavior, and working RAM. There are also deliberately broken ROM variants that must &lt;em&gt;fail&lt;/em&gt; the same checks.&lt;/p&gt;

&lt;p&gt;The 256 KiB development image includes a scaffold for &lt;strong&gt;81 candidate System Card API slots&lt;/strong&gt;. Those entries are placeholders that stop rather than return success. &lt;strong&gt;None of those 81 services is implemented as a compatible BIOS API yet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mao2009/oss-pce-cd-bios/blob/main/docs/api-stub-scaffold.md" rel="noopener noreferrer"&gt;Diagnostic API scaffold and its limits&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading 2,048 bytes from a synthetic CD
&lt;/h2&gt;

&lt;p&gt;The next step was an independent, development-only HuC6280 program that drives the emulated CD-ROM² controller.&lt;/p&gt;

&lt;p&gt;It sends a SCSI &lt;code&gt;READ(6)&lt;/code&gt; command through the CD interface registers and transfers a MODE1 data sector to guest RAM. The verifier checks all &lt;strong&gt;2,048 user-data bytes&lt;/strong&gt; against a separately generated reference. A three-sector, &lt;strong&gt;6,144-byte&lt;/strong&gt; case is covered too.&lt;/p&gt;

&lt;p&gt;This runs through guest HuC6280 instructions in the actual Geargrafx core, rather than copying the expected data into RAM from the host side. Cold- and warm-reset cases are checked, along with negative scenarios such as invalid addresses, damaged disc data, and interrupted transfers.&lt;/p&gt;

&lt;p&gt;This is an important low-level milestone, but it is &lt;strong&gt;not&lt;/strong&gt; the System Card &lt;code&gt;CD_READ&lt;/code&gt; API. Its calling convention, return behavior, interrupts, retries, and compatibility with real hardware remain separate work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mao2009/oss-pce-cd-bios/blob/main/docs/cd-read-diagnostic.md" rel="noopener noreferrer"&gt;CD sector-read diagnostic&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A new milestone: load code from CD and execute it in RAM
&lt;/h2&gt;

&lt;p&gt;The most recent experiment connects a few of these pieces into one traceable sequence.&lt;/p&gt;

&lt;p&gt;A small, independently assembled &lt;strong&gt;28-byte HuC6280 program&lt;/strong&gt; is embedded in sector 5 of an entirely synthetic CUE/BIN image. The development ROM then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Synthetic CD sector (LBA 5)
             |
       SCSI READ(6)
             |
   2,048 bytes into RAM
             |
   Validate payload header
             |
     Copy code to $3000
             |
      Jump to RAM code
             |
   Write observable markers
             |
 Return to diagnostic ROM
             |
       Confirm halt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test checks the order of the actual execution checkpoints, the bytes copied into RAM, and the marker values written by the downloaded code. It also confirms the return to the diagnostic ROM and the final halt over 64 instructions.&lt;/p&gt;

&lt;p&gt;Corrupting the payload checksum, header, or entry offset must prevent execution. Other negative tests ensure that simply reaching an expected-looking result cannot substitute for the correct execution path.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;a custom load-and-execute experiment&lt;/strong&gt;, not CD game boot. It does not implement the System Card boot process or prove that any existing game can run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mao2009/oss-pce-cd-bios/blob/main/docs/cd-load-exec-probe.md" rel="noopener noreferrer"&gt;Load-and-execute implementation and evidence&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The less visible work matters too
&lt;/h2&gt;

&lt;p&gt;A few supporting pieces have landed alongside the ROM experiments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disc inspection:&lt;/strong&gt; CUE/BIN and TOC diagnostic tools check track boundaries, &lt;code&gt;INDEX 00/01&lt;/code&gt;, pregaps, LBA/MSF calculations, and malformed inputs using synthetic fixtures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bus-reset observation:&lt;/strong&gt; a separate Geargrafx experiment measures two reset pulses through &lt;code&gt;$1804&lt;/code&gt; and verifies the bus is free afterward. This is not proof of the System Card &lt;code&gt;CD_RESET&lt;/code&gt; service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence and isolation:&lt;/strong&gt; an isolated trace harness distinguishes &lt;code&gt;PASS&lt;/code&gt;, &lt;code&gt;FAIL&lt;/code&gt;, &lt;code&gt;BLOCKED&lt;/code&gt;, and &lt;code&gt;SKIP&lt;/code&gt; instead of treating missing prerequisites as success.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source provenance:&lt;/strong&gt; an automated guard checks for suspicious binary or proprietary material in changes. It reduces risk but is not a legal guarantee; adopting third-party code still requires human review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current main-branch diagnostic CI has passed for the latest load-and-execute work. Reproducing and checking the scope of those results is part of the project, not an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;A useful homebrew-CD boot path is still ahead, followed by validated System Card entry points, error handling, interrupts, and comparison against an independent emulator and eventually real hardware.&lt;/p&gt;

&lt;p&gt;I want to keep the evidence labels precise:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A ROM that loads is not a BIOS that boots. A sector that reads is not an implemented &lt;code&gt;CD_READ&lt;/code&gt; service. A 28-byte test program running from RAM is not commercial game compatibility.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Still, being able to follow bytes from a self-authored disc, through the guest CD interface, into RAM, and then watch the guest CPU execute those bytes is genuine progress.&lt;/p&gt;

&lt;p&gt;If you work on HuC6280 assembly, CD-ROM² emulation, firmware replacement, or reproducible compatibility tests, I'd welcome technical feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mao2009/oss-pce-cd-bios" rel="noopener noreferrer"&gt;https://github.com/mao2009/oss-pce-cd-bios&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This project is independent of NEC, Hudson Soft, Konami, and other rights holders. It does not distribute proprietary BIOS images, commercial games, or game-derived disc data.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>PSXRecompStudio Progress: Persona's Opening, a Recompiler Demo, and a Runtime Mystery</title>
      <dc:creator>loach</dc:creator>
      <pubDate>Fri, 16 Oct 2026 04:01:23 +0000</pubDate>
      <link>https://dev.to/loach2009/psxrecompstudio-progress-personas-opening-a-recompiler-demo-and-a-runtime-mystery-3p92</link>
      <guid>https://dev.to/loach2009/psxrecompstudio-progress-personas-opening-a-recompiler-demo-and-a-runtime-mystery-3p92</guid>
      <description>&lt;p&gt;I'm building &lt;a href="https://github.com/mao2009/PSXRecompStudio" rel="noopener noreferrer"&gt;PSXRecompStudio&lt;/a&gt;, an open-source research project exploring static recompilation of PlayStation 1 software.&lt;/p&gt;

&lt;p&gt;The long-term goal is to help authorized rights holders turn existing PS1 games into maintainable, rebuildable ports, even when their original source code and build tools are no longer available.&lt;/p&gt;

&lt;p&gt;This is still research-stage work, not a way to recompile and play a complete commercial game today. But over the past few weeks, several parts of the project have become much easier to demonstrate and measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing Persona's opening for the first time
&lt;/h2&gt;

&lt;p&gt;One of the most encouraging milestones was getting visible opening imagery from &lt;em&gt;Megami Ibunroku Persona&lt;/em&gt; (&lt;em&gt;Revelations: Persona&lt;/em&gt;) through the &lt;strong&gt;OpenBIOS interpreter execution path&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I added screenshots captured at specific VBlank intervals rather than trying to reconstruct an earlier frame from the final state of video memory. The resulting captures show the ATLUS logo and opening text referencing Zhuangzi's butterfly dream.&lt;/p&gt;

&lt;p&gt;That matters because the project had spent a long time making progress that was mostly visible in CPU traces, BIOS calls, and test output. Seeing recognizable frames is a different kind of feedback.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fri0qbngotren4my8droy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fri0qbngotren4my8droy.png" alt="Persona opening: ATLUS logo" width="320" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The qualification is important: &lt;strong&gt;this is interpreter execution, not a successfully recompiled version of Persona&lt;/strong&gt;. Reaching the title screen, playing the game, and matching this behavior in the generated native host remain unproven.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mao2009/PSXRecompStudio/pull/754" rel="noopener noreferrer"&gt;How the VBlank capture was implemented (PR #754)&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A tiny PS1 executable can go all the way through recompilation
&lt;/h2&gt;

&lt;p&gt;There is also now a reproducible showcase for the actual recompilation pipeline.&lt;/p&gt;

&lt;p&gt;Using the released &lt;code&gt;v0.0.0-alpha.2&lt;/code&gt; CLI, a script generates an original five-instruction PS-X EXE, then follows this path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Synthetic PS-X EXE
       |
       v
MIPS analysis and lowering
       |
       v
Generated host code
       |
       v
Native artifact build
       |
       v
Execute and inspect output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo prints &lt;code&gt;P&lt;/code&gt; to the terminal. Real console captures document the result on Windows, and an equivalent released-binary run was verified on Linux.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffqodd4e9r73g9f0ctz29.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffqodd4e9r73g9f0ctz29.png" alt="Recompiler demo in a real console" width="800" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Five instructions do not demonstrate compatibility with an entire commercial game. They do demonstrate that analysis, code generation, building, and execution can work together end to end for a controlled input.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mao2009/PSXRecompStudio/pull/781" rel="noopener noreferrer"&gt;Demo verification and screenshots (PR #781)&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard part: two execution paths, different results
&lt;/h2&gt;

&lt;p&gt;The biggest open problem is less photogenic.&lt;/p&gt;

&lt;p&gt;The interpreter can advance through Persona's opening, but the &lt;strong&gt;standalone AOT + interpreter-fallback runtime still produces a black screen&lt;/strong&gt; in the investigated scenario.&lt;/p&gt;

&lt;p&gt;AOT means &lt;em&gt;ahead-of-time compilation&lt;/em&gt;: known PS1 instructions are translated into host code before execution. When the generated host encounters a path it cannot handle, an interpreter can take over.&lt;/p&gt;

&lt;p&gt;I am comparing the two execution paths instead of assuming the GPU renderer is responsible.&lt;/p&gt;

&lt;p&gt;At one shared checkpoint, the recorded CPU state, RAM, VRAM, GPU state, captured device registers, and guest-cycle count match. &lt;strong&gt;That is equality at one checkpoint, not a claim that all preceding execution was identical.&lt;/strong&gt; Their subsequent instruction paths diverge, and the root cause has not yet been identified.&lt;/p&gt;

&lt;p&gt;One useful outcome of this investigation was finding a misleading diagnostic: a run stopped by an explicit request could be reported as if it had exhausted its instruction budget. The diagnostic was corrected, but the underlying rendering divergence is still open.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mao2009/PSXRecompStudio/pull/787" rel="noopener noreferrer"&gt;Investigation and measured limits (draft PR #787)&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete runtime optimization
&lt;/h2&gt;

&lt;p&gt;On October 16, I merged a smaller, measurable change to the generated-host runtime.&lt;/p&gt;

&lt;p&gt;The runtime was spending a lot of time exchanging synchronous messages between generated code and the host. In particular, a guest-time report was often sent immediately before an MMIO access.&lt;/p&gt;

&lt;p&gt;Combining that time report with the MMIO request into one message reduced &lt;strong&gt;IPC round trips per guest instruction from 0.106 to 0.071&lt;/strong&gt;, approximately &lt;strong&gt;33% fewer&lt;/strong&gt; in the measured setup.&lt;/p&gt;

&lt;p&gt;A separate throughput measurement had a higher median, but considerable host noise, so I'm treating the round-trip reduction as the more reliable result.&lt;/p&gt;

&lt;p&gt;This improves communication overhead; it &lt;strong&gt;does not&lt;/strong&gt; establish that Persona now boots further or that the black-screen divergence is fixed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mao2009/PSXRecompStudio/pull/796" rel="noopener noreferrer"&gt;IPC optimization and measurements (PR #796)&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests should say what they actually tested
&lt;/h2&gt;

&lt;p&gt;Another recent change was deliberately unglamorous: integration tests requiring real CHD files now &lt;strong&gt;skip explicitly&lt;/strong&gt; when their input is unavailable, instead of silently returning and appearing to pass.&lt;/p&gt;

&lt;p&gt;With a lawful local fixture present, the relevant tests can run normally. Without it, the test report distinguishes missing evidence from successful execution.&lt;/p&gt;

&lt;p&gt;I want the project's status reports to keep three things separate: what a synthetic input proves, what a locally supplied real disc proves, and what has not been verified.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mao2009/PSXRecompStudio/pull/791" rel="noopener noreferrer"&gt;Real-ROM test reporting fix (PR #791)&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;The immediate priority is to identify the first meaningful behavioral difference between interpreter and generated-host execution, then extend the runtime only with evidence for the missing behavior.&lt;/p&gt;

&lt;p&gt;There is still substantial work ahead in BIOS and device behavior, recompiler coverage, runtime integration, and independent validation. A working demo and recognizable screenshots are milestones, not compatibility guarantees.&lt;/p&gt;

&lt;p&gt;But I'm happy to have moved from &lt;em&gt;“the traces are changing”&lt;/em&gt; to a project with visible frames, a reproducible native-code demo, a precise unsolved divergence, and measurements that can guide performance work.&lt;/p&gt;

&lt;p&gt;If you work on emulators, recompilers, MIPS, or retro-game preservation, I'd be interested in technical feedback—especially on differential execution testing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/mao2009/PSXRecompStudio" rel="noopener noreferrer"&gt;https://github.com/mao2009/PSXRecompStudio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The project does not distribute commercial game images, BIOS files, or copyrighted game data. Persona screenshots document interoperability research; the game and its imagery belong to their respective rights holders. PSXRecompStudio is unaffiliated with ATLUS.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>computerscience</category>
      <category>gamedev</category>
      <category>opensource</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Implementing 'You Can't' for AI Coding Agents</title>
      <dc:creator>loach</dc:creator>
      <pubDate>Thu, 08 Oct 2026 03:28:21 +0000</pubDate>
      <link>https://dev.to/loach2009/implementing-you-cant-for-ai-coding-agents-2d1a</link>
      <guid>https://dev.to/loach2009/implementing-you-cant-for-ai-coding-agents-2d1a</guid>
      <description>&lt;p&gt;AI coding agents are getting better at writing software. I use them in my own open-source projects, too.&lt;/p&gt;

&lt;p&gt;But there's a distinction I keep coming back to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teaching an AI what it shouldn't do is not the same as enforcing what its code is allowed to do.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We ask agents not to break architectural boundaries, introduce side effects, or add unapproved dependencies. Those instructions are useful, but instructions are not guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompts aren't enforcement
&lt;/h2&gt;

&lt;p&gt;Imagine an agent generates this code inside a Domain layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApp.Domain&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Infrastructure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the referenced type exists, the C# compiler will not normally know that the dependency violates your architecture.&lt;/p&gt;

&lt;p&gt;The code may work, compile, and still be unacceptable.&lt;/p&gt;

&lt;p&gt;A README or &lt;code&gt;AGENTS.md&lt;/code&gt; communicates a rule. It does not independently enforce it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Move the decision outside the agent
&lt;/h2&gt;

&lt;p&gt;The workflow I want looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI agent or human writes code
             |
             v
      Compiler + Analyzers
             |
      Policy satisfied?
         /       \
       Yes        No
        |          |
   Candidate     Diagnostic
   for merge       |
                   v
                Revise code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent can propose a solution. An independent tool decides whether the proposal satisfies the project's checkable constraints.&lt;/p&gt;

&lt;p&gt;For C# projects, Roslyn Analyzers let us report diagnostics during compilation. Configured as errors, those diagnostics can make a violating build fail.&lt;/p&gt;

&lt;p&gt;This gives AI agents immediate, deterministic feedback rather than another natural-language reminder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three open-source projects exploring the approach
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ArchitectureAnalyzer: Compile the architecture contract
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/mao2009/ArchitectureAnalyzer" rel="noopener noreferrer"&gt;ArchitectureAnalyzer&lt;/a&gt; uses a JSON architecture contract and Roslyn diagnostics to check rules such as layer dependencies, forbidden APIs, and dependency graph constraints.&lt;/p&gt;

&lt;p&gt;An architecture document records intent. A machine-checkable contract helps detect drift between that intent and the codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  PureSharp: Enforce specific code-level contracts
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/mao2009/PureSharp" rel="noopener noreferrer"&gt;PureSharp&lt;/a&gt; introduces functional-programming-oriented checks for C#.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PureMethod&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It detects selected side effects within its documented purity contract. It also checks conventions for immutable local variables and FluentIf termination.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;_value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Calculate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// _value = 100; // Diagnosed reassignment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a whole-program mathematical proof of purity. It's enforcement of explicitly defined, mechanically checkable properties.&lt;/p&gt;

&lt;h3&gt;
  
  
  PolicySharp: Allow by policy, deny by default
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/mao2009/PolicySharp" rel="noopener noreferrer"&gt;PolicySharp&lt;/a&gt; takes an allowlist-oriented approach.&lt;/p&gt;

&lt;p&gt;A denylist catches violations someone remembered to enumerate. A new API or dependency may not appear on that list.&lt;/p&gt;

&lt;p&gt;A default-deny policy instead follows this model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Explicitly allowed    -&amp;gt; ALLOW
Explicitly denied     -&amp;gt; DENY
Unknown or ambiguous  -&amp;gt; DENY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, a policy can describe which namespaces a Domain scope is allowed to depend on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default-deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scopes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyApp.Domain.**"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"namespaces"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"System"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"MyApp.Domain.**"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This illustrates the policy model; precise enforcement coverage depends on the implemented version.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if the agent changes the rules?
&lt;/h2&gt;

&lt;p&gt;There's an obvious bypass:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent introduces a prohibited dependency.&lt;/li&gt;
&lt;li&gt;An analyzer reports an error.&lt;/li&gt;
&lt;li&gt;The agent edits the policy or project configuration.&lt;/li&gt;
&lt;li&gt;The build passes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's why an analyzer alone isn't enough.&lt;/p&gt;

&lt;p&gt;The authority to modify application code should be separate from the authority to weaken the constraints. Policy files, project files, and analyzer references may need independent approvals and CI checks.&lt;/p&gt;

&lt;p&gt;Static analysis also has limits. It cannot enforce properties it does not analyze, and a build gate only matters when the project actually runs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freedom to propose is not permission to merge
&lt;/h2&gt;

&lt;p&gt;I don't want to constrain every implementation choice an AI makes.&lt;/p&gt;

&lt;p&gt;I want to distinguish &lt;strong&gt;freedom to propose a solution&lt;/strong&gt; from &lt;strong&gt;permission to accept the resulting change&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Within the approved boundaries, the agent can explore different approaches. If a boundary needs to change, that should become an explicit design decision instead of a workaround silently introduced during implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Prompts communicate intent.&lt;/p&gt;

&lt;p&gt;Analyzers enforce checkable rules.&lt;/p&gt;

&lt;p&gt;CI validates changes.&lt;/p&gt;

&lt;p&gt;Approval processes protect the rules themselves.&lt;/p&gt;

&lt;p&gt;Instead of expecting AI to remember everything it must never do, we can create development environments that reliably detect prohibited changes before accepting them.&lt;/p&gt;

&lt;p&gt;That is the direction I'm exploring with ArchitectureAnalyzer, PureSharp, and PolicySharp.&lt;/p&gt;

&lt;p&gt;The goal isn't less capable AI. It's more predictable, maintainable AI-assisted software development.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>csharp</category>
      <category>programming</category>
    </item>
    <item>
      <title>Publishing .NET Packages to NuGet.org with an API Key</title>
      <dc:creator>loach</dc:creator>
      <pubDate>Thu, 10 Sep 2026 12:00:55 +0000</pubDate>
      <link>https://dev.to/loach2009/publishing-net-packages-to-nugetorg-with-an-api-key-37n5</link>
      <guid>https://dev.to/loach2009/publishing-net-packages-to-nugetorg-with-an-api-key-37n5</guid>
      <description>&lt;p&gt;Publishing a .NET library manually is fine once or twice, but it becomes tedious when releases are frequent or automated through CI/CD.&lt;/p&gt;

&lt;p&gt;The standard solution is to push packages to NuGet.org with &lt;code&gt;dotnet nuget push&lt;/code&gt; and an API key.&lt;/p&gt;

&lt;p&gt;This article covers the basic flow and the most important security consideration: &lt;strong&gt;never commit the API key into the repository&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a .NET project&lt;/li&gt;
&lt;li&gt;a generated &lt;code&gt;.nupkg&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;a NuGet.org account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, your package might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./bin/Release/YourPackage.1.0.0.nupkg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create a NuGet API key
&lt;/h2&gt;

&lt;p&gt;Sign in to NuGet.org and open the API Keys page.&lt;/p&gt;

&lt;p&gt;When creating a new key, configure at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a recognizable key name&lt;/li&gt;
&lt;li&gt;an expiration period&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;Push&lt;/code&gt; scope&lt;/li&gt;
&lt;li&gt;an optional package pattern if you want to restrict which packages the key can publish&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The generated key should be treated like a password.&lt;/p&gt;

&lt;p&gt;Do not put it in source code, scripts committed to Git, or Markdown examples containing a real value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Push a package
&lt;/h2&gt;

&lt;p&gt;The basic command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet nuget push &amp;lt;PACKAGE_FILE&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api-key&lt;/span&gt; &amp;lt;YOUR_API_KEY&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; https://api.nuget.org/v3/index.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet nuget push ./bin/Release/YourPackage.1.0.0.nupkg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api-key&lt;/span&gt; YOUR_API_KEY_HERE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; https://api.nuget.org/v3/index.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A useful option for CI is &lt;code&gt;--skip-duplicate&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet nuget push ./bin/Release/&lt;span class="k"&gt;*&lt;/span&gt;.nupkg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api-key&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NUGET_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; https://api.nuget.org/v3/index.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--skip-duplicate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids failing a rerun just because the same package version was already published.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store the key in GitHub Actions Secrets
&lt;/h2&gt;

&lt;p&gt;For GitHub Actions, store the key as a repository secret, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NUGET_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reference it from the workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish package&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;NUGET_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NUGET_API_KEY }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;dotnet nuget push ./bin/Release/*.nupkg \&lt;/span&gt;
      &lt;span class="s"&gt;--api-key "$NUGET_API_KEY" \&lt;/span&gt;
      &lt;span class="s"&gt;--source https://api.nuget.org/v3/index.json \&lt;/span&gt;
      &lt;span class="s"&gt;--skip-duplicate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API key stays outside the repository while still being available to the CI job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer narrow credentials
&lt;/h2&gt;

&lt;p&gt;If the publishing key only needs to release one package family, restrict it to that package pattern where possible.&lt;/p&gt;

&lt;p&gt;A narrow key reduces the impact if it is ever exposed.&lt;/p&gt;

&lt;p&gt;The same principle applies to other CI credentials: give automation only the permissions it actually needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;The mechanics of publishing a NuGet package are simple. The part worth designing carefully is credential handling.&lt;/p&gt;

&lt;p&gt;A solid release flow usually looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;build
  ↓
pack
  ↓
secret injected by CI
  ↓
dotnet nuget push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping the credential out of the repository makes the process both automatable and reasonably safe.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>dotnet</category>
      <category>security</category>
    </item>
    <item>
      <title>PureSharp: Bringing Functional Safety to C# with Roslyn Analyzers</title>
      <dc:creator>loach</dc:creator>
      <pubDate>Thu, 10 Sep 2026 11:55:44 +0000</pubDate>
      <link>https://dev.to/loach2009/puresharp-bringing-functional-safety-to-c-with-roslyn-analyzers-4293</link>
      <guid>https://dev.to/loach2009/puresharp-bringing-functional-safety-to-c-with-roslyn-analyzers-4293</guid>
      <description>&lt;p&gt;C# is a very flexible language, but that flexibility also makes it easy for side effects and mutable state to spread through a codebase.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;PureSharp&lt;/strong&gt; to bring a few functional-programming ideas into everyday C# development and make them enforceable at compile time.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/mao2009/PureSharp" rel="noopener noreferrer"&gt;https://github.com/mao2009/PureSharp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NuGet: &lt;a href="https://www.nuget.org/packages/loach.PureSharp" rel="noopener noreferrer"&gt;https://www.nuget.org/packages/loach.PureSharp&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What PureSharp tries to enforce
&lt;/h2&gt;

&lt;p&gt;PureSharp focuses on three ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Purity&lt;/strong&gt;: methods explicitly marked as pure should not perform side effects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Immutability&lt;/strong&gt;: local variables can opt into a "do not reassign" convention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safer control flow&lt;/strong&gt;: fluent conditional expressions should terminate explicitly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key point is that these rules are not just documentation. Roslyn analyzers report violations while you write and build the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Declaring a pure method
&lt;/h2&gt;

&lt;p&gt;A method can be marked with &lt;code&gt;[PureMethod]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;PureSharp.Core&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Calculator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PureMethod&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside such a method, PureSharp can report operations that violate the declared purity contract, such as accessing mutable static state, calling methods that are not known to be pure, or performing I/O.&lt;/p&gt;

&lt;p&gt;The goal is not to claim that every C# method can be proven mathematically pure. The goal is more practical: if a developer explicitly says "this method should be pure," the analyzer should help keep it that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Opt-in immutability for local variables
&lt;/h2&gt;

&lt;p&gt;C# does not have a general-purpose &lt;code&gt;const&lt;/code&gt;-style feature for local variables initialized at runtime.&lt;/p&gt;

&lt;p&gt;PureSharp uses a naming convention: local variables beginning with &lt;code&gt;_&lt;/code&gt; are treated as non-reassignable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ProcessData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CalculateValue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Reported by the analyzer:&lt;/span&gt;
    &lt;span class="c1"&gt;// _result = 100;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turns a simple convention into something the compiler can enforce.&lt;/p&gt;

&lt;p&gt;The analyzer can also require immediate initialization and can suggest the immutable convention for locals that are never reassigned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fluent conditional expressions
&lt;/h2&gt;

&lt;p&gt;PureSharp also provides &lt;code&gt;Fluent.If&lt;/code&gt;, which makes it possible to use conditional branching as an expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Fluent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;If&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ElseIf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Else&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The analyzer checks that a fluent chain is properly terminated with &lt;code&gt;.Else()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The motivation is simple: if the construct is intended to produce a value, all paths should be explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Roslyn analyzers?
&lt;/h2&gt;

&lt;p&gt;These rules could be described in a style guide, but a style guide is only useful when someone remembers to enforce it.&lt;/p&gt;

&lt;p&gt;Roslyn analyzers make the feedback immediate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;violations appear in the IDE&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dotnet build&lt;/code&gt; can fail on violations&lt;/li&gt;
&lt;li&gt;CI does not need a separate custom validator&lt;/li&gt;
&lt;li&gt;rules stay close to the normal C# development workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes them especially useful for conventions that are easy to understand but tedious to police in code review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Install the package from NuGet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package loach.PureSharp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the analyzer participates in normal builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned from building it
&lt;/h2&gt;

&lt;p&gt;The interesting part of a tool like PureSharp is not the syntax of a single diagnostic. It is deciding where static analysis can give useful guarantees without pretending to understand more than it actually can.&lt;/p&gt;

&lt;p&gt;C# remains an imperative, object-oriented language with many escape hatches. PureSharp is therefore intentionally opt-in: developers declare the rules they want, and the analyzer checks those declarations as far as static analysis reasonably allows.&lt;/p&gt;

&lt;p&gt;That balance is what makes Roslyn analyzers attractive for this kind of developer tooling.&lt;/p&gt;

&lt;p&gt;PureSharp is open source here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mao2009/PureSharp" rel="noopener noreferrer"&gt;https://github.com/mao2009/PureSharp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>opensource</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Using Ctrl+Enter to Submit in OpenCode and Enter for New Lines</title>
      <dc:creator>loach</dc:creator>
      <pubDate>Thu, 10 Sep 2026 11:55:33 +0000</pubDate>
      <link>https://dev.to/loach2009/using-ctrlenter-to-submit-in-opencode-and-enter-for-new-lines-2065</link>
      <guid>https://dev.to/loach2009/using-ctrlenter-to-submit-in-opencode-and-enter-for-new-lines-2065</guid>
      <description>&lt;p&gt;Another OpenCode behavior I wanted to change was message submission.&lt;/p&gt;

&lt;p&gt;By default, pressing &lt;code&gt;Enter&lt;/code&gt; submits the current prompt. If you are used to chat tools where &lt;code&gt;Enter&lt;/code&gt; inserts a new line and &lt;code&gt;Ctrl + Enter&lt;/code&gt; sends, this makes accidental submissions easy.&lt;/p&gt;

&lt;p&gt;My preferred setup is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Enter&lt;/code&gt;: insert a new line&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Ctrl + Enter&lt;/code&gt;: submit the prompt&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  OpenCode configuration
&lt;/h2&gt;

&lt;p&gt;Edit your OpenCode configuration file and update the keybindings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keybinds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"app_exit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ctrl+d,&amp;lt;leader&amp;gt;q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input_submit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ctrl+return"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input_newline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shift+return,return,alt+return,ctrl+j"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells OpenCode to treat normal Enter-style keys as new lines and reserve &lt;code&gt;Ctrl + Enter&lt;/code&gt; for submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  The terminal may need configuration too
&lt;/h2&gt;

&lt;p&gt;There is one important detail: some terminals do not send a distinct key sequence for &lt;code&gt;Ctrl + Enter&lt;/code&gt; by default.&lt;/p&gt;

&lt;p&gt;In that case, OpenCode cannot distinguish it from plain Enter.&lt;/p&gt;

&lt;p&gt;For Windows Terminal, you can define a custom sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"actions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sendInput"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;001b[13;5u"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User.sendInput.CtrlEnterCustom"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then bind &lt;code&gt;Ctrl + Enter&lt;/code&gt; to that action in the terminal settings.&lt;/p&gt;

&lt;p&gt;The exact terminal configuration can vary, but the principle is the same: the terminal must send a distinguishable sequence before OpenCode can bind it separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this improves the workflow
&lt;/h2&gt;

&lt;p&gt;Long prompts often contain code blocks, lists, and multiple paragraphs.&lt;/p&gt;

&lt;p&gt;Making &lt;code&gt;Enter&lt;/code&gt; safe for editing removes the pressure to finish the whole prompt on one line and reduces accidental sends.&lt;/p&gt;

&lt;p&gt;It also makes OpenCode behave more like many chat and editor-oriented interfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Keybindings are a small detail, but AI coding tools are used interactively for long periods. Small interaction problems become surprisingly expensive when repeated all day.&lt;/p&gt;

&lt;p&gt;For me, separating newline and submit made OpenCode much more comfortable to use.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>software</category>
      <category>tools</category>
    </item>
    <item>
      <title>Preventing Accidental OpenCode Exits with Ctrl+C</title>
      <dc:creator>loach</dc:creator>
      <pubDate>Thu, 10 Sep 2026 11:50:23 +0000</pubDate>
      <link>https://dev.to/loach2009/preventing-accidental-opencode-exits-with-ctrlc-39dh</link>
      <guid>https://dev.to/loach2009/preventing-accidental-opencode-exits-with-ctrlc-39dh</guid>
      <description>&lt;p&gt;OpenCode is powerful, but one default behavior kept getting in my way: pressing &lt;code&gt;Ctrl + C&lt;/code&gt; could close the application itself.&lt;/p&gt;

&lt;p&gt;If you spend a lot of time in terminals, &lt;code&gt;Ctrl + C&lt;/code&gt; is muscle memory. You press it to interrupt a command, stop a long-running process, or simply because your hands move before you think.&lt;/p&gt;

&lt;p&gt;Losing the whole OpenCode session because of that is frustrating.&lt;/p&gt;

&lt;p&gt;The fix is simple: move the application exit action to a different keybinding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change the exit keybinding
&lt;/h2&gt;

&lt;p&gt;Open your OpenCode configuration file and customize the keybindings.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keybinds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"app_exit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ctrl+d,&amp;lt;leader&amp;gt;q"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this configuration, &lt;code&gt;Ctrl + C&lt;/code&gt; is no longer the application-exit shortcut.&lt;/p&gt;

&lt;p&gt;You can then use &lt;code&gt;Ctrl + D&lt;/code&gt; or the leader-based shortcut to exit intentionally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this small change matters
&lt;/h2&gt;

&lt;p&gt;AI coding agents often stay open for long sessions with valuable context.&lt;/p&gt;

&lt;p&gt;A keybinding that is common for interruption in terminal workflows should not unexpectedly destroy that session.&lt;/p&gt;

&lt;p&gt;Separating "interrupt what is happening" from "close the entire application" makes the interface much harder to use incorrectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;This is a tiny configuration change, but it removed one of the most annoying sources of accidental session loss for me.&lt;/p&gt;

&lt;p&gt;If you use OpenCode inside a terminal and frequently hit &lt;code&gt;Ctrl + C&lt;/code&gt; out of habit, remapping &lt;code&gt;app_exit&lt;/code&gt; is worth doing early.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>software</category>
      <category>tools</category>
    </item>
    <item>
      <title>Setting Up Japanese Input on Fedora KDE with Fcitx5 and Mozc</title>
      <dc:creator>loach</dc:creator>
      <pubDate>Thu, 10 Sep 2026 11:50:12 +0000</pubDate>
      <link>https://dev.to/loach2009/setting-up-japanese-input-on-fedora-kde-with-fcitx5-and-mozc-4gi3</link>
      <guid>https://dev.to/loach2009/setting-up-japanese-input-on-fedora-kde-with-fcitx5-and-mozc-4gi3</guid>
      <description>&lt;p&gt;After moving to Fedora KDE, one of the first things I wanted to fix was Japanese input.&lt;/p&gt;

&lt;p&gt;My goal was not only to install Mozc, but also to make the conversion keys feel closer to what I was used to on macOS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Henkan&lt;/code&gt; enables the IME&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Muhenkan&lt;/code&gt; disables the IME&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article documents the setup I used with &lt;strong&gt;Fcitx5 + Mozc&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install Fcitx5 and Mozc
&lt;/h2&gt;

&lt;p&gt;On Fedora, install the required packages with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; fcitx5-mozc fcitx5-configtool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Enable Fcitx5 in KDE
&lt;/h2&gt;

&lt;p&gt;Open KDE System Settings and go to the keyboard / virtual keyboard settings.&lt;/p&gt;

&lt;p&gt;Select Fcitx 5, or the Fcitx 5 Wayland launcher depending on your environment, and apply the change.&lt;/p&gt;

&lt;p&gt;After logging out and back in, Fcitx5 should start as your input method framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add Mozc
&lt;/h2&gt;

&lt;p&gt;Open the Fcitx5 configuration tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fcitx5-configtool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add Mozc to the list of available input methods.&lt;/p&gt;

&lt;p&gt;If the keyboard layout is detected incorrectly, verify that the underlying keyboard layout matches your physical keyboard before adjusting IME shortcuts. Otherwise, a shortcut problem can actually be a layout problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mac-like conversion key behavior
&lt;/h2&gt;

&lt;p&gt;For Japanese keyboards, the &lt;code&gt;Henkan&lt;/code&gt; and &lt;code&gt;Muhenkan&lt;/code&gt; keys are convenient dedicated switches.&lt;/p&gt;

&lt;p&gt;I configured them so that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Henkan&lt;/code&gt; activates Japanese input&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Muhenkan&lt;/code&gt; returns to direct input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This avoids using a single toggle key and makes the current IME state more predictable.&lt;/p&gt;

&lt;p&gt;That small change made Fedora much more comfortable for daily development because I no longer had to wonder whether the next keystroke would be interpreted as Japanese or ASCII.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I prefer explicit on/off keys
&lt;/h2&gt;

&lt;p&gt;Toggle shortcuts are compact, but they depend on remembering the current state.&lt;/p&gt;

&lt;p&gt;Separate on/off shortcuts are more deterministic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Henkan   -&amp;gt; IME ON
Muhenkan -&amp;gt; IME OFF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For programming, this is especially useful because switching from Japanese prose back to symbols, identifiers, and shell commands happens constantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Fcitx5 + Mozc works well on Fedora KDE once the desktop integration and keyboard shortcuts are configured.&lt;/p&gt;

&lt;p&gt;The installation itself is simple; the quality-of-life improvement comes from tuning the input method to your keyboard habits.&lt;/p&gt;

&lt;p&gt;If you are moving from macOS or another environment where dedicated IME on/off keys are part of your muscle memory, mapping &lt;code&gt;Henkan&lt;/code&gt; and &lt;code&gt;Muhenkan&lt;/code&gt; explicitly is worth trying.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>tools</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Keeping Translated OSS Documentation Fresh with an SSOT and CI</title>
      <dc:creator>loach</dc:creator>
      <pubDate>Thu, 10 Sep 2026 11:11:01 +0000</pubDate>
      <link>https://dev.to/loach2009/keeping-translated-oss-documentation-fresh-with-an-ssot-and-ci-3h61</link>
      <guid>https://dev.to/loach2009/keeping-translated-oss-documentation-fresh-with-an-ssot-and-ci-3h61</guid>
      <description>&lt;p&gt;Publishing an OSS project in multiple languages is useful, but translated documentation introduces a maintenance problem very quickly.&lt;/p&gt;

&lt;p&gt;Typical failure modes are familiar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the English README changes but the Japanese version does not&lt;/li&gt;
&lt;li&gt;nobody is sure which version is authoritative&lt;/li&gt;
&lt;li&gt;a translation gains content that never reaches the canonical document&lt;/li&gt;
&lt;li&gt;developers or AI tools read an outdated translation as if it were current design documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ran into this while expanding the English documentation for &lt;strong&gt;PSXRecompStudio&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/mao2009/PSXRecompStudio" rel="noopener noreferrer"&gt;https://github.com/mao2009/PSXRecompStudio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My solution was to make the English documents the explicit &lt;strong&gt;canonical source / SSOT&lt;/strong&gt; and add CI checks that detect stale translations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why translations need a source of truth
&lt;/h2&gt;

&lt;p&gt;Suppose a repository has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;README.md
README.ja.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both files are treated as equally authoritative, every edit creates a coordination problem.&lt;/p&gt;

&lt;p&gt;Instead, I use a simpler rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;English is canonical. Translations are derived documents.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A translated document is still valuable for readers, but architectural and specification decisions ultimately refer back to the canonical file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store the relationship as data
&lt;/h2&gt;

&lt;p&gt;The canonical-to-translation mapping is kept in JSON rather than hard-coded into the CI script.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pairs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"canonical"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"README.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"translation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"README.ja.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ja"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"canonicalMarker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[English README (Canonical / SSOT)](README.md)"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This describes which file is authoritative, which file is its translation, the language, and which canonical marker should appear in the translation.&lt;/p&gt;

&lt;p&gt;As the documentation set grows, adding a new pair is a data change rather than a code change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the canonical relationship visible to humans too
&lt;/h2&gt;

&lt;p&gt;The translated document should clearly say that it is a translation and link back to the canonical source.&lt;/p&gt;

&lt;p&gt;That helps in two ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;readers know which file wins when there is disagreement&lt;/li&gt;
&lt;li&gt;CI can verify that the relationship is still declared&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This avoids a common documentation smell: two almost-identical files with no explicit indication of which one is authoritative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detect stale translations in CI
&lt;/h2&gt;

&lt;p&gt;The next step is to make freshness machine-checkable.&lt;/p&gt;

&lt;p&gt;A practical approach is to record which canonical revision a translation was last synchronized against. CI can then compare that information with the current canonical file.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;canonical changed
      ↓
translation metadata still points to old revision
      ↓
CI reports stale translation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact implementation can vary. The important part is that freshness becomes a build-time property instead of something maintainers have to remember manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for AI-assisted development
&lt;/h2&gt;

&lt;p&gt;Outdated translations are especially dangerous when repositories are consumed by AI coding agents.&lt;/p&gt;

&lt;p&gt;A human may notice that a translated document looks old. An agent may simply treat the first relevant file it finds as the current specification.&lt;/p&gt;

&lt;p&gt;By declaring one SSOT and checking synchronization automatically, the repository gives both humans and tools a clearer hierarchy of trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat documentation like code
&lt;/h2&gt;

&lt;p&gt;The broader lesson is that documentation maintenance benefits from the same ideas we already use for source code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;define ownership&lt;/li&gt;
&lt;li&gt;define the source of truth&lt;/li&gt;
&lt;li&gt;represent relationships in machine-readable form&lt;/li&gt;
&lt;li&gt;validate invariants in CI&lt;/li&gt;
&lt;li&gt;fail visibly when derived artifacts become stale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Translations are derived artifacts, even when humans maintain them manually.&lt;/p&gt;

&lt;p&gt;Once I started treating them that way, the maintenance model became much clearer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Multilingual OSS documentation is valuable, but duplicating information without an explicit authority model creates ambiguity.&lt;/p&gt;

&lt;p&gt;Making one language canonical and validating translation freshness in CI gives the repository a predictable rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;translations may lag temporarily, but they cannot silently pretend to be current.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That small distinction makes documentation easier to maintain and safer to consume.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Enforcing C# Architecture Rules at Compile Time with a Roslyn Analyzer</title>
      <dc:creator>loach</dc:creator>
      <pubDate>Thu, 10 Sep 2026 11:01:55 +0000</pubDate>
      <link>https://dev.to/loach2009/enforcing-c-architecture-rules-at-compile-time-with-a-roslyn-analyzer-4fbj</link>
      <guid>https://dev.to/loach2009/enforcing-c-architecture-rules-at-compile-time-with-a-roslyn-analyzer-4fbj</guid>
      <description>&lt;p&gt;When a C# project adopts layered architecture or Clean Architecture, we often define rules such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Domain layer must not depend on the Application layer&lt;/li&gt;
&lt;li&gt;the Domain layer must not call I/O APIs such as &lt;code&gt;Console&lt;/code&gt; or &lt;code&gt;File&lt;/code&gt; directly&lt;/li&gt;
&lt;li&gt;interop entry points must stay inside a specific boundary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem is that rules written only in README files, ADRs, or design documents are still easy to violate.&lt;/p&gt;

&lt;p&gt;A review can miss a dependency. A temporary shortcut can become permanent. Over time, the implementation slowly drifts away from the architecture described in the documentation.&lt;/p&gt;

&lt;p&gt;To make these rules executable, I built &lt;strong&gt;ArchitectureAnalyzer&lt;/strong&gt;, a Roslyn Analyzer that validates architecture rules during normal C# compilation.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/mao2009/ArchitectureAnalyzer" rel="noopener noreferrer"&gt;https://github.com/mao2009/ArchitectureAnalyzer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Project site: &lt;a href="https://mao2009.github.io/ArchitectureAnalyzer/" rel="noopener noreferrer"&gt;https://mao2009.github.io/ArchitectureAnalyzer/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the architecture as data
&lt;/h2&gt;

&lt;p&gt;ArchitectureAnalyzer keeps project-specific rules outside the analyzer itself. They live in an &lt;code&gt;architecture.contract.json&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"layers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Domain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"namespaceRoots"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"MyApp.Domain"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Application"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"namespaceRoots"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"MyApp.Application"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"forbiddenDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Domain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Application"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Domain must not depend on the outer Application layer."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this contract, any type below &lt;code&gt;MyApp.Domain&lt;/code&gt; is classified as Domain and any type below &lt;code&gt;MyApp.Application&lt;/code&gt; as Application.&lt;/p&gt;

&lt;p&gt;If Domain references Application, the build fails with a Roslyn diagnostic such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error AARC002: 'MyApp.Domain.Order' (Domain) must not depend on
'MyApp.Application.OrderService' (Application):
Domain must not depend on the outer Application layer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means the compiler catches the violation before a human reviewer has to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forbidden APIs
&lt;/h2&gt;

&lt;p&gt;Architecture rules are not limited to layer-to-layer dependencies.&lt;/p&gt;

&lt;p&gt;You can also block specific APIs from a layer. For example, to keep console I/O out of Domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"forbiddenApis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"layer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Domain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"System.Console"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Console I/O must be abstracted behind an Infrastructure adapter."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then code like this can be rejected at compile time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApp.Domain&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why JSON instead of hard-coded rules?
&lt;/h2&gt;

&lt;p&gt;Different projects use different architectures. Hard-coding &lt;code&gt;Domain&lt;/code&gt;, &lt;code&gt;Application&lt;/code&gt;, and &lt;code&gt;Infrastructure&lt;/code&gt; into the analyzer would make it project-specific.&lt;/p&gt;

&lt;p&gt;Instead, ArchitectureAnalyzer follows a simple split:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The analyzer implements the mechanism. The project defines the policy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That also means architecture rules can be reviewed and versioned in Git like source code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attribute-based layer declarations
&lt;/h2&gt;

&lt;p&gt;Version 0.1.0 also supports explicit layer markers using attributes.&lt;/p&gt;

&lt;p&gt;A contract can map attributes to layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"layerDeclaration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"markerAttributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"attributeFqn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyApp.Architecture.DomainAttribute"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"layer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Domain"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"attributeFqn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyApp.Architecture.ApplicationAttribute"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"layer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Application"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"validateNamespaceConsistency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"markerNamespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyApp.Architecture"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a type can declare its architectural role directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;MyApp.Architecture&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApp.Domain&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Domain&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When namespace and attribute declarations disagree, the analyzer can report the mismatch.&lt;/p&gt;

&lt;p&gt;This is useful when you want architecture to be visible both from project structure and directly in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Roslyn Analyzer?
&lt;/h2&gt;

&lt;p&gt;The same validation could be implemented as a custom CLI or CI script, but a Roslyn Analyzer has an important advantage: it becomes part of the normal development loop.&lt;/p&gt;

&lt;p&gt;You get diagnostics in the IDE, during &lt;code&gt;dotnet build&lt;/code&gt;, and in CI without teaching developers a separate command.&lt;/p&gt;

&lt;p&gt;Install the package and provide the contract as an additional file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"loach.ArchitectureAnalyzer"&lt;/span&gt;
                    &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
                    &lt;span class="na"&gt;PrivateAssets=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;AdditionalFiles&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"architecture.contract.json"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the normal build is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same command naturally becomes a CI architecture gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;p&gt;ArchitectureAnalyzer does not decide whether your architecture is good.&lt;/p&gt;

&lt;p&gt;It only answers a narrower question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does the implementation still obey the architecture rules this project explicitly defined?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reflection, &lt;code&gt;dynamic&lt;/code&gt;, generated code that is excluded from analysis, and some indirect access patterns are outside the guarantees of a normal static analyzer.&lt;/p&gt;

&lt;p&gt;That limitation is intentional. The tool is designed to prevent architecture drift, not to replace architectural design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Architecture documentation is valuable, but documentation alone cannot stop code from drifting away from it.&lt;/p&gt;

&lt;p&gt;By moving the rules into a machine-readable contract and validating them during compilation, architecture becomes something the build can enforce rather than something reviewers merely have to remember.&lt;/p&gt;

&lt;p&gt;ArchitectureAnalyzer is open source, and the project is available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mao2009/ArchitectureAnalyzer" rel="noopener noreferrer"&gt;https://github.com/mao2009/ArchitectureAnalyzer&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>csharp</category>
      <category>dotnet</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
