<?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: kanzaki-chiya</title>
    <description>The latest articles on DEV Community by kanzaki-chiya (@kanzakichiya).</description>
    <link>https://dev.to/kanzakichiya</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%2F4241051%2F5dc81cb4-30b1-4043-8604-58512d3f6159.jpg</url>
      <title>DEV Community: kanzaki-chiya</title>
      <link>https://dev.to/kanzakichiya</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kanzakichiya"/>
    <language>en</language>
    <item>
      <title>NecroHex: semantic patches for 1990s WAD map files, built with Kiro</title>
      <dc:creator>kanzaki-chiya</dc:creator>
      <pubDate>Thu, 12 Nov 2026 12:28:00 +0000</pubDate>
      <link>https://dev.to/kanzakichiya/necrohex-semantic-patches-for-1990s-wad-map-files-built-with-kiro-5glh</link>
      <guid>https://dev.to/kanzakichiya/necrohex-semantic-patches-for-1990s-wad-map-files-built-with-kiro-5glh</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is written for the Kiroween 2026 hackathon (Resurrection category).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;NecroHex is a patch workbench for Doom-format WAD map files. It runs in the browser at &lt;strong&gt;&lt;a href="https://necrohex.koxia.cn" rel="noopener noreferrer"&gt;https://necrohex.koxia.cn&lt;/a&gt;&lt;/strong&gt;, and the code is at &lt;strong&gt;&lt;a href="https://github.com/kanzaki-chiya/NecroHex" rel="noopener noreferrer"&gt;https://github.com/kanzaki-chiya/NecroHex&lt;/a&gt;&lt;/strong&gt; under the MIT license. You open a WAD and load a patch. The workbench shows what would change on the map and why anything was refused, and lets you export a small PWAD with a receipt. Everything is tested against &lt;a href="https://freedoom.github.io/" rel="noopener noreferrer"&gt;Freedoom&lt;/a&gt;, the free content set, so every screenshot here is Freedoom.&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%2Fi4d4ndb47zpghcdbjbft.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%2Fi4d4ndb47zpghcdbjbft.png" alt="The workbench with the Haunted House patch applied to Freedoom MAP01" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The core of it was built in about three days, starting with the first commit on 2026-11-09.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: editing a file format from 1993
&lt;/h2&gt;

&lt;p&gt;A WAD map is a set of fixed-size binary records: sectors, things, linedefs and sidedefs. Old maps are still edited and remixed today, usually in a map editor or by rewriting bytes at known offsets. What I wanted was a change like "make every light in this map 30% darker, but don't touch the player starts" written down as a file that you can review before it's applied, replay on the next release of the map, check with a receipt, and revert.&lt;/p&gt;

&lt;p&gt;Without that, editing is risky, and it gets worse when an AI agent does the editing. An agent that can write raw bytes can corrupt a map in ways nobody notices until the map is played.&lt;/p&gt;

&lt;p&gt;NecroHex takes a different approach. A patch is a JSON list of operations. Each operation selects records (all of them, ones matching a &lt;code&gt;where&lt;/code&gt; filter, or one record pinned by index plus a fingerprint) and changes fields with &lt;code&gt;set&lt;/code&gt;, &lt;code&gt;scale&lt;/code&gt; or &lt;code&gt;clamp&lt;/code&gt;. Before anything is written:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails&lt;/strong&gt; refuse risky writes unless the patch opts in. Examples are replacing player starts, keys or bosses, overwriting a nonzero sector special, or changing a thing into a different category. Some changes only produce warnings, such as a monster that gets bigger or a height change on a trigger-linked sector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value checks&lt;/strong&gt; reject flat names that don't exist, unknown thing types and ceilings below floors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A receipt&lt;/strong&gt; lists every byte span that changes, with old and new values, so a patch can be verified and reverted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI agents get the same patch layer through an MCP server with five tools: &lt;code&gt;inspect_wad&lt;/code&gt;, &lt;code&gt;query_entities&lt;/code&gt;, &lt;code&gt;plan_patch&lt;/code&gt;, &lt;code&gt;apply_patch&lt;/code&gt; and &lt;code&gt;verify_output&lt;/code&gt;. There is no tool that writes raw bytes, and &lt;code&gt;apply_patch&lt;/code&gt; only accepts a &lt;code&gt;planId&lt;/code&gt; from a matching &lt;code&gt;plan_patch&lt;/code&gt; call.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I used Kiro
&lt;/h2&gt;

&lt;p&gt;The initial steering files and design documents were prepared with Claude Code before development started, from my original design; the repository says so in &lt;code&gt;docs/kiro-usage.md&lt;/code&gt;. The workbench's look comes from an HTML mockup prepared the same way. The code in the repository was written in Kiro. I used the Kiro IDE throughout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Specs, including one written after the code
&lt;/h3&gt;

&lt;p&gt;I used three specs: &lt;code&gt;wad-roundtrip&lt;/code&gt;, &lt;code&gt;patch-engine&lt;/code&gt; and &lt;code&gt;workbench&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wad-roundtrip&lt;/code&gt; was written in the wrong order. The code and tests already existed, and the requirements, design and tasks came afterwards. That turned out to be useful. Writing down what the code was supposed to guarantee, and checking it against the code, surfaced five real defects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the parser didn't copy its input, so a caller could change the buffer underneath it;&lt;/li&gt;
&lt;li&gt;a field write could land in the WAD header;&lt;/li&gt;
&lt;li&gt;one field name returned a &lt;code&gt;NaN&lt;/code&gt; byte span;&lt;/li&gt;
&lt;li&gt;a bare &lt;code&gt;SECTORS&lt;/code&gt; lump with no map marker could be written to;&lt;/li&gt;
&lt;li&gt;Hexen-format &lt;code&gt;THINGS&lt;/code&gt; were read with the Doom layout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one was fixed with a regression test.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;patch-engine&lt;/code&gt; went the normal way: requirements, design and tasks first, then Kiro worked through &lt;code&gt;tasks.md&lt;/code&gt; one item at a time and ticked each one off. The design lists properties the engine must keep, and they are checked with property-based tests on generated data and on a real Freedoom fixture. Property 3 says applying a &lt;code&gt;set&lt;/code&gt; twice gives the same result as applying it once. It produced counterexamples three times. None of them was an engine bug. Each one showed that the property was stated too broadly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A later operation can select records that an earlier operation just changed, so a second run matches more records.&lt;/li&gt;
&lt;li&gt;The guardrail against overwriting a nonzero sector special blocks the second run. The value written the first time is now the old value, and it's nonzero.&lt;/li&gt;
&lt;li&gt;A patch can change fields that are part of its own pin's fingerprint. The second run then no longer recognises the record.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each time the property got an explicit precondition, the shrunk case became a regression test, and the guardrail stayed as it was.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;workbench&lt;/code&gt; was the browser app. Kiro rebuilt the prepared mockup in Vue on top of the real engine, so the browser and the CLI share one set of rules. While writing its design, Kiro checked the code and found that the WAD adapter imported &lt;code&gt;node:child_process&lt;/code&gt;, which can't run in a browser. It moved that code to the CLI first and added a test so &lt;code&gt;core&lt;/code&gt; and &lt;code&gt;adapter-wad&lt;/code&gt; can't import Node modules again. A browser check during the tasks found a stale verdict: after switching maps, the title showed the new map's name with the previous map's count (MAP02 showed 300 changes, the real number was 327).&lt;/p&gt;

&lt;h3&gt;
  
  
  Steering
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;binary-safety.md&lt;/code&gt; is always included. It states the rules the product depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;serializing an unchanged file gives the same bytes;&lt;/li&gt;
&lt;li&gt;patches only change the spans listed in the receipt;&lt;/li&gt;
&lt;li&gt;the source file is never overwritten;&lt;/li&gt;
&lt;li&gt;the tools never accept raw offsets and bytes;&lt;/li&gt;
&lt;li&gt;relocation never guesses between similar candidates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also says not to weaken these rules to make a feature or a test pass. That came up for real. When the write guardrails went in, 15 existing property tests and regression cases started failing, because their test writes now hit the new rules: a marker value landing on a player start, or a flat name the fixture didn't contain. Kiro changed the tests and left every guardrail as it was. The same happened with the second Property 3 counterexample above. I can't prove the steering file is the reason, but it's the behaviour the file asks for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hooks
&lt;/h3&gt;

&lt;p&gt;Two hooks run on save. One regenerates the typed record code when the record layout YAML changes. The other runs &lt;code&gt;pnpm -r typecheck&lt;/code&gt; when a source file changes. Kiro tested both in a fresh session: the typecheck processes started after a save, and the generated file was rewritten. They haven't caught a problem yet, and their output didn't come back to the agent, so I can't say more than that they fire.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP: Kiro patching a map through its own tools
&lt;/h3&gt;

&lt;p&gt;Once the server was registered in &lt;code&gt;.kiro/settings/mcp.json&lt;/code&gt;, Kiro showed it as connected with five tools. In a new session I asked Kiro to change one room on MAP01, using only those tools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;inspect_wad&lt;/code&gt;: Freedoom's &lt;code&gt;freedoom2.wad&lt;/code&gt; has 32 maps, and MAP01 has 211 sectors and 200 things.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;query_entities&lt;/code&gt;: Kiro found the player starts, then looked for indoor sectors with no special and no tag. It picked &lt;strong&gt;sector #12&lt;/strong&gt;, an indoor room away from the spawn.&lt;/li&gt;
&lt;li&gt;Kiro wrote a patch that pins #12 by index and fingerprint, changes the floor to &lt;code&gt;FLOOR5_2&lt;/code&gt;, and sets special 8 (a glowing light).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;plan_patch&lt;/code&gt;: ready. The pin matched exactly with score 1, two fields change, and all 15 checks passed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apply_patch&lt;/code&gt; with that &lt;code&gt;planId&lt;/code&gt;: a PWAD and a receipt.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;verify_output&lt;/code&gt;: replaying the receipt, checking that only the listed spans changed, and reverting all passed. DSDA-Doom ran MAP01 for 350 tics and exited cleanly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After each Kiro report I checked the result independently. Loading the PWAD in the engine showed the new floor in the room:&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%2Fxdjj5cxu65nxgu14arjq.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%2Fxdjj5cxu65nxgu14arjq.png" alt="Sector #12 before (top) and after (bottom): the floor changes from the grey grid to FLOOR5_2" width="799" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The glow didn't show at all. In the original engine, a glowing sector moves between its own light level and its darkest neighbour's. Sector #12 is at 160, and both its neighbours are at 255, so the glow went from 160 to 160. The patch was valid and the receipt was correct, but the effect was invisible.&lt;/p&gt;

&lt;p&gt;That became a new warning. Kiro added &lt;code&gt;sector-light-effect-invisible&lt;/code&gt;: if after the patch a sector has light special 1, 8 or 17 and no darker neighbour, the plan says so. It uses the light levels after the patch on both sides. On the Haunted House demo patch it fires once: MAP01 #39 glows at 89 against a darkest neighbour of 134.&lt;/p&gt;

&lt;h2&gt;
  
  
  Relocation: finding the same room in a newer release
&lt;/h2&gt;

&lt;p&gt;Map authors renumber sectors when they edit. Between Freedoom 0.12.1 and 0.13.0, MAP01 went from 198 to 211 sectors. A patch that says "sector #74" is wrong on the new version.&lt;/p&gt;

&lt;p&gt;A pin carries a fingerprint of its record: centroid, bounding box, area, heights, line count, textures and tag. If the record at the expected index doesn't match, the engine scores the other sectors. It relocates only when exactly one candidate scores at least 0.85 and leads the next one by at least 0.1. Otherwise the whole patch is refused.&lt;/p&gt;

&lt;p&gt;The Relocation Demo patch pins MAP01 #74 from 0.12.1. On 0.13.0 it resolves to &lt;strong&gt;#196&lt;/strong&gt; with score 0.8728:&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%2Fkd06qclbbmyq11gbp5px.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%2Fkd06qclbbmyq11gbp5px.png" alt="RELOCATED: #74 → #196, the old position drawn dashed, the new one in orange" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I want to be honest about how strong that is. 0.8728 is only 0.023 above the threshold. The sector's boundary lines changed between versions, so this match is backed by area overlap (94% of #74's sample points fall inside #196), not by identical geometry. The weights were calibrated on 0.12.1 and 0.13.0 only, and 0.11.3 was held out. On the held-out version there is one known wrong pick: MAP25 #621 → #622, two concentric octagonal rings where the outer one was merged into the room. It's documented in the repository, and I didn't tune the weights to hide it. The workbench marks every relocated record in orange with "review".&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that went wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flaky tests.&lt;/strong&gt; A coverage threshold in one property test was too tight and failed about one run in ten. Kiro worked out the distribution and loosened it, and tripled the runs of a second test that missed now and then.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows file names.&lt;/strong&gt; The CLI and MCP server refuse to overwrite existing files. An independent probe showed that Windows device names (&lt;code&gt;NUL&lt;/code&gt;, &lt;code&gt;COM1.wad&lt;/code&gt;) and NTFS stream syntax (&lt;code&gt;out.wad:x&lt;/code&gt;) got past that check. Kiro now rejects them on every platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An always-false flag.&lt;/strong&gt; The export report said no map was modified, because the export re-parsed the output and lost track of what had changed. The bytes were right and the report was wrong. Now the changes are replayed on the input and must match the engine's output byte for byte, or nothing is written.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI's first run.&lt;/strong&gt; It failed before any test ran: a step used a git-ignored cache folder as its working directory, and that folder doesn't exist on a fresh checkout. One &lt;code&gt;mkdir&lt;/code&gt; fixed it. The second run downloaded and checked both Freedoom releases and ran all 295 adapter tests without skipping any.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it ended up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;511 tests across five packages: core, the WAD adapter, the CLI, the MCP server and the web workbench. CI runs them on every push with real Freedoom data; only the four tests that need the DSDA-Doom engine are skipped there.&lt;/li&gt;
&lt;li&gt;A browser workbench that calls the same engine as the CLI. A PWAD exported in the browser is byte-identical to one exported by the CLI.&lt;/li&gt;
&lt;li&gt;The PWADs it writes load and run in DSDA-Doom. Passing checks only mean the checks passed: they don't prove a map can be finished.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this used about 1,400 Kiro credits.&lt;/p&gt;

&lt;p&gt;What I liked most about Kiro was how well the spec workflow and steering held the line: writing designs with explicit properties before coding—and even retrofitting a spec onto wad-roundtrip—caught real bugs, and when new guardrails broke existing tests, Kiro fixed the tests instead of weakening the rules. The rough edges were mostly in the IDE plumbing: the inline run buttons in tasks.md failed with "Could not locate tasks.md" so I had to run task lists from chat, and PostFileSave hook output never came back to the agent, which meant polling processes just to prove the hooks fired. Next time I would write the spec before the code on the first package too, and wire up the MCP server and in-engine checks on day one so visual blind spots like the invisible light effect show up sooner.&lt;/p&gt;

&lt;p&gt;Try it at &lt;strong&gt;&lt;a href="https://necrohex.koxia.cn" rel="noopener noreferrer"&gt;https://necrohex.koxia.cn&lt;/a&gt;&lt;/strong&gt;. Pick "Careless Edit" to watch the guardrails refuse a patch, or "Relocation Demo" to see a pin follow its room to a new version. The code is at &lt;strong&gt;&lt;a href="https://github.com/kanzaki-chiya/NecroHex" rel="noopener noreferrer"&gt;https://github.com/kanzaki-chiya/NecroHex&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written for Kiroween 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gamedev</category>
      <category>typescript</category>
      <category>hackathon</category>
    </item>
  </channel>
</rss>
