<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Martin Lysk Blog RSS Feed</title>
        <link>https://blog.lysk.tech/</link>
        <description>Personal blog about technology and programming</description>
        <lastBuildDate>Sun, 09 Aug 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>Copyright © 2026 Martin Lysk</copyright>
        <item>
            <title><![CDATA[Markleft: How I review claude plans in Markdown]]></title>
            <link>https://blog.lysk.tech/markleft-ai-markdown-review</link>
            <guid>https://blog.lysk.tech/markleft-ai-markdown-review</guid>
            <pubDate>Sun, 09 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[AI output seldom meets the intent on the first prompt, so iteration is necessary. Today, iteration usually means providing feedback in a follow-up prompt, which leads to a rewrite of the whole document. The prompt that explains the intent behind the revision remains buried in the chat.]]></description>
            <content:encoded><![CDATA[<p>AI output seldom meets the intent on the first prompt, so iteration is necessary. Today, iteration usually means providing feedback in a follow-up prompt, which leads to a rewrite of the whole document. The prompt that explains the intent behind the revision remains buried in the chat.</p>
<p>Imagine sending a colleague a five page document. They read it and have some concerns. The workflow would <strong>not</strong> be:</p>
<blockquote>
<ul>
<li class="">Email me a prose description of everything you dislike</li>
<li class="">I send you a completely rewritten document</li>
<li class="">Then compare the two files and figure out whether I understood you</li>
</ul>
</blockquote>
<p>That is absurd—this workflow has been solved for decades by comments and tracked changes in Word, or by Suggesting mode in Google Docs.</p>
<p><img decoding="async" loading="lazy" alt="pelican" src="https://blog.lysk.tech/assets/images/markleft-editor-c9680d62e9b30e1fa4d8ca9fc280a5c8.png" width="1481" height="497" class="img_VXcA"></p>
<p>Want to see it in action? <a href="https://blog.lysk.tech/markleft-ai-markdown-review#see-it-in-action" class="">spoiler</a></p>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="working-with-ai-in-markdown-and-where-it-breaks">Working with AI in Markdown and where it breaks<a href="https://blog.lysk.tech/markleft-ai-markdown-review#working-with-ai-in-markdown-and-where-it-breaks" class="hash-link" aria-label="Direct link to Working with AI in Markdown and where it breaks" title="Direct link to Working with AI in Markdown and where it breaks" translate="no">​</a></h3>
<p>Let's play through the process of creating a document—and iterate once.</p>
<p>A prompt to Claude:</p>
<blockquote>
<p>Please write a little story about a pelican on a bike in Markdown, and add a small SVG illustration of the pelican.</p>
</blockquote>
<p>This results in:</p>
 <table>
    <thead>
      <tr>
        <th width="50%">Markdown</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>
<div class="language-text codeBlockContainer_huE5 theme-code-block" style="--prism-background-color:#F3F4F6;--prism-color:#1F2937"><div class="codeBlockContent_oko_"><pre tabindex="0" class="prism-code language-text codeBlock_BBgU thin-scrollbar" style="background-color:#F3F4F6;color:#1F2937"><code class="codeBlockLines_t9f9"><span class="token-line" style="color:#1F2937"><span class="token plain">### A Poor Pelican</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">Perry the pelican found a bright</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">red bicycle leaning against the pier—and,</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">after one curious glance,</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">decided it was exactly the sort of adventure</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">the morning required.</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">He wobbled past the fishing boats,</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">rang the tiny bell with his beak,</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">and rolled onto the beach—just in time for breakfast.</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">![pelican](./pelican-on-a-bycicle-v1.svg)</span><br></span></code></pre></div></div>
</td></tr><tr><td>Rendered</td></tr><tr><td>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="a-poor-pelican">A Poor Pelican<a href="https://blog.lysk.tech/markleft-ai-markdown-review#a-poor-pelican" class="hash-link" aria-label="Direct link to A Poor Pelican" title="Direct link to A Poor Pelican" translate="no">​</a></h3>
<p>Perry the pelican found a bright red bicycle leaning against the pier—and, after one curious glance, decided it was exactly the sort of adventure the morning required. He wobbled past the fishing boats, rang the tiny bell with his beak, and rolled onto the beach—just in time for breakfast.<img decoding="async" loading="lazy" alt="pelican" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA5NjAgNjAwIiByb2xlPSJpbWciIGFyaWEtbGFiZWxsZWRieT0idGl0bGUgZGVzYyI+CiAgPHRpdGxlIGlkPSJ0aXRsZSI+UGVsaWNhbiByaWRpbmcgYSBiaWN5Y2xlPC90aXRsZT4KICA8ZGVzYyBpZD0iZGVzYyI+QSBjaGVlcmZ1bCB3aGl0ZSBwZWxpY2FuIHBlZGFscyBhIHRlYWwgYmljeWNsZSBhbG9uZyBhIHNlYXNpZGUgcGF0aCBiZW5lYXRoIGEgd2FybSBzdW4uPC9kZXNjPgogIDxkZWZzPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJza3kiIHgxPSIwIiB5MT0iMCIgeDI9IjAiIHkyPSIxIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjYmZlOWZmIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2Y1ZmJmNCIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0ic2VhIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzJhYTdiZCIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM2OGM2YzciLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICA8ZmlsdGVyIGlkPSJzaGFkb3ciIHg9Ii0yMCUiIHk9Ii0yMCUiIHdpZHRoPSIxNDAlIiBoZWlnaHQ9IjE0MCUiPgogICAgICA8ZmVEcm9wU2hhZG93IGR4PSIwIiBkeT0iNiIgc3RkRGV2aWF0aW9uPSI2IiBmbG9vZC1jb2xvcj0iIzE4MzMzYiIgZmxvb2Qtb3BhY2l0eT0iLjIiLz4KICAgIDwvZmlsdGVyPgogIDwvZGVmcz4KCiAgPHJlY3Qgd2lkdGg9Ijk2MCIgaGVpZ2h0PSI2MDAiIGZpbGw9InVybCgjc2t5KSIvPgogIDxjaXJjbGUgY3g9Ijc5MCIgY3k9IjEwNSIgcj0iNTUiIGZpbGw9IiNmZmQxNjYiIG9wYWNpdHk9Ii45NSIvPgogIDxwYXRoIGQ9Ik0wIDMzMCBDMTUwIDMwMCAyODUgMzUwIDQzMCAzMjIgUzczNSAyOTIgOTYwIDMyOCBWNDQwIEgwWiIgZmlsbD0idXJsKCNzZWEpIi8+CiAgPHBhdGggZD0iTTAgMzU2IEMxNDAgMzQwIDI1MCAzNzggMzkwIDM1MCBTNzAwIDMyNiA5NjAgMzYwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlOGZmZmYiIHN0cm9rZS13aWR0aD0iOCIgb3BhY2l0eT0iLjgiLz4KICA8cGF0aCBkPSJNMCA0MzAgUTI1MCA0MTAgNDgwIDQzOCBUOTYwIDQyMCBWNjAwIEgwWiIgZmlsbD0iI2U2Y2Y5ZCIvPgogIDxwYXRoIGQ9Ik0wIDQ5MiBRMjUwIDQ2NSA0ODAgNDkzIFQ5NjAgNDc2IiBmaWxsPSJub25lIiBzdHJva2U9IiNjMDljNjIiIHN0cm9rZS13aWR0aD0iNyIgc3Ryb2tlLWRhc2hhcnJheT0iMjAgMTgiIG9wYWNpdHk9Ii41NSIvPgoKICA8ZyBmaWxsPSJub25lIiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBvcGFjaXR5PSIuOCI+CiAgICA8cGF0aCBkPSJNOTIgMTI1IHExOC0xNyAzNiAwIHExOC0xNyAzNiAwIi8+CiAgICA8cGF0aCBkPSJNMjUwIDgyIHExNS0xNCAzMCAwIHExNS0xNCAzMCAwIi8+CiAgICA8cGF0aCBkPSJNNjkwIDE4NCBxMTQtMTMgMjggMCBxMTQtMTMgMjggMCIvPgogIDwvZz4KCiAgPGcgZmlsdGVyPSJ1cmwoI3NoYWRvdykiPgogICAgPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMTYzYTQzIiBzdHJva2Utd2lkdGg9IjEyIj4KICAgICAgPGNpcmNsZSBjeD0iMzAwIiBjeT0iNDc1IiByPSI5MSIgZmlsbD0iI2Y4ZmJmNSIgZmlsbC1vcGFjaXR5PSIuMTYiLz4KICAgICAgPGNpcmNsZSBjeD0iNjkwIiBjeT0iNDc1IiByPSI5MSIgZmlsbD0iI2Y4ZmJmNSIgZmlsbC1vcGFjaXR5PSIuMTYiLz4KICAgIDwvZz4KICAgIDxnIHN0cm9rZT0iIzdhOTI5NSIgc3Ryb2tlLXdpZHRoPSIzIiBvcGFjaXR5PSIuNzUiPgogICAgICA8cGF0aCBkPSJNMzAwIDM4NHYxODJNMjA5IDQ3NWgxODJNMjM2IDQxMWwxMjggMTI4TTIzNiA1MzlsMTI4LTEyOCIvPgogICAgICA8cGF0aCBkPSJNNjkwIDM4NHYxODJNNTk5IDQ3NWgxODJNNjI2IDQxMWwxMjggMTI4TTYyNiA1MzlsMTI4LTEyOCIvPgogICAgPC9nPgoKICAgIDxnIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzA4N2Y4YyIgc3Ryb2tlLXdpZHRoPSIxNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KICAgICAgPHBhdGggZD0iTTMwMCA0NzVsMTI4LTE4IDg2LTEyMSA2MiAxMzlIMzAwbDg2LTEzOWgxMjgiLz4KICAgICAgPHBhdGggZD0iTTU3NiA0NzVsMTE0IDAiLz4KICAgICAgPHBhdGggZD0iTTUxNCAzMzZsLTIyLTQzIi8+CiAgICAgIDxwYXRoIGQ9Ik00OTAgMjkyaDU1Ii8+CiAgICAgIDxwYXRoIGQ9Ik00MjggNDU3bC0xOCA0MyIvPgogICAgICA8cGF0aCBkPSJNMzk4IDUwNmgzNSIvPgogICAgICA8cGF0aCBkPSJNNTE0IDMzNmw4NC0xOCIvPgogICAgICA8cGF0aCBkPSJNNTk4IDMxOGwyNCAyMCIvPgogICAgPC9nPgogICAgPGNpcmNsZSBjeD0iNDI4IiBjeT0iNDU3IiByPSIxNyIgZmlsbD0iI2YyYTY1YSIgc3Ryb2tlPSIjMTYzYTQzIiBzdHJva2Utd2lkdGg9IjYiLz4KCiAgICA8Zz4KICAgICAgPHBhdGggZD0iTTQzMCAzMjUgQzM4MiAyODUgMzg0IDIxMSA0NDAgMTc0IEM0OTQgMTM5IDU2OCAxNjYgNTgyIDIyNyBDNTk0IDI4MCA1NTMgMzM1IDQ4OSAzNDhaIiBmaWxsPSIjZjhmNWU4IiBzdHJva2U9IiMyOTRjNTQiIHN0cm9rZS13aWR0aD0iNyIvPgogICAgICA8cGF0aCBkPSJNNDQ1IDMwOSBDNDE2IDI2OSA0MjYgMjIwIDQ2NSAxOTggQzQ5NSAxODEgNTMzIDE4OSA1NDkgMjE2IEM1MDkgMjE1IDQ3NyAyNTEgNDgwIDI5OFoiIGZpbGw9IiNkOWU1ZDEiLz4KICAgICAgPHBhdGggZD0iTTQ3NSAzMzcgQzQ1MiAzNjcgNDMzIDM5OCA0MjQgNDM4IiBmaWxsPSJub25lIiBzdHJva2U9IiNlN2EyNGQiIHN0cm9rZS13aWR0aD0iMTMiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogICAgICA8cGF0aCBkPSJNNTE2IDMzNyBDNTM2IDM2NiA1NTAgMzk3IDU2NSA0MzAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2U3YTI0ZCIgc3Ryb2tlLXdpZHRoPSIxMyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CiAgICAgIDxwYXRoIGQ9Ik00MDUgNDM3IHEyMi0xMiA0MyAzIiBmaWxsPSJub25lIiBzdHJva2U9IiNkZjgzMzciIHN0cm9rZS13aWR0aD0iMTAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogICAgICA8cGF0aCBkPSJNNTQ4IDQzMCBxMjEtMTAgNDEgNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZGY4MzM3IiBzdHJva2Utd2lkdGg9IjEwIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KCiAgICAgIDxwYXRoIGQ9Ik00MzkgMTc4IEM0MDggMTQ0IDQxNCA5NiA0NDkgNzYgQzQ4MiA1NyA1MjYgNzIgNTM4IDEwOSBDNTQ3IDEzNyA1MzIgMTY5IDUwNSAxODVaIiBmaWxsPSIjZmZmZGYzIiBzdHJva2U9IiMyOTRjNTQiIHN0cm9rZS13aWR0aD0iNyIvPgogICAgICA8cGF0aCBkPSJNNDQ1IDc5IEM0MzEgNTUgNDQyIDMyIDQ2NCAyOCBDNDg2IDI0IDUwNiA0MCA1MDcgNjEgQzQ4NiA1NCA0NjQgNjIgNDQ1IDc5WiIgZmlsbD0iI2ZmZmRmMyIgc3Ryb2tlPSIjMjk0YzU0IiBzdHJva2Utd2lkdGg9IjYiLz4KICAgICAgPGNpcmNsZSBjeD0iNDgxIiBjeT0iOTYiIHI9IjgiIGZpbGw9IiMxNjNhNDMiLz4KICAgICAgPGNpcmNsZSBjeD0iNDg0IiBjeT0iOTMiIHI9IjIuNSIgZmlsbD0iI2ZmZiIvPgogICAgICA8cGF0aCBkPSJNNDQzIDExMyBDMzkyIDEwOCAzMzkgMTI0IDMwMiAxNTQgQzM1NCAxNzAgNDIwIDE2NiA0NjQgMTQzWiIgZmlsbD0iI2Y0YTI2MSIgc3Ryb2tlPSIjMjk0YzU0IiBzdHJva2Utd2lkdGg9IjciIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KICAgICAgPHBhdGggZD0iTTMwMyAxNTQgQzM1MCAxNTEgNDAzIDE0NSA0NTggMTMyIiBmaWxsPSJub25lIiBzdHJva2U9IiNkOTc3M2YiIHN0cm9rZS13aWR0aD0iNCIgb3BhY2l0eT0iLjgiLz4KICAgICAgPHBhdGggZD0iTTQ0NiAxNDggQzQxOCAxNjAgNDEwIDE5MCA0MjYgMjEyIEM0NTMgMjEwIDQ3OCAxOTIgNDg2IDE2OVoiIGZpbGw9IiNmNmJkNjAiIHN0cm9rZT0iIzI5NGM1NCIgc3Ryb2tlLXdpZHRoPSI2Ii8+CgogICAgICA8cGF0aCBkPSJNNDI1IDIyMiBDMzgyIDIzMCAzNDggMjYyIDMzNCAzMDMgQzM2NSAzMTAgNDAxIDI5NiA0MjUgMjcxIiBmaWxsPSIjZWVmMmRmIiBzdHJva2U9IiMyOTRjNTQiIHN0cm9rZS13aWR0aD0iNyIvPgogICAgICA8cGF0aCBkPSJNMzM0IDMwMyBDMzU0IDI4OCAzNzcgMjc2IDQwNSAyNjkiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2NiZDljMyIgc3Ryb2tlLXdpZHRoPSI1Ii8+CiAgICAgIDxwYXRoIGQ9Ik01NDggMjI1IEM1ODEgMjM1IDYwMyAyNjMgNjA4IDI5NyBDNTc5IDI5OSA1NTMgMjg1IDUzOCAyNjIiIGZpbGw9IiNlZWYyZGYiIHN0cm9rZT0iIzI5NGM1NCIgc3Ryb2tlLXdpZHRoPSI3Ii8+CiAgICAgIDxwYXRoIGQ9Ik01OTEgMjg3IEM2MDQgMzA1IDYxMSAzMTkgNjE1IDMzNyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTdhMjRkIiBzdHJva2Utd2lkdGg9IjExIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KICAgIDwvZz4KCiAgICA8Zz4KICAgICAgPHBhdGggZD0iTTYyNiAzNDkgaDk0IHY1OCBoLTk0eiIgZmlsbD0iI2M5ODczZiIgc3Ryb2tlPSIjNjg0YjJlIiBzdHJva2Utd2lkdGg9IjYiLz4KICAgICAgPHBhdGggZD0iTTYzNCAzNTd2NDJNNjUzIDM1N3Y0Mk02NzIgMzU3djQyTTY5MSAzNTd2NDJNNzEwIDM1N3Y0MiIgc3Ryb2tlPSIjZTRiNTZkIiBzdHJva2Utd2lkdGg9IjUiLz4KICAgICAgPHBhdGggZD0iTTYyMCAzNDkgcTUyLTQwIDEwNSAwIiBmaWxsPSJub25lIiBzdHJva2U9IiM2ODRiMmUiIHN0cm9rZS13aWR0aD0iNyIvPgogICAgICA8cGF0aCBkPSJNNjUwIDM3NyBxMTgtMjkgMzggMCBxLTE4IDE5LTM4IDBaIiBmaWxsPSIjYThkYWRjIiBzdHJva2U9IiMzMTViNjMiIHN0cm9rZS13aWR0aD0iNCIvPgogICAgICA8Y2lyY2xlIGN4PSI2ODAiIGN5PSIzNjkiIHI9IjMiIGZpbGw9IiMxNjNhNDMiLz4KICAgIDwvZz4KICA8L2c+CgogIDx0ZXh0IHg9IjUyIiB5PSI3MCIgZmlsbD0iIzIwNGI1NSIgZm9udC1mYW1pbHk9InVpLXJvdW5kZWQsIHN5c3RlbS11aSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyOCIgZm9udC13ZWlnaHQ9IjcwMCI+UGVycnkgdGVzdHMgdGhlIG1vZGVs4oCZcyBhZHZpY2U8L3RleHQ+Cjwvc3ZnPgo=" width="960" height="600" class="img_VXcA"></p>
</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="step-one-collecting-and-formulating-the-critique">Step one: Collecting and formulating the critique<a href="https://blog.lysk.tech/markleft-ai-markdown-review#step-one-collecting-and-formulating-the-critique" class="hash-link" aria-label="Direct link to Step one: Collecting and formulating the critique" title="Direct link to Step one: Collecting and formulating the critique" translate="no">​</a></h3>
<p>Turning your feedback into useful instructions is already a precision problem.</p>
<blockquote>
<p><strong>The headline</strong> promises the wrong story. Remove the em dashes, shorten <strong>the second sentence</strong>, fix the pelican's head, put <strong>the left foot</strong> on a pedal, and make the bird look sportier.</p>
</blockquote>
<p>That sounds specific. It is not.</p>
<p>“<strong>The headline</strong>” works only because this toy document has one obvious headline. In a longer article you need to describe the headline you want to change.</p>
<p>“<strong>The second sentence</strong>” is way more specific, but it makes both the reviewer and the AI count sentences, hoping both count them the same way.</p>
<p><strong>“The left foot”</strong> is worse: prose cannot point to the exact shape in the SVG that broke the illusion. That is why tools such as Figma allow comments directly on the design surface.</p>
<p>Now try giving the same kind of feedback on a multi-page document with repeated headings, nested tables, diagrams, and code.</p>
<blockquote>
<p>Please update the second image in the third paragraph that displays the network architecture. Move the backup server to the top right, next to the second database server, in read-only mode.</p>
</blockquote>
<p>In larger documents, another problem emerges: collecting the feedback in the first place. While reading paragraph 1, one may think, “This doesn't make sense,” but maybe paragraph 3 explains it. When such a thought comes up, I don't want to stop reading and formulate a prompt. I want to leave a mark, continue reading, and come back later.</p>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="step-two-understand-the-change">Step two: Understand the change<a href="https://blog.lysk.tech/markleft-ai-markdown-review#step-two-understand-the-change" class="hash-link" aria-label="Direct link to Step two: Understand the change" title="Direct link to Step two: Understand the change" translate="no">​</a></h3>
<p>Several approaches can help, but they share the same weakness: the AI produces a new document before the author can review the proposed changes in context.</p>
<p>We are still in the first iteration. An update executed by an LLM does not mean that our feedback was taken into account or that our intent was met. We are left with two tasks:</p>
<ol>
<li class="">
<p><strong>Find the change.</strong> One round of “spot the difference” across the whole document. Git can show diffs, but changes hidden behind rendered Mermaid diagrams, tables, and SVGs are much harder to grasp.</p>
</li>
<li class="">
<p><strong>Recover the intent.</strong> Once we know what changed, we still have to remember which part of our original prompt the change was meant to address and decide whether the result actually satisfies the feedback.</p>
</li>
</ol>
<p>Remember that more specific prompt:</p>
<blockquote>
<p>shorten the second sentence</p>
</blockquote>
<p>What if the first sentence is dropped in the rework by the AI? Mapping this prompt to the intent means:</p>
<ol>
<li class="">What was sentence 2 before the change?</li>
<li class="">How did this sentence change?</li>
<li class="">Does this reflect the intent of being shorter?</li>
</ol>
<p>We should only care whether that sentence is now shorter.</p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="the-ideasuggestion-mode-for-markdown">The Idea—Suggestion Mode for Markdown<a href="https://blog.lysk.tech/markleft-ai-markdown-review#the-ideasuggestion-mode-for-markdown" class="hash-link" aria-label="Direct link to The Idea—Suggestion Mode for Markdown" title="Direct link to The Idea—Suggestion Mode for Markdown" translate="no">​</a></h2>
<p>What is needed is a way to annotate parts of the document directly instead of describing their location vaguely, along with a way to review proposed changes against those annotations.</p>
<p>This is exactly what <em>Markleft</em> provides. It is based on three main components:</p>
<ol>
<li class="">A <strong>WYSIWYG</strong> Markdown editor for <strong>humans</strong> to create and edit <strong>comments</strong>, propose changes, and <strong>apply suggestions</strong> without thinking about the underlying format.</li>
<li class="">A <strong>Markdown-compatible annotation spec</strong> consumable by AI, allowing users to <strong>comment</strong> on text, code, tables, Mermaid diagrams, images, and SVGs, and to propose <strong>suggestions</strong>.</li>
<li class="">A <strong>prompt</strong> that tells the AI how to address the comments and instructs it to <strong>append suggestions only</strong> in <strong>Markleft</strong>.</li>
</ol>
<blockquote>
<p>AI gives you a Mark<strong>down </strong>. By clicking a bookmark you can review it as a Mark<strong>up</strong>. You have remarks? Leave a note and save the Markdown. The file has a Mark<strong>left</strong> now. Pass it back to AI. Instead of a rewrite it makes Suggestions you can check and see if they cover your Mark<strong>right.&nbsp;</strong></p>
</blockquote>
<p>Markdown remains the document format.</p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="markleft">Markleft<a href="https://blog.lysk.tech/markleft-ai-markdown-review#markleft" class="hash-link" aria-label="Direct link to Markleft" title="Direct link to Markleft" translate="no">​</a></h2>
<p>The spec uses ordinary Markdown constructs to enable suggestion mode.</p>
<p><img decoding="async" loading="lazy" alt="How annotations and append-only suggestions travel through Markdown" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDYyMCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsbGVkYnk9InRpdGxlIGRlc2MiPgogIDx0aXRsZSBpZD0idGl0bGUiPk1hcmtMZWZ0IGFubm90YXRpb24gYW5kIHN1Z2dlc3Rpb24gcHJvdG9jb2w8L3RpdGxlPgogIDxkZXNjIGlkPSJkZXNjIj5BIE1hcmtkb3duIGRvY3VtZW50IGtlZXBzIGl0cyBvcmlnaW5hbCBibG9ja3Mgd2hpbGUgdHlwZWQgZm9vdG5vdGUgY29tbWVudHMgYW5kIGFwcGVuZC1vbmx5IHN1Z2dlc3Rpb24gZGVmaW5pdGlvbnMgZmVlZCBhIHJpY2ggSFRNTCByZXZpZXcgdmlldy48L2Rlc2M+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agc3RvcC1jb2xvcj0iI2Y5ZjZlYyIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2VlZjVmMiIvPjwvbGluZWFyR3JhZGllbnQ+CiAgICA8bWFya2VyIGlkPSJhcnJvdyIgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSI4IiByZWZZPSI1IiBtYXJrZXJXaWR0aD0iOCIgbWFya2VySGVpZ2h0PSI4IiBvcmllbnQ9ImF1dG8iPjxwYXRoIGQ9Ik0wIDBsMTAgNS0xMCA1eiIgZmlsbD0iIzM4NTc2MCIvPjwvbWFya2VyPgogICAgPGZpbHRlciBpZD0ic2hhZG93Ij48ZmVEcm9wU2hhZG93IGR4PSIwIiBkeT0iNCIgc3RkRGV2aWF0aW9uPSI2IiBmbG9vZC1jb2xvcj0iIzE3MzQzZCIgZmxvb2Qtb3BhY2l0eT0iLjE0Ii8+PC9maWx0ZXI+CiAgPC9kZWZzPgogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjYyMCIgcng9IjI4IiBmaWxsPSJ1cmwoI2JnKSIvPgogIDxnIGZvbnQtZmFtaWx5PSJ1aS1tb25vc3BhY2UsU0ZNb25vLVJlZ3VsYXIsTWVubG8sbW9ub3NwYWNlIiBmaWx0ZXI9InVybCgjc2hhZG93KSI+CiAgICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1MiA2NSkiPjxyZWN0IHdpZHRoPSI0MzAiIGhlaWdodD0iNDkwIiByeD0iMjIiIGZpbGw9IiNmZmZkZjgiIHN0cm9rZT0iIzcxOGU5MSIgc3Ryb2tlLXdpZHRoPSIzIi8+PHRleHQgeD0iMjgiIHk9IjQ1IiBmb250LWZhbWlseT0idWktcm91bmRlZCxzeXN0ZW0tdWksc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyMyIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzI0NDc1MCI+TWFya2Rvd24gc3RheXMgY2Fub25pY2FsPC90ZXh0PjxyZWN0IHg9IjI1IiB5PSI3MCIgd2lkdGg9IjM4MCIgaGVpZ2h0PSIxMDUiIHJ4PSIxMiIgZmlsbD0iI2VkZjRmMSIvPjx0ZXh0IHg9IjQzIiB5PSIxMDEiIGZvbnQtc2l6ZT0iMTUiIGZpbGw9IiMzMTU0NWQiPiZsdDshLS0gbWFya2xlZnQ6YmxvY2sgaWQ9ImJhYmY4MjViIiAtLSZndDs8L3RleHQ+PHRleHQgeD0iNDMiIHk9IjEzMiIgZm9udC1zaXplPSIxNSIgZmlsbD0iIzMxNTQ1ZCI+IVtQZWxpY2FuXShwZWxpY2FuLXYxLnN2Zyk8L3RleHQ+PHRleHQgeD0iNDMiIHk9IjE1NyIgZm9udC1zaXplPSIxNSIgZmlsbD0iI2JhNjYzOCI+W15pbWFnZS01MjMzLTI1MjgtLi4uXTwvdGV4dD48cmVjdCB4PSIyNSIgeT0iMTk4IiB3aWR0aD0iMzgwIiBoZWlnaHQ9Ijk0IiByeD0iMTIiIGZpbGw9IiNmZmYwZGYiLz48dGV4dCB4PSI0MyIgeT0iMjI5IiBmb250LXNpemU9IjE1IiBmaWxsPSIjOGE0YzJlIj5bXmltYWdlLTUyMzMtMjUyOC0uLi5dOjwvdGV4dD48dGV4dCB4PSI0MyIgeT0iMjU3IiBmb250LXNpemU9IjE1IiBmaWxsPSIjMzE1NDVkIj4gICAgdGhlIGhlYWQgbG9va3Mgc3RyYW5nZTwvdGV4dD48cmVjdCB4PSIyNSIgeT0iMzE2IiB3aWR0aD0iMzgwIiBoZWlnaHQ9IjE0NSIgcng9IjEyIiBmaWxsPSIjZWVlYWZmIi8+PHRleHQgeD0iNDMiIHk9IjM0NyIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzUxNDZhMyI+W15zdWdnZXN0aW9uLXMyLXVwZGF0ZS1ibG9jay08L3RleHQ+PHRleHQgeD0iNDMiIHk9IjM3MCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzUxNDZhMyI+YmFiZjgyNWJdOjwvdGV4dD48dGV4dCB4PSI0MyIgeT0iNDAxIiBmb250LXNpemU9IjE1IiBmaWxsPSIjMzE1NDVkIj4gICAgIVtQZWxpY2FuXShwZWxpY2FuLXYyLnN2Zyk8L3RleHQ+PHRleHQgeD0iNDMiIHk9IjQzMyIgZm9udC1zaXplPSIxNCIgZmlsbD0iI2JhNjYzOCI+ICAgIFteaW1hZ2UtNTIzMy0yNTI4LS4uLl08L3RleHQ+PC9nPgogIDwvZz4KICA8ZyBmaWxsPSJub25lIiBzdHJva2U9IiMzODU3NjAiIHN0cm9rZS13aWR0aD0iNSIgbWFya2VyLWVuZD0idXJsKCNhcnJvdykiPjxwYXRoIGQ9Ik01MDUgMTkwaDExMCIvPjxwYXRoIGQ9Ik01MDUgMzg3aDExMCIvPjwvZz4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2MzYgNjUpIiBmb250LWZhbWlseT0idWktcm91bmRlZCxzeXN0ZW0tdWksc2Fucy1zZXJpZiIgZmlsdGVyPSJ1cmwoI3NoYWRvdykiPjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNDkwIiByeD0iMjIiIGZpbGw9IiNmZmZkZjgiIHN0cm9rZT0iIzcxOGU5MSIgc3Ryb2tlLXdpZHRoPSIzIi8+PHRleHQgeD0iMzAiIHk9IjQ1IiBmb250LXNpemU9IjIzIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMjQ0NzUwIj5IVE1MIGJlY29tZXMgdGhlIHJldmlldyB3b3Jrc3BhY2U8L3RleHQ+PHJlY3QgeD0iMzAiIHk9Ijc2IiB3aWR0aD0iNDUyIiBoZWlnaHQ9IjMwMCIgcng9IjE2IiBmaWxsPSIjZDllZGYyIi8+PHJlY3QgeD0iNDgiIHk9Ijk0IiB3aWR0aD0iNDE2IiBoZWlnaHQ9IjI2NCIgcng9IjEyIiBmaWxsPSIjODJjNmQxIi8+PHBhdGggZD0iTTQ4IDI1OGM3Mi0zMCAxMzAgOCAyMDQtMTRzMTM4LTggMjEyIDE4djk2SDQ4eiIgZmlsbD0iI2U0Y2M5OCIvPjxjaXJjbGUgY3g9IjM1NCIgY3k9IjE1MSIgcj0iNDMiIGZpbGw9IiNmZmZkZjMiIHN0cm9rZT0iIzMxNTQ1ZCIgc3Ryb2tlLXdpZHRoPSI1Ii8+PHBhdGggZD0iTTMyMyAxNTljLTQyLTYtODIgNS0xMTAgMjYgMzkgMTMgODkgMTAgMTI3LTl6IiBmaWxsPSIjZjRhMjYxIiBzdHJva2U9IiMzMTU0NWQiIHN0cm9rZS13aWR0aD0iNSIvPjxjaXJjbGUgY3g9IjM2OSIgY3k9IjE0MiIgcj0iNiIgZmlsbD0iIzE3MzQzZCIvPjxjaXJjbGUgY3g9IjE4MSIgY3k9IjMwMiIgcj0iNDUiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzE3MzQzZCIgc3Ryb2tlLXdpZHRoPSI3Ii8+PGNpcmNsZSBjeD0iMzcxIiBjeT0iMzAyIiByPSI0NSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMTczNDNkIiBzdHJva2Utd2lkdGg9IjciLz48cGF0aCBkPSJNMTgxIDMwMmw3OC0xMCA0Ni03MSAzMiA4MUgxODFsNDktODFoNzUiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzA4N2Y4YyIgc3Ryb2tlLXdpZHRoPSI5IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMjU2IDc2djMwMCIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjQiLz48Y2lyY2xlIGN4PSIyNTYiIGN5PSIyMjciIHI9IjE5IiBmaWxsPSIjZDc3ODQzIiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iNCIvPjxjaXJjbGUgY3g9IjM2NSIgY3k9IjEzOSIgcj0iMTUiIGZpbGw9IiNmZmYxZGMiIHN0cm9rZT0iI2Q3Nzg0MyIgc3Ryb2tlLXdpZHRoPSI0Ii8+PHRleHQgeD0iMzY1IiB5PSIxNDUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiM4YTQ3MjgiPjE8L3RleHQ+PHRleHQgeD0iMzAiIHk9IjQxNyIgZm9udC1zaXplPSIxNyIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzI0NDc1MCI+T3JpZ2luYWwgKyBzdWdnZXN0aW9uICsgaW50ZW50PC90ZXh0Pjx0ZXh0IHg9IjMwIiB5PSI0NDkiIGZvbnQtc2l6ZT0iMTYiIGZpbGw9IiM1ODcwNzgiPkFjY2VwdCwgcmVqZWN0LCBvciByZWZpbmXigJR3aXRob3V0IGxvc2luZyBjb250ZXh0LjwvdGV4dD48L2c+CiAgPHRleHQgeD0iNjAwIiB5PSI1OTUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJ1aS1yb3VuZGVkLHN5c3RlbS11aSxzYW5zLXNlcmlmIiBmb250LXNpemU9IjE3IiBmaWxsPSIjMzg1NzYwIj5TdGFibGUgSURzIGxvY2F0ZSBibG9ja3MgwrcgVHlwZWQgZm9vdG5vdGVzIGxvY2F0ZSBpbnRlbnQgwrcgU3VnZ2VzdGlvbnMgb25seSBhcHBlbmQ8L3RleHQ+Cjwvc3ZnPgo=" width="1200" height="620" class="img_VXcA"></p>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="markleft---the-editor">Markleft - the editor<a href="https://blog.lysk.tech/markleft-ai-markdown-review#markleft---the-editor" class="hash-link" aria-label="Direct link to Markleft - the editor" title="Direct link to Markleft - the editor" translate="no">​</a></h3>
<p>The <em>Markleft</em> editor operates on local Markdown files. It runs as a bookmarklet in Chrome, so you can open a Markdown file, activate the bookmarklet, and get a full-featured editor with comments and suggestions.</p>
<p>It reads the Markdown file, parses its footnotes into Markleft annotations, comments, and suggestions, and renders them.</p>
<p>If a user comments on a selection or places a marker inside an image, the editor injects a corresponding Markleft footnote. When the user saves the file, the editor compiles a prompt that explains Markleft to the AI and describes how to compose suggestions that address the comments.</p>
<p>The AI composes suggestions and appends them to the Markdown file. The editor detects those changes and renders the new suggestions with links to the comments they address. Text is diffed within corresponding rendered elements. List items and table cells are paired before their text is compared, avoiding one meaningless diff across an entire structure. Image-only replacements become a before-and-after slider.</p>
<h5 class="anchor anchorTargetStickyNavbar_l_sM" id="see-it-in-action">See it in action<a href="https://blog.lysk.tech/markleft-ai-markdown-review#see-it-in-action" class="hash-link" aria-label="Direct link to See it in action" title="Direct link to See it in action" translate="no">​</a></h5>
<video controls="">
  <source src="/assets/medias/markleft-editor-35e2ecd2f7c66ad00f8670f44b73491b.mp4" type="video/mp4">
</video>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="markleft---the-spec">Markleft - the spec<a href="https://blog.lysk.tech/markleft-ai-markdown-review#markleft---the-spec" class="hash-link" aria-label="Direct link to Markleft - the spec" title="Direct link to Markleft - the spec" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_l_sM" id="annotations-are-just-markdown-footnotes">Annotations are just Markdown footnotes<a href="https://blog.lysk.tech/markleft-ai-markdown-review#annotations-are-just-markdown-footnotes" class="hash-link" aria-label="Direct link to Annotations are just Markdown footnotes" title="Direct link to Annotations are just Markdown footnotes" translate="no">​</a></h4>
<p>An annotation—like a comment—consists of two parts: an anchor that identifies what it comments on and the comment itself. Markdown has a concept of footnotes that most Markdown renderers support. An anchor uses the format <code>[^id-of-the-footnote]</code>, while its definition appears on a separate line in the format <code>[^id-of-the-footnote]: body of the footnote</code>. To encode additional information—such as selected words or x/y coordinates inside an image—we use a schema in the footnote ID itself.</p>
<p>For a text range:</p>
<div class="language-markdown codeBlockContainer_huE5 theme-code-block" style="--prism-background-color:#F3F4F6;--prism-color:#1F2937"><div class="codeBlockContent_oko_"><pre tabindex="0" class="prism-code language-markdown codeBlock_BBgU thin-scrollbar" style="background-color:#F3F4F6;color:#1F2937"><code class="codeBlockLines_t9f9"><span class="token-line" style="color:#1F2937"><span class="token plain">This sentence needs less ceremony.[^range-prev-12-chars-14824-a1b2]</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"></span><span class="token url-reference url punctuation" style="color:#9CA3AF">[</span><span class="token url-reference url variable" style="color:#3B82F6">^range-prev-12-chars-14824-a1b2</span><span class="token url-reference url punctuation" style="color:#9CA3AF">]</span><span class="token url-reference url punctuation" style="color:#9CA3AF">:</span><span class="token url-reference url" style="color:#3B82F6"> Make</span><span class="token plain"> this more direct.</span><br></span></code></pre></div></div>
<p><code>range-prev-12-chars</code> says that the annotation covers the previous twelve visible, non-whitespace characters. The remaining components provide identity and a content fingerprint so Markleft can detect when an anchor has become stale.</p>
<p>Other IDs encode other kinds of anchors:</p>
<ul>
<li class=""><code>image-X-Y-*</code> stores normalized image coordinates.</li>
<li class=""><code>code-line-L-col-C-len-N-*</code> identifies a code range.</li>
<li class=""><code>block-*</code> addresses the containing block.</li>
<li class=""><code>comment-*</code> represents a reply to another comment.</li>
</ul>
<p>Because the comment is a footnote, Markdown tools preserve it even when they do not understand Markleft. To a normal renderer such as GitHub, it is just a footnote. To the AI and the Markleft editor, it identifies a point on an image or a highlighted sentence inside a text block.</p>
<h4 class="anchor anchorTargetStickyNavbar_l_sM" id="stable-block-ids-make-structural-changes-addressable">Stable block IDs make structural changes addressable<a href="https://blog.lysk.tech/markleft-ai-markdown-review#stable-block-ids-make-structural-changes-addressable" class="hash-link" aria-label="Direct link to Stable block IDs make structural changes addressable" title="Direct link to Stable block IDs make structural changes addressable" translate="no">​</a></h4>
<p>To allow suggestions to target blocks reliably, we need stable identifiers. Markleft injects an HTML comment immediately before each real document block:</p>
<div class="language-markdown codeBlockContainer_huE5 theme-code-block" style="--prism-background-color:#F3F4F6;--prism-color:#1F2937"><div class="codeBlockContent_oko_"><pre tabindex="0" class="prism-code language-markdown codeBlock_BBgU thin-scrollbar" style="background-color:#F3F4F6;color:#1F2937"><code class="codeBlockLines_t9f9"><span class="token-line" style="color:#1F2937"><span class="token tag punctuation" style="color:#9CA3AF">&lt;</span><span class="token tag" style="color:#EA580C">!-</span><span class="token tag" style="color:#EA580C"> </span><span class="token tag attr-name" style="color:#10B981">-</span><span class="token tag" style="color:#EA580C"> </span><span class="token tag attr-name namespace" style="color:#10B981">markleft:</span><span class="token tag attr-name" style="color:#10B981">block</span><span class="token tag" style="color:#EA580C"> </span><span class="token tag attr-name" style="color:#10B981">id</span><span class="token tag attr-value punctuation attr-equals" style="color:#9CA3AF">=</span><span class="token tag attr-value punctuation" style="color:#9CA3AF">"</span><span class="token tag attr-value" style="color:#3B82F6">babf825b</span><span class="token tag attr-value punctuation" style="color:#9CA3AF">"</span><span class="token tag" style="color:#EA580C"> </span><span class="token tag attr-name" style="color:#10B981">--</span><span class="token tag punctuation" style="color:#9CA3AF">&gt;</span><br></span></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_l_sM" id="suggestions-are-unreferenced-append-only-footnotes">Suggestions are unreferenced, append-only footnotes<a href="https://blog.lysk.tech/markleft-ai-markdown-review#suggestions-are-unreferenced-append-only-footnotes" class="hash-link" aria-label="Direct link to Suggestions are unreferenced, append-only footnotes" title="Direct link to Suggestions are unreferenced, append-only footnotes" translate="no">​</a></h4>
<p>A suggestion is a footnote definition with a reserved ID and intentionally no inline footnote anchor in the original body.</p>
<div class="language-markdown codeBlockContainer_huE5 theme-code-block" style="--prism-background-color:#F3F4F6;--prism-color:#1F2937"><div class="codeBlockContent_oko_"><pre tabindex="0" class="prism-code language-markdown codeBlock_BBgU thin-scrollbar" style="background-color:#F3F4F6;color:#1F2937"><code class="codeBlockLines_t9f9"><span class="token-line" style="color:#1F2937"><span class="token url-reference url punctuation" style="color:#9CA3AF">[</span><span class="token url-reference url variable" style="color:#3B82F6">^suggestion-s2-update-block-babf825b</span><span class="token url-reference url punctuation" style="color:#9CA3AF">]</span><span class="token url-reference url punctuation" style="color:#9CA3AF">:</span><span class="token url-reference url" style="color:#3B82F6"> replacement</span><span class="token plain"> Markdown</span><br></span></code></pre></div></div>
<p>The ID says:</p>
<ul>
<li class="">this is suggestion <code>s2</code>;</li>
<li class="">the operation is <code>update</code>;</li>
<li class="">the target is block <code>babf825b</code>.&nbsp;</li>
</ul>
<p>Insert-before, insert-after, and delete operations use the same pattern. The last line of a suggestion body contains footnote anchors for the comments the suggestion addresses; that line is metadata, not part of the proposed content.</p>
<p>This is the crucial append-only property: an AI can add a proposal without receiving permission to alter the document it is reviewing.</p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="give-it-a-try">Give it a try<a href="https://blog.lysk.tech/markleft-ai-markdown-review#give-it-a-try" class="hash-link" aria-label="Direct link to Give it a try" title="Direct link to Give it a try" translate="no">​</a></h2>
<ol>
<li class="">Show the bookmarks bar (⌘⇧B on macOS or Ctrl+Shift+B on Windows/Linux) in chrome</li>
<li class="">Drag this link: <a href="https://blog.lysk.tech/markleft-ai-markdown-review#" data-bookmarklet="javascript:(()=%3E{const d=document,h=d.createElement(&quot;div&quot;),r=h.attachShadow({mode:&quot;open&quot;}),fail=()=%3E{r.querySelector(&quot;i&quot;).style.display=&quot;none&quot;;r.querySelector(&quot;p&quot;).innerHTML=`Couldn’t load Markleft. Check your connection, then %3Ca href=&quot;https://github.com/martin-lysk/markleft&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;%3Echeck for updates or file an issue%3C/a%3E.`};r.innerHTML=`%3Cstyle%3E:host{all:initial}main{position:fixed;z-index:2147483647;inset:0;display:grid;place-items:center;background:#111b;color:#fff;font:16px system-ui;cursor:pointer}section{display:flex;gap:16px;align-items:flex-start;max-width:430px;padding:28px 32px;border:1px solid #ffffff28;border-radius:16px;background:#1c1c20;box-shadow:0 20px 60px #0008}i{flex:none;width:24px;height:24px;border:3px solid #777;border-top-color:#fff;border-radius:50%;animation:s .7s linear infinite}p{margin:0;line-height:1.5}a{color:#9ccaff}a:hover{color:#fff}@keyframes s{to{transform:rotate(360deg)}}%3C/style%3E%3Cmain%3E%3Csection%3E%3Ci%3E%3C/i%3E%3Cp%3EOpening Markleft…%3C/p%3E%3C/section%3E%3C/main%3E`;d.documentElement.append(h);h.addEventListener(&quot;click&quot;,()=%3Eh.remove());setTimeout(()=%3Eh.remove(),10000);const s=d.createElement(&quot;script&quot;),t=setTimeout(fail,8000);s.src=&quot;https://martin-lysk.github.io/markleft/bookmark.js?&quot;+Date.now();s.onload=()=%3EclearTimeout(t);s.onerror=()=%3E{clearTimeout(t);fail()};d.documentElement.append(s)})()" class="">Open in M←</a> into chromes bookmark bar.</li>
<li class="">Open a local Markdown file in chrome and click the bookmark</li>
</ol>
<p>Find the source code and a full installation instruction here: <a href="https://github.com/martin-lysk/markleft" target="_blank" rel="noopener noreferrer" class="">https://github.com/martin-lysk/markleft</a></p>]]></content:encoded>
            <category>ai</category>
            <category>markdown</category>
            <category>review</category>
        </item>
        <item>
            <title><![CDATA[SQLite on Git, Part II: Unlocking Zlib's less known Feature]]></title>
            <link>https://blog.lysk.tech/sqlite-on-git-part-2</link>
            <guid>https://blog.lysk.tech/sqlite-on-git-part-2</guid>
            <pubDate>Mon, 06 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In the previous post, we followed the white rabbit down into Git's .git folder. We understood: Git uses zlib's deflate algorithm to compress objects.]]></description>
            <content:encoded><![CDATA[<img decoding="async" loading="lazy" align="right" width="200" src="https://github.com/martin-lysk/martin-lysk/blob/main/blog/26-04-01-tale-file-part2/behind_the_curtain.png?raw=true" class="img_VXcA">  
<p>In the previous post, we followed the white rabbit down into Git's <code>.git</code> folder. We understood: Git uses zlib's <code>deflate</code> algorithm to compress objects.</p>
<p><strong>The Problem:</strong> Each range of zlib-compressed data depends on reading all the data before it. A 1Gb sqlite database compressed with zlib must read the whole file to access a single row<sup><a href="https://blog.lysk.tech/sqlite-on-git-part-2#user-content-fn-1-535a6d" id="user-content-fnref-1-535a6d" data-footnote-ref="" aria-describedby="footnote-label" class="anchorTargetStickyNavbar_l_sM">1</a></sup>.</p>
<p>In this article we're going to build a mental model on how zlib compresses and we're going to use <code>Z_FULL_FLUSH</code> to compress data in a way which is compatible with Git and allows us to random access data in objects stored in git.</p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="what-has-happened-so-far">What has happened so far<a href="https://blog.lysk.tech/sqlite-on-git-part-2#what-has-happened-so-far" class="hash-link" aria-label="Direct link to What has happened so far" title="Direct link to What has happened so far" translate="no">​</a></h2>
<p>If you're just joining us, here's the quick recap:</p>
<ul>
<li class=""><a class="" href="https://blog.lysk.tech/sqlite-on-git-prologue"><strong>Prologue</strong></a>: We looked at <em>random access</em>, what it is,  and why it is essential for running SQLite databases on top of Git's storage</li>
<li class=""><a class="" href="https://blog.lysk.tech/sqlite-on-git-part-1"><strong>Part 1</strong></a>: We explored Git's <code>.git</code> folder, learned how loose objects are stored, and discovered that <code>Z_FULL_FLUSH</code> could be the key to enabling random access.</li>
</ul>
<p>Let's look behind the curtain of <strong>zlib</strong> the compression library used by Git's storage layer and get a step closer to <em>random access</em>.</p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="understanding-sequential-compression">Understanding sequential compression<a href="https://blog.lysk.tech/sqlite-on-git-part-2#understanding-sequential-compression" class="hash-link" aria-label="Direct link to Understanding sequential compression" title="Direct link to Understanding sequential compression" translate="no">​</a></h2>
<p>Sequential compression is a way of reducing data size where each part depends on what came before it. To read any point, you have to process everything earlier first.</p>
<p>Let's take the following sentence as an example.</p>
<p><code>Hi beautiful World, how you are doing on this beautiful World, what a beautiful World to live on.</code></p>
<p>You can express the same text like this:</p>
<p><code>Hi beautiful World, how you are doing on this [4,15], what a [4,15] to live on</code></p>
<p>See how the text got smaller by expressing <code>beautiful World</code> by pointing to its first occurrence (character <code>[4,</code>) and its length (<code>15]</code>)?</p>
<p></p><div class="excalidraw-player gh-dark-mode-only"><img decoding="async" loading="lazy" alt="Defalation" src="https://blog.lysk.tech/assets/images/deflation.stat.dark.exp-678b8589e20528185322cb755e00e529.svg#gh-dark-mode-only" width="1810" height="764" class="img_VXcA"><img decoding="async" loading="lazy" alt="Defalation" src="https://blog.lysk.tech/assets/images/deflation.anim.dark.exp-a7f9b17849041e9defa5c9e055e10dfd.svg#gh-dark-mode-only" width="1810" height="764" class="img_VXcA"></div>
<div class="excalidraw-player gh-light-mode-only"><img decoding="async" loading="lazy" alt="Deflation" src="https://blog.lysk.tech/assets/images/deflation.stat.light.exp-be9ab2dbe0ce059d5580a545b0328064.svg#gh-light-mode-only" width="1810" height="764" class="img_VXcA"><img decoding="async" loading="lazy" alt="Deflation" src="https://blog.lysk.tech/assets/images/deflation.anim.light.exp-4a290cb070ecba14798a53725aa1c45e.svg#gh-light-mode-only" width="1810" height="764" class="img_VXcA"></div><p></p>
<p>By referencing repetitive sequences of characters, you can reduce the size of the original text while still being able to recreate the original text.</p>
<p>But what happens if I only want to read the part of the sentence after the second <code>,</code>? Reading <code> what a [4,15] we live in?</code> doesn't make sense - the reference <code>[4,15]</code> is wrong/can't be resolved. To resolve it we have to know <code>Hi beautiful World</code> first - we have to read the whole sentence from the very beginning.</p>
<p></p><div class="excalidraw-player gh-dark-mode-only"><img decoding="async" loading="lazy" alt="Defalation" src="https://blog.lysk.tech/assets/images/random_access.stat.dark.exp-e3154b606c3a74f302ed1bec357d199a.svg#gh-dark-mode-only" width="1486" height="308" class="img_VXcA"><img decoding="async" loading="lazy" alt="Defalation" src="https://blog.lysk.tech/assets/images/random_access.anim.dark.exp-c8c2948f1c466988e2be426e5e384a3a.svg#gh-dark-mode-only" width="1486" height="308" class="img_VXcA"></div>
<div class="excalidraw-player gh-light-mode-only"><img decoding="async" loading="lazy" alt="Deflation" src="https://blog.lysk.tech/assets/images/random_access.stat.light.exp-acce93ac248bd1cf7dc6a022f6f21fd4.svg#gh-light-mode-only" width="1486" height="308" class="img_VXcA"><img decoding="async" loading="lazy" alt="Deflation" src="https://blog.lysk.tech/assets/images/random_access.anim.light.exp-ae36eac78129b04f7d17cda91316c8cd.svg#gh-light-mode-only" width="1486" height="308" class="img_VXcA"></div><p></p>
<p>This is - simplified - what zlib does, it reads data coming in, finds repetitive sequences and replaces them with references of previous occurrences.</p>
<p>Okay what is the point - just read the 62 Characters... Remember why we are doing all of this - a sqlite database file can grow over hundreds of megabytes of size - reading just a particular row must not require a read of the whole file.</p>
<div class="markdown-alert markdown-alert-note" dir="auto">
<p class="markdown-alert-title" dir="auto">Note</p>
<p>What I outline here is extremely simplified description of zlib - it should be just enough to follow the article. If you want to understand it better I suggest reading <a href="https://www.euccas.me/zlib/" target="_blank" rel="noopener noreferrer" class="">Understanding zlib</a> by Euccas Chen.</p>
</div>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="compress-in-blocks">Compress in blocks<a href="https://blog.lysk.tech/sqlite-on-git-part-2#compress-in-blocks" class="hash-link" aria-label="Direct link to Compress in blocks" title="Direct link to Compress in blocks" translate="no">​</a></h2>
<p>A common approach to deal with this and allow reading data partially is to not compress the data as a whole. If we define blocks of content that we always want to access independently we can compress each block individually. This ensures later blocks don't reference back to data from previous blocks and we can access the content of those blocks without knowing the other blocks.</p>
<p>Let's take the previous example:</p>
<p><code>Hi beautiful World, how you are doing on this beautiful World, what a beautiful World to live on.</code></p>
<p>We can split it up into blocks, let's say a block should be 61 characters we would get two blocks:</p>
<p><code>Hi beautiful World, how you are doing on this beautiful World</code><br>
and<br>
<code>, what a beautiful World to live on.</code></p>
<p>We can now compress each block separately:</p>
<p><code>Hi beautiful World, how you are doing on this [4,15]</code><br>
and<br>
<code>, what a beautiful World to live on.</code></p>
<p></p><div class="excalidraw-player gh-dark-mode-only"><img decoding="async" loading="lazy" alt="Defalation" src="https://blog.lysk.tech/assets/images/block_deflation.stat.dark.exp-420b4c74fd5a7de4cd942f8211f725e9.svg#gh-dark-mode-only" width="1810" height="764" class="img_VXcA"><img decoding="async" loading="lazy" alt="Defalation" src="https://blog.lysk.tech/assets/images/block_deflation.anim.dark.exp-d2bddfbf15295dbf8af1e7aad3704db0.svg#gh-dark-mode-only" width="1810" height="764" class="img_VXcA"></div>
<div class="excalidraw-player gh-light-mode-only"><img decoding="async" loading="lazy" alt="Deflation" src="https://blog.lysk.tech/assets/images/block_deflation.stat.light.exp-c0f086df19a5b17adaaf99a7dce68480.svg#gh-light-mode-only" width="1810" height="764" class="img_VXcA"><img decoding="async" loading="lazy" alt="Deflation" src="https://blog.lysk.tech/assets/images/block_deflation.anim.light.exp-a5f21808ff8c91313fcc65108783b21b.svg#gh-light-mode-only" width="1810" height="764" class="img_VXcA"></div><p></p>
<p>While the compression is not as good as before - we can now independently access the two blocks and read <code>, what a beautiful World to live on.</code> without reading the first part of the sentence.</p>
<p></p><div class="excalidraw-player gh-dark-mode-only"><img decoding="async" loading="lazy" alt="Defalation" src="https://blog.lysk.tech/assets/images/block_random_access.stat.dark.exp-f924b501487f10903f586bf029a7019f.svg#gh-dark-mode-only" width="1770" height="308" class="img_VXcA"><img decoding="async" loading="lazy" alt="Defalation" src="https://blog.lysk.tech/assets/images/block_random_access.anim.dark.exp-4ead72d9e8ad3b2ebb2ab56b9a72152a.svg#gh-dark-mode-only" width="1770" height="308" class="img_VXcA"></div>
<div class="excalidraw-player gh-light-mode-only"><img decoding="async" loading="lazy" alt="Deflation" src="https://blog.lysk.tech/assets/images/block_random_access.stat.light.exp-6b4677d5241e047a699e86511e51f35c.svg#gh-light-mode-only" width="1770" height="308" class="img_VXcA"><img decoding="async" loading="lazy" alt="Deflation" src="https://blog.lysk.tech/assets/images/block_random_access.anim.light.exp-e6ae2899503914945f03a998c0d97001.svg#gh-light-mode-only" width="1770" height="308" class="img_VXcA"></div><p></p>
<p>Funfact: This approach is also used in git itself already and discussed by Linus Torvalds - the the creator himself.</p>
<blockquote>
<p>&lt;linus&gt; In particular, while the pack-file is then compressed,
it's compressed just one object at a time, so the actual
compression factor is less than it could be in theory. But it
means that it's all nice random-access with a simple index to
do "object name-&gt;location in packfile" translation.</p>
</blockquote>
<p>see <a href="https://github.com/git/git/blob/master/Documentation/technical/pack-heuristics.adoc" target="_blank" rel="noopener noreferrer" class="">pack-heuristics.adoc</a></p>
<div class="markdown-alert markdown-alert-note" dir="auto">
<p class="markdown-alert-title" dir="auto">Note</p>
<p>the random access that is described here is referring to accessing whole objects in git and won't help us for accessing just parts of the objects.</p>
</div>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="the-problem">The problem<a href="https://blog.lysk.tech/sqlite-on-git-part-2#the-problem" class="hash-link" aria-label="Direct link to The problem" title="Direct link to The problem" translate="no">​</a></h2>
<p>If we compress separate blocks we end up having multiple deflated chunks <code>deflate(block1)</code> and <code>deflate(block2)</code>, we can't just concatenate those chunks (the approach Linus took when designing the pack file format) because Git wouldn't just magically consume it.</p>
<p>While independent block deflation worked for Linus Torvalds inside of the pack file because he defined the format and controls how it is read - this is not an option for us... we just don't control the binary layout of git's internal files - we have to deal with the format given.</p>
<p>When git inflates the objects it expects <strong>one continuous stream</strong> and it would refuse to consume our block based deflated data. This is where <code>Z_FULL_FLUSH</code> comes into play.</p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="the-solution---z_full_flush">The Solution - <code>Z_FULL_FLUSH</code><a href="https://blog.lysk.tech/sqlite-on-git-part-2#the-solution---z_full_flush" class="hash-link" aria-label="Direct link to the-solution---z_full_flush" title="Direct link to the-solution---z_full_flush" translate="no">​</a></h2>
<p>The <code>Z_FULL_FLUSH</code> allows us to do block based compression within a single compression stream. By passing <code>Z_FULL_FLUSH</code> as the second parameter to the compression algorithm - we can tell Zlib to reset its compression state.</p>
<blockquote>
<p>If flush is set to Z_FULL_FLUSH, all output is flushed as with Z_SYNC_FLUSH, and the compression state is reset so that decompression can restart from this point if previous compressed data has been damaged or if <strong>random access</strong> is desired. Using Z_FULL_FLUSH too often can seriously degrade compression.</p>
</blockquote>
<p>It's written in plainsight the flag exists for <strong>random access</strong> and I was not the first to discover that - as stated earlier I took inspirations from a blog post by Hengs Li Random <a href="https://lh3.github.io/2014/07/05/random-access-to-zlib-compressed-files" target="_blank" rel="noopener noreferrer" class="">access to zlib compressed files</a> writing about an approach used by Genomic researchers to access huge files.</p>
<p><strong>Mental model</strong>: Passing <code>Z_FULL_FLUSH</code> is like asking Zlib to "Forget everything you have seen before". By doing that Zlib won't back-reference to any data before the Z_FULL_FLUSH was sent.</p>
<p>Let's look at our example a last time.<br>
<em>Block 1</em>: <code>Hi beautiful World, how you are doing on this beautiful World</code><br>
<em>Block 2</em>: <code>, what a beautiful World to live on.</code></p>
<p>Instead of deflating them independently - we start deflating <em>Block 1</em> and when we continue with <em>Block 2</em> we just pass a <code>Z_FULL_FLUSH</code> together with the data.</p>
<p></p><div class="excalidraw-player gh-dark-mode-only"><img decoding="async" loading="lazy" alt="Defalation" src="https://blog.lysk.tech/assets/images/flushed_deflation.stat.dark.exp-16e9d84f4b960f32919f50f56c0f914f.svg#gh-dark-mode-only" width="1810" height="899" class="img_VXcA"><img decoding="async" loading="lazy" alt="Defalation" src="https://blog.lysk.tech/assets/images/flushed_deflation.anim.dark.exp-8116a9173e7aa0c3f044176f89be85ae.svg#gh-dark-mode-only" width="1810" height="899" class="img_VXcA"></div>
<div class="excalidraw-player gh-light-mode-only"><img decoding="async" loading="lazy" alt="Deflation" src="https://blog.lysk.tech/assets/images/flushed_deflation.stat.light.exp-5785cdf0378c1b8707865bc5e14252d4.svg#gh-light-mode-only" width="1810" height="899" class="img_VXcA"><img decoding="async" loading="lazy" alt="Deflation" src="https://blog.lysk.tech/assets/images/flushed_deflation.anim.light.exp-ea0abadb7668115355564f899e778177.svg#gh-light-mode-only" width="1810" height="899" class="img_VXcA"></div><p></p>
<p>This results in a 100% Zlib compatible continuous compression stream with flushpoints after each block which allows us to access them independently.</p>
<p>We just need to know the end of each block to jump to in the compressed data.</p>
<p></p><div class="excalidraw-player gh-dark-mode-only"><img decoding="async" loading="lazy" alt="Defalation" src="https://blog.lysk.tech/assets/images/flush_random_access.stat.dark.exp-a6c47eacb96a5493933a193ede792413.svg#gh-dark-mode-only" width="1770" height="308" class="img_VXcA"><img decoding="async" loading="lazy" alt="Defalation" src="https://blog.lysk.tech/assets/images/flush_random_access.anim.dark.exp-4ce4bf55c01596d32c541a4812cd04c5.svg#gh-dark-mode-only" width="1770" height="308" class="img_VXcA"></div>
<div class="excalidraw-player gh-light-mode-only"><img decoding="async" loading="lazy" alt="Deflation" src="https://blog.lysk.tech/assets/images/flush_random_access.stat.light.exp-fbf189f82d6b9ba44e2a4f4b394be203.svg#gh-light-mode-only" width="1770" height="308" class="img_VXcA"><img decoding="async" loading="lazy" alt="Deflation" src="https://blog.lysk.tech/assets/images/flush_random_access.anim.light.exp-e03efcac7fe46cf6730d24e5d9a81161.svg#gh-light-mode-only" width="1770" height="308" class="img_VXcA"></div><p></p>
<p>A quick skim through the sources of Git looks promising: <a href="https://github.com/git/git/blob/master/git-zlib.c" target="_blank" rel="noopener noreferrer" class="">https://github.com/git/git/blob/master/git-zlib.c</a> and <a href="https://github.com/git/git/blob/master/compat/zlib-compat.h" target="_blank" rel="noopener noreferrer" class="">https://github.com/git/git/blob/master/compat/zlib-compat.h</a> only reference the standard zlib library - as long as the compressed data is zlib compatible Git should be fine with it.</p>
<p>Why this is so great: if we can recompress the objects in git's object store with a flush after every block and git swallows such objects without hiccups we can utilize git's storage - with all its great infrastructure and tooling around it - while being able to read only parts of the objects in random access fashion.</p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="in-practice">In practice<a href="https://blog.lysk.tech/sqlite-on-git-part-2#in-practice" class="hash-link" aria-label="Direct link to In practice" title="Direct link to In practice" translate="no">​</a></h2>
<p>This sounds too good to be true - totally hooked I took a look at existing zlib libraries in Javascript and found <a href="https://github.com/nodeca/pako" target="_blank" rel="noopener noreferrer" class="">pako</a>. It looked pretty promising but while it allows you to push chunks:</p>
<div class="language-javascript codeBlockContainer_huE5 theme-code-block" style="--prism-background-color:#F3F4F6;--prism-color:#1F2937"><div class="codeBlockContent_oko_"><pre tabindex="0" class="prism-code language-javascript codeBlock_BBgU thin-scrollbar" style="background-color:#F3F4F6;color:#1F2937"><code class="codeBlockLines_t9f9"><span class="token-line" style="color:#1F2937"><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> deflator </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token keyword" style="color:#3B82F6">new</span><span class="token plain"> </span><span class="token class-name" style="color:#F59E0B">pako</span><span class="token class-name punctuation" style="color:#9CA3AF">.</span><span class="token class-name" style="color:#F59E0B">Deflate</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">deflator</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token function" style="color:#F59E0B">push</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"Hello beautiful World, how you are doing on this beautiful World"</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">deflator</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token function" style="color:#F59E0B">push</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">", what a beautiful World to live on."</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><br></span></code></pre></div></div>
<p><code>Z_FULL_FLUSH</code> is not exposed via the given API.</p>
<p>So I extended pako to do just that (compare <a href="https://github.com/martin-lysk/talepack/blob/main/packages/zlib-random-access/src/ChunkBlockDeflate.ts" target="_blank" rel="noopener noreferrer" class=""><code>ChunkBlockDeflate</code></a>).</p>
<p>To test my hypothesis I wrote a little <a href="https://github.com/martin-lysk/talepack/blob/main/packages/git-random-access/src/loose-object-test/repacked-loose-object.test.ts" target="_blank" rel="noopener noreferrer" class="">test</a> in vitest which:</p>
<ol>
<li class="">create a normal git object containing <code>Hi beautiful World, how you are doing on this beautiful World, what a beautiful World to live on.</code></li>
<li class="">read the deflated blob content and inflate it using pako and split the content into <code>header</code>, <code>block 1</code> and <code>block 2</code></li>
<li class="">create a ChunkBlockDeflator and push the header, then push block 1, then push block 2 with <code>Z_FULL_FLUSH</code></li>
<li class="">store the deflated result back into the blob</li>
<li class="">check if <code>git cat-file</code> still reads the object</li>
<li class="">read the deflated bytes from block 2 and inflate them independently</li>
</ol>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
show me the code
</summary><div><div class="collapsibleContent_ocAz">
<!-- -->
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
<div class="language-typescript codeBlockContainer_huE5 theme-code-block" style="--prism-background-color:#F3F4F6;--prism-color:#1F2937"><div class="codeBlockContent_oko_"><pre tabindex="0" class="prism-code language-typescript codeBlock_BBgU thin-scrollbar" style="background-color:#F3F4F6;color:#1F2937"><code class="codeBlockLines_t9f9"><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"></span><span class="token keyword module" style="color:#3B82F6">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#9CA3AF">{</span><span class="token imports"> describe</span><span class="token imports punctuation" style="color:#9CA3AF">,</span><span class="token imports"> it</span><span class="token imports punctuation" style="color:#9CA3AF">,</span><span class="token imports"> expect </span><span class="token imports punctuation" style="color:#9CA3AF">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#3B82F6">from</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"vitest"</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"></span><span class="token keyword module" style="color:#3B82F6">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#9CA3AF">{</span><span class="token imports"> execSync </span><span class="token imports punctuation" style="color:#9CA3AF">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#3B82F6">from</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"child_process"</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"></span><span class="token keyword module" style="color:#3B82F6">import</span><span class="token plain"> </span><span class="token imports">fs</span><span class="token plain"> </span><span class="token keyword module" style="color:#3B82F6">from</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"fs"</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"></span><span class="token keyword module" style="color:#3B82F6">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#9CA3AF">{</span><span class="token imports"> tmpdir </span><span class="token imports punctuation" style="color:#9CA3AF">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#3B82F6">from</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"os"</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"></span><span class="token keyword module" style="color:#3B82F6">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#9CA3AF">{</span><span class="token imports"> join </span><span class="token imports punctuation" style="color:#9CA3AF">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#3B82F6">from</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"path"</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"></span><span class="token keyword module" style="color:#3B82F6">import</span><span class="token plain"> </span><span class="token imports operator" style="color:#9CA3AF">*</span><span class="token imports"> </span><span class="token imports keyword module" style="color:#3B82F6">as</span><span class="token imports"> pako</span><span class="token plain"> </span><span class="token keyword module" style="color:#3B82F6">from</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"pako"</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"></span><span class="token keyword module" style="color:#3B82F6">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#9CA3AF">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">BlockInflate</span><span class="token imports punctuation" style="color:#9CA3AF">,</span><span class="token imports"> </span><span class="token imports maybe-class-name">ChunkBlockDeflate</span><span class="token imports"> </span><span class="token imports punctuation" style="color:#9CA3AF">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#3B82F6">from</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"@talepack/zlib-random-access"</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"></span><span class="token function" style="color:#F59E0B">describe</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"loose object test"</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#9CA3AF">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">	</span><span class="token function" style="color:#F59E0B">it</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"should create a simple git blob and verify its content"</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#9CA3AF">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">		</span><span class="token comment" style="color:#6B7280">// Create a temp dir</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">		</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> tempDir </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> fs</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">mkdtempSync</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token function" style="color:#F59E0B">join</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token function" style="color:#F59E0B">tmpdir</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"git-simple-test-"</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">		</span><span class="token keyword control-flow" style="color:#3B82F6">try</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// ------------------------------------------------------------------</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// 1. Create a normal git object containing </span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token comment" style="color:#6B7280">// `Hi beautiful World, how you are doing on this beautiful World, what a beautiful World to live on.`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// ------------------------------------------------------------------</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// Create a git repo in it using init</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token function" style="color:#F59E0B">execSync</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token template-string template-punctuation string" style="color:#10B981">`</span><span class="token template-string string" style="color:#10B981">git init "</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#9CA3AF">${</span><span class="token template-string interpolation">tempDir</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#9CA3AF">}</span><span class="token template-string string" style="color:#10B981">"</span><span class="token template-string template-punctuation string" style="color:#10B981">`</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">{</span><span class="token plain"> encoding</span><span class="token operator" style="color:#9CA3AF">:</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"utf-8"</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> cwd</span><span class="token operator" style="color:#9CA3AF">:</span><span class="token plain"> tempDir </span><span class="token punctuation" style="color:#9CA3AF">}</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// Create a readme containing "Hi beautiful World, how you are doing on this beautiful World, what a beautiful World to live on."</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> readmePath </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token function" style="color:#F59E0B">join</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">tempDir</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"readme.md"</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			fs</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">writeFileSync</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">readmePath</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"Hi beautiful World, how you are doing on this beautiful World, what a beautiful World to live on."</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// Use git add to add the readme</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token function" style="color:#F59E0B">execSync</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token template-string template-punctuation string" style="color:#10B981">`</span><span class="token template-string string" style="color:#10B981">git add readme.md</span><span class="token template-string template-punctuation string" style="color:#10B981">`</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">{</span><span class="token plain"> encoding</span><span class="token operator" style="color:#9CA3AF">:</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"utf-8"</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> cwd</span><span class="token operator" style="color:#9CA3AF">:</span><span class="token plain"> tempDir </span><span class="token punctuation" style="color:#9CA3AF">}</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// Use `git hash-object readme.md` to get the hash</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> hashOutput </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token function" style="color:#F59E0B">execSync</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token template-string template-punctuation string" style="color:#10B981">`</span><span class="token template-string string" style="color:#10B981">git hash-object readme.md</span><span class="token template-string template-punctuation string" style="color:#10B981">`</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">				encoding</span><span class="token operator" style="color:#9CA3AF">:</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"utf-8"</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">				cwd</span><span class="token operator" style="color:#9CA3AF">:</span><span class="token plain"> tempDir</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token punctuation" style="color:#9CA3AF">}</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">trim</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> blobHash </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> hashOutput</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// ------------------------------------------------------------------</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// 2. Read the deflated blob content and inflate it using pako and split the content into `header`, `block 1` and `block 2`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// ------------------------------------------------------------------</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// Read the compressed blob file content</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> objectPath </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token function" style="color:#F59E0B">join</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">				tempDir</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">				</span><span class="token string" style="color:#10B981">".git"</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">				</span><span class="token string" style="color:#10B981">"objects"</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">				blobHash</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">slice</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token number" style="color:#EA580C">0</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token number" style="color:#EA580C">2</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">				blobHash</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">slice</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token number" style="color:#EA580C">2</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> compressedData </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> fs</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">readFileSync</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">objectPath</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// Use pako to inflate the blob file into a buffer</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> inflated </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> pako</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">inflate</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">compressedData</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> inflatedString </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token maybe-class-name">Buffer</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token keyword module" style="color:#3B82F6">from</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">inflated</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">toString</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"utf-8"</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// Verify the content matches "blob 12\0hello world"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// The format is: "blob &lt;size&gt;\0&lt;content&gt;"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// "hello world" is 12 bytes</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token console class-name" style="color:#F59E0B">console</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">log</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"inflatedString should be: 'blob 97\0Hi beautiful World, how you are doing on this beautiful World, what a beautiful World to live on.'\n"</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> inflatedString </span><span class="token operator" style="color:#9CA3AF">===</span><span class="token plain"> </span><span class="token string" style="color:#10B981">'blob 97\0Hi beautiful World, how you are doing on this beautiful World, what a beautiful World to live on.'</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token string" style="color:#10B981">''</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token function" style="color:#F59E0B">expect</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">inflatedString</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">toBe</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"blob 97\0Hi beautiful World, how you are doing on this beautiful World, what a beautiful World to live on."</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> header </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> inflatedString</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">split</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"\0"</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">[</span><span class="token number" style="color:#EA580C">0</span><span class="token punctuation" style="color:#9CA3AF">]</span><span class="token operator" style="color:#9CA3AF">!</span><span class="token plain"> </span><span class="token operator" style="color:#9CA3AF">+</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"\0"</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> block1 </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> inflatedString</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">substring</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">header</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">length</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> header</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">length</span><span class="token plain"> </span><span class="token operator" style="color:#9CA3AF">+</span><span class="token plain"> </span><span class="token number" style="color:#EA580C">61</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> block2 </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> inflatedString</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">substring</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">header</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">length</span><span class="token plain"> </span><span class="token operator" style="color:#9CA3AF">+</span><span class="token plain">  </span><span class="token number" style="color:#EA580C">61</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token function" style="color:#F59E0B">expect</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">header</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">toBe</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"blob 97\0"</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token console class-name" style="color:#F59E0B">console</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">log</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"header"</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> header</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token function" style="color:#F59E0B">expect</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">block1</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">toBe</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"Hi beautiful World, how you are doing on this beautiful World"</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token console class-name" style="color:#F59E0B">console</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">log</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"block1"</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> block1</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token function" style="color:#F59E0B">expect</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">block2</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">toBe</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">", what a beautiful World to live on."</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token console class-name" style="color:#F59E0B">console</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">log</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"block2"</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> block2</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain">	</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// ------------------------------------------------------------------</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// 3. Create a ChunkBlockDeflator and push the header, then push block 1, then push block 2 with `Z_FULL_FLUSH`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// ------------------------------------------------------------------</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> deflate </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token keyword" style="color:#3B82F6">new</span><span class="token plain"> </span><span class="token class-name maybe-class-name" style="color:#F59E0B">ChunkBlockDeflate</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token punctuation" style="color:#9CA3AF">{</span><span class="token plain"> level</span><span class="token operator" style="color:#9CA3AF">:</span><span class="token plain"> </span><span class="token number" style="color:#EA580C">6</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">}</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			deflate</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">pushChunk</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token maybe-class-name">Buffer</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token keyword module" style="color:#3B82F6">from</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">header</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"this-magic-parameter-comes-later"</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			deflate</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">pushChunk</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token maybe-class-name">Buffer</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token keyword module" style="color:#3B82F6">from</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">block1</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"this-magic-parameter-comes-later"</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			deflate</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">pushChunk</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token maybe-class-name">Buffer</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token keyword module" style="color:#3B82F6">from</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">block2</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"this-magic-parameter-comes-later"</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token boolean" style="color:#EA580C">true</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> deflationInfo </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> deflate</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">deflationInfo</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// ------------------------------------------------------------------</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// 4. Store the deflated result back into the blob</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// ------------------------------------------------------------------</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// remove the original blob file to make sure we are not just reading the same file again</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			fs</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">unlinkSync</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">objectPath</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// Write the block deflated blob</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			fs</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">writeFileSync</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">objectPath</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> deflate</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">result</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token function" style="color:#F59E0B">execSync</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token template-string template-punctuation string" style="color:#10B981">`</span><span class="token template-string string" style="color:#10B981">chmod 444 "</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#9CA3AF">${</span><span class="token template-string interpolation">objectPath</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#9CA3AF">}</span><span class="token template-string string" style="color:#10B981">"</span><span class="token template-string template-punctuation string" style="color:#10B981">`</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">{</span><span class="token plain"> encoding</span><span class="token operator" style="color:#9CA3AF">:</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"utf-8"</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">}</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// ------------------------------------------------------------------</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// 5. Check if git still reads the object</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// ------------------------------------------------------------------</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// Verify with git cat-file -p</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> catFileOutput </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token function" style="color:#F59E0B">execSync</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">				</span><span class="token template-string template-punctuation string" style="color:#10B981">`</span><span class="token template-string string" style="color:#10B981">git cat-file -p </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#9CA3AF">${</span><span class="token template-string interpolation">blobHash</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#9CA3AF">}</span><span class="token template-string template-punctuation string" style="color:#10B981">`</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">				</span><span class="token punctuation" style="color:#9CA3AF">{</span><span class="token plain"> encoding</span><span class="token operator" style="color:#9CA3AF">:</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"utf-8"</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> cwd</span><span class="token operator" style="color:#9CA3AF">:</span><span class="token plain"> tempDir </span><span class="token punctuation" style="color:#9CA3AF">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token function" style="color:#F59E0B">expect</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">compressedData</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">length</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">not</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">toBe</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">deflate</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">result</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">length</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> gitContent </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token maybe-class-name">Buffer</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token keyword module" style="color:#3B82F6">from</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">catFileOutput</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> gitContentStr </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> gitContent</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">toString</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"utf-8"</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token function" style="color:#F59E0B">expect</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">gitContentStr</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">toBe</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"Hi beautiful World, how you are doing on this beautiful World, what a beautiful World to live on."</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token console class-name" style="color:#F59E0B">console</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">log</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"gitContentStr should be: 'Hi beautiful World, how you are doing on this beautiful World, what a beautiful World to live on.'\n"</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> gitContentStr </span><span class="token operator" style="color:#9CA3AF">===</span><span class="token plain"> </span><span class="token string" style="color:#10B981">'Hi beautiful World, how you are doing on this beautiful World, what a beautiful World to live on.'</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token string" style="color:#10B981">''</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// ------------------------------------------------------------------</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// 6. Read the deflated bytes from block 2 and inflate them independently</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// ------------------------------------------------------------------</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> objectFileHandle </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> fs</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">openSync</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">objectPath</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token string" style="color:#10B981">'r'</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> block2Deflated </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token keyword" style="color:#3B82F6">new</span><span class="token plain"> </span><span class="token class-name known-class-name class-name" style="color:#F59E0B">Uint8Array</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">deflationInfo</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">blocks</span><span class="token punctuation" style="color:#9CA3AF">[</span><span class="token number" style="color:#EA580C">2</span><span class="token punctuation" style="color:#9CA3AF">]</span><span class="token operator" style="color:#9CA3AF">!</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">end</span><span class="token plain"> </span><span class="token operator" style="color:#9CA3AF">-</span><span class="token plain"> deflationInfo</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">blocks</span><span class="token punctuation" style="color:#9CA3AF">[</span><span class="token number" style="color:#EA580C">2</span><span class="token punctuation" style="color:#9CA3AF">]</span><span class="token operator" style="color:#9CA3AF">!</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">start</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			fs</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">readSync</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">objectFileHandle</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> block2Deflated</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token number" style="color:#EA580C">0</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> block2Deflated</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">length</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> deflationInfo</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">blocks</span><span class="token punctuation" style="color:#9CA3AF">[</span><span class="token number" style="color:#EA580C">2</span><span class="token punctuation" style="color:#9CA3AF">]</span><span class="token operator" style="color:#9CA3AF">!</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token property-access">start</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> inflatedBlock </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token maybe-class-name">BlockInflate</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">inflateBlockChunk</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">block2Deflated</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">				deflationInfo</span><span class="token operator" style="color:#9CA3AF">:</span><span class="token plain"> deflationInfo </span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token punctuation" style="color:#9CA3AF">}</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> block2DeflatedString </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token keyword" style="color:#3B82F6">new</span><span class="token plain"> </span><span class="token class-name maybe-class-name" style="color:#F59E0B">TextDecoder</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">decode</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">inflatedBlock </span><span class="token keyword module" style="color:#3B82F6">as</span><span class="token plain"> </span><span class="token known-class-name class-name" style="color:#F59E0B">Uint8Array</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token function" style="color:#F59E0B">expect</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">block2DeflatedString</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">toBe</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">", what a beautiful World to live on."</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token console class-name" style="color:#F59E0B">console</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">log</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"block2DeflatedString should be: ', what a beautiful World to live on.'\n"</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> block2DeflatedString </span><span class="token operator" style="color:#9CA3AF">===</span><span class="token plain"> </span><span class="token string" style="color:#10B981">', what a beautiful World to live on.'</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token string" style="color:#10B981">''</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">		</span><span class="token punctuation" style="color:#9CA3AF">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#3B82F6">finally</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token comment" style="color:#6B7280">// Clean up temp directory</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">			</span><span class="token function" style="color:#F59E0B">execSync</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token template-string template-punctuation string" style="color:#10B981">`</span><span class="token template-string string" style="color:#10B981">rm -rf "</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#9CA3AF">${</span><span class="token template-string interpolation">tempDir</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#9CA3AF">}</span><span class="token template-string string" style="color:#10B981">"</span><span class="token template-string template-punctuation string" style="color:#10B981">`</span><span class="token punctuation" style="color:#9CA3AF">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">{</span><span class="token plain"> encoding</span><span class="token operator" style="color:#9CA3AF">:</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"utf-8"</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">}</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">		</span><span class="token punctuation" style="color:#9CA3AF">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">	</span><span class="token punctuation" style="color:#9CA3AF">}</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"></span><span class="token punctuation" style="color:#9CA3AF">}</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span></code></pre></div></div>
</summary><div><div class="collapsibleContent_ocAz">
<!-- -->
<div class="language-text codeBlockContainer_huE5 theme-code-block" style="--prism-background-color:#F3F4F6;--prism-color:#1F2937"><div class="codeBlockContent_oko_"><pre tabindex="0" class="prism-code language-text codeBlock_BBgU thin-scrollbar" style="background-color:#F3F4F6;color:#1F2937"><code class="codeBlockLines_t9f9"><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">inflatedString should be: 'blob 97Hi beautiful World, how you are doing on this beautiful World, what a beautiful World to live on.'</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"> true </span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">header blob 97</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">block1 Hi beautiful World, how you are doing on this beautiful World</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">block2 , what a beautiful World to live on.</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">gitContentStr should be: 'Hi beautiful World, how you are doing on this beautiful World, what a beautiful World to live on.'</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"> true </span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">block2DeflatedString should be: ', what a beautiful World to live on.'</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"> true </span><br></span></code></pre></div></div>
</div></div></details>
</div></div></details>
<br>
<p>The full text being printed by <code>git cat-file</code> followed by the second block being partially inflated showing <code>, what a beautiful World to live on.</code></p>
<p><strong>WHAAAAAAT? IT WORKS!</strong> We successfully recompressed a loose object in a way which 1) is git compatible and 2) allows us to read subsets of the file without touching the rest of the file.</p>
<blockquote>
<p>"Down, and down, and down, till she began to wonder if she was going right through the World, so as to come out on the other side!"</p>
</blockquote>
<p>This is a good <strong>start</strong>, now that we can access subsets of the file we need to find a way to give sqlite access to those objects - sqlite can't just read those Git loose objects it expects the content of the sqlite file to be decompressed.</p>
<p>SQLite has the concept of a <a href="https://sqlite.org/vfs.html" target="_blank" rel="noopener noreferrer" class="">VFS</a> which allows us to transform data from the format, sqlite expects to the data layout on disc.</p>
<p>In the upcoming article we're gonna find a little bottle labeled "DRINK ME" - we're gonna grow the data back into the format and size so it suits sqlite with the help of a virtual filesystem.</p>
<!-- -->
<section data-footnotes="" class="footnotes"><h2 class="anchor anchorTargetStickyNavbar_l_sM sr-only" id="footnote-label">Footnotes<a href="https://blog.lysk.tech/sqlite-on-git-part-2#footnote-label" class="hash-link" aria-label="Direct link to Footnotes" title="Direct link to Footnotes" translate="no">​</a></h2>
<ol>
<li class="anchorTargetStickyNavbar_l_sM" id="user-content-fn-1-535a6d">
<p>Thanks <a href="https://invisible.college/@toomim" target="_blank" rel="noopener noreferrer" class="">Mike Toomim</a> for the grisp problem statment <a href="https://blog.lysk.tech/sqlite-on-git-part-2#user-content-fnref-1-535a6d" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>]]></content:encoded>
            <category>git</category>
            <category>zlib</category>
            <category>compression</category>
            <category>Z_FULL_FLUSH</category>
        </item>
        <item>
            <title><![CDATA[How I manage Images for my Blog]]></title>
            <link>https://blog.lysk.tech/excalidraw-frame-export</link>
            <guid>https://blog.lysk.tech/excalidraw-frame-export</guid>
            <pubDate>Sun, 29 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[TL;DR I use an Excalidraw, wrap the elements of interest with a frame, name it with export_ prefix, my forked excalidraw extension automatically generates SVGs for light and dark mode.]]></description>
            <content:encoded><![CDATA[<p><em>TL;DR</em> I use an Excalidraw, wrap the elements of interest with a frame, name it with <code>export_</code> prefix, my forked <a href="https://github.com/martin-lysk/excalidraw-vscode" target="_blank" rel="noopener noreferrer" class="">excalidraw extension</a> automatically generates SVGs for light and dark mode.</p>
<p><img decoding="async" loading="lazy" alt="export" src="https://blog.lysk.tech/assets/images/export.dark.exp-252e0eacdb94f7d74ecc629f7c4ea4da.svg#gh-dark-mode-only" width="884" height="531" class="img_VXcA">
<img decoding="async" loading="lazy" alt="export" src="https://blog.lysk.tech/assets/images/export.light.exp-9ea347e12c4c7c5d619086eb6016a934.svg#gh-light-mode-only" width="884" height="531" class="img_VXcA"></p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="using-excalidraw">Using Excalidraw<a href="https://blog.lysk.tech/excalidraw-frame-export#using-excalidraw" class="hash-link" aria-label="Direct link to Using Excalidraw" title="Direct link to Using Excalidraw" translate="no">​</a></h2>
<p>I used Excalidraw a lot in the past.</p>
<ol>
<li class="">When breaking down a technical problem for myself</li>
<li class="">When explaining a concept or an architecture to my coworkers.</li>
</ol>
<p>Just recently a new usecase evolved.</p>
<ol start="3">
<li class="">Expressing my thoughts in my Blog.</li>
</ol>
<p>While writing my <a href="https://blog.lysk.tech/nfs3-event-side-channel" target="_blank" rel="noopener noreferrer" class="">first article</a> the dependency between graphics and the text lead to a lot frustration. Fine-tuning the graphic led to an easier text. Changes in the text made me realize that some information in the graphic is not needed to grasp what should land.</p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="the-problem">The Problem<a href="https://blog.lysk.tech/excalidraw-frame-export#the-problem" class="hash-link" aria-label="Direct link to The Problem" title="Direct link to The Problem" translate="no">​</a></h2>
<p>Every change in a graphic in Excalidraw meant 9 clicks in Excalidraw.</p>
<ol>
<li class="">Selecting the frame</li>
<li class="">pressing export</li>
<li class="">choose the right name + darkmode/lightmode postfix</li>
<li class="">export</li>
<li class="">switch light/dark mode</li>
<li class="">choose the right name + darkmode/lightmode postfix</li>
<li class="">export again</li>
<li class="">realize that one label crossed the frame boundary</li>
<li class="">starting at 1 again.</li>
</ol>
<p>It took me about 45 seconds.</p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="the-solution">The solution<a href="https://blog.lysk.tech/excalidraw-frame-export#the-solution" class="hash-link" aria-label="Direct link to The solution" title="Direct link to The solution" translate="no">​</a></h2>
<p>Automate it :-) .</p>
<p><img decoding="async" loading="lazy" src="https://imgs.xkcd.com/comics/the_general_problem.png" alt="FS events - client side write" class="img_VXcA"><br>
Image © <a href="https://xkcd.com/" target="_blank" rel="noopener noreferrer" class="">Randall Munroe, XKCD.com</a></p>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="first-approach---the-github-action">First approach - the GitHub action<a href="https://blog.lysk.tech/excalidraw-frame-export#first-approach---the-github-action" class="hash-link" aria-label="Direct link to First approach - the GitHub action" title="Direct link to First approach - the GitHub action" translate="no">​</a></h3>
<p>...20 minutes later...
A bit of bash thanks to open source (specifically <a href="https://github.com/JonRC/excalirender" target="_blank" rel="noopener noreferrer" class="">JonRC's excalirender</a>) - it worked...</p>
<p>A little GitHub action that:</p>
<ol>
<li class="">looks for changed excalidraw files in the last push,</li>
<li class="">uses jq to find frames inside of those,</li>
<li class="">exports them in dark and light mode as [framename]-[light/dark],</li>
<li class="">commits those new svg files to the repo again.</li>
</ol>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>See code here:
</summary><div><div class="collapsibleContent_ocAz">
<!-- -->
<div class="language-bash codeBlockContainer_huE5 theme-code-block" style="--prism-background-color:#F3F4F6;--prism-color:#1F2937"><div class="codeBlockContent_oko_"><pre tabindex="0" class="prism-code language-bash codeBlock_BBgU thin-scrollbar" style="background-color:#F3F4F6;color:#1F2937"><code class="codeBlockLines_t9f9"><span class="token-line" style="color:#1F2937"><span class="token plain">name: Export Excalidraw Frames</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">on:</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">  push:</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">    branches:</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">      - main</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">  pull_request:</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">    branches:</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">      - main</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">permissions:</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">  contents: </span><span class="token function" style="color:#F59E0B">write</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">jobs:</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">  export-frames:</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">    runs-on: ubuntu-latest</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">    steps:</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">      - name: Checkout repository</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">        uses: actions/checkout@v4</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">        with:</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          fetch-depth: </span><span class="token number" style="color:#EA580C">2</span><span class="token plain">  </span><span class="token comment" style="color:#6B7280"># Fetch last 2 commits to compare with HEAD~1</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">      - name: Get changed Excalidraw files</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">        id: changed-files</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">        run: </span><span class="token operator" style="color:#9CA3AF">|</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token comment" style="color:#6B7280"># Get list of changed .excalidraw files since last commit</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token keyword" style="color:#3B82F6">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">[</span><span class="token punctuation" style="color:#9CA3AF">[</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">${{ github.event_name }</span><span class="token string" style="color:#10B981">}"</span><span class="token plain"> </span><span class="token operator" style="color:#9CA3AF">==</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"push"</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">]</span><span class="token punctuation" style="color:#9CA3AF">]</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"> </span><span class="token keyword" style="color:#3B82F6">then</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token assign-left variable" style="color:#3B82F6">CHANGED_FILES</span><span class="token operator" style="color:#9CA3AF">=</span><span class="token variable" style="color:#3B82F6">$(</span><span class="token variable function" style="color:#F59E0B">git</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable function" style="color:#F59E0B">diff</span><span class="token variable" style="color:#3B82F6"> --name-only HEAD~1 HEAD </span><span class="token variable operator" style="color:#9CA3AF">|</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable function" style="color:#F59E0B">grep</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable string" style="color:#10B981">'\.excalidraw$'</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable operator" style="color:#9CA3AF">||</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable boolean" style="color:#EA580C">true</span><span class="token variable" style="color:#3B82F6">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token keyword" style="color:#3B82F6">else</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token comment" style="color:#6B7280"># For PRs, compare with target branch</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token assign-left variable" style="color:#3B82F6">CHANGED_FILES</span><span class="token operator" style="color:#9CA3AF">=</span><span class="token variable" style="color:#3B82F6">$(</span><span class="token variable function" style="color:#F59E0B">git</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable function" style="color:#F59E0B">diff</span><span class="token variable" style="color:#3B82F6"> --name-only origin/$</span><span class="token variable punctuation" style="color:#9CA3AF">{</span><span class="token variable punctuation" style="color:#9CA3AF">{</span><span class="token variable" style="color:#3B82F6"> github.base_ref </span><span class="token variable punctuation" style="color:#9CA3AF">}</span><span class="token variable punctuation" style="color:#9CA3AF">}</span><span class="token variable" style="color:#3B82F6"> HEAD </span><span class="token variable operator" style="color:#9CA3AF">|</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable function" style="color:#F59E0B">grep</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable string" style="color:#10B981">'\.excalidraw$'</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable operator" style="color:#9CA3AF">||</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable boolean" style="color:#EA580C">true</span><span class="token variable" style="color:#3B82F6">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token keyword" style="color:#3B82F6">fi</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token keyword" style="color:#3B82F6">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">[</span><span class="token plain"> </span><span class="token parameter variable" style="color:#3B82F6">-z</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">$CHANGED_FILES</span><span class="token string" style="color:#10B981">"</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">]</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"> </span><span class="token keyword" style="color:#3B82F6">then</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token builtin class-name" style="color:#F59E0B">echo</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"No changed .excalidraw files found"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token builtin class-name" style="color:#F59E0B">echo</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"has_changes=false"</span><span class="token plain"> </span><span class="token operator" style="color:#9CA3AF">&gt;&gt;</span><span class="token plain"> </span><span class="token variable" style="color:#3B82F6">$GITHUB_OUTPUT</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token keyword" style="color:#3B82F6">else</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token builtin class-name" style="color:#F59E0B">echo</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"Changed files:"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token builtin class-name" style="color:#F59E0B">echo</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">$CHANGED_FILES</span><span class="token string" style="color:#10B981">"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token builtin class-name" style="color:#F59E0B">echo</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">$CHANGED_FILES</span><span class="token string" style="color:#10B981">"</span><span class="token plain"> </span><span class="token operator" style="color:#9CA3AF">&gt;</span><span class="token plain"> /tmp/changed_files.txt</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token builtin class-name" style="color:#F59E0B">echo</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"has_changes=true"</span><span class="token plain"> </span><span class="token operator" style="color:#9CA3AF">&gt;&gt;</span><span class="token plain"> </span><span class="token variable" style="color:#3B82F6">$GITHUB_OUTPUT</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token keyword" style="color:#3B82F6">fi</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">      - name: Install excalirender</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">        if: steps.changed-files.outputs.has_changes </span><span class="token operator" style="color:#9CA3AF">==</span><span class="token plain"> </span><span class="token string" style="color:#10B981">'true'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">        run: </span><span class="token operator" style="color:#9CA3AF">|</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token function" style="color:#F59E0B">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#3B82F6">-fsSL</span><span class="token plain"> https://raw.githubusercontent.com/JonRC/excalirender/main/install.sh </span><span class="token operator" style="color:#9CA3AF">|</span><span class="token plain"> </span><span class="token function" style="color:#F59E0B">sh</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token builtin class-name" style="color:#F59E0B">echo</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"</span><span class="token string environment constant" style="color:#EA580C">$HOME</span><span class="token string" style="color:#10B981">/.local/bin"</span><span class="token plain"> </span><span class="token operator" style="color:#9CA3AF">&gt;&gt;</span><span class="token plain"> </span><span class="token variable" style="color:#3B82F6">$GITHUB_PATH</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          excalirender </span><span class="token parameter variable" style="color:#3B82F6">--version</span><span class="token plain"> </span><span class="token operator" style="color:#9CA3AF">||</span><span class="token plain"> </span><span class="token builtin class-name" style="color:#F59E0B">echo</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"excalirender installed"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">      - name: Export frames </span><span class="token keyword" style="color:#3B82F6">for</span><span class="token plain"> changed files</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">        if: steps.changed-files.outputs.has_changes </span><span class="token operator" style="color:#9CA3AF">==</span><span class="token plain"> </span><span class="token string" style="color:#10B981">'true'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">        run: </span><span class="token operator" style="color:#9CA3AF">|</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token comment" style="color:#6B7280"># Create export script</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token function" style="color:#F59E0B">cat</span><span class="token plain"> </span><span class="token operator" style="color:#9CA3AF">&gt;</span><span class="token plain"> /tmp/export_frames.sh </span><span class="token operator" style="color:#9CA3AF">&lt;&lt;</span><span class="token plain"> </span><span class="token string" style="color:#10B981">'EOF'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token comment" style="color:#6B7280">#!/bin/bash</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token assign-left variable" style="color:#3B82F6">EXCALIDRAW_FILE</span><span class="token operator" style="color:#9CA3AF">=</span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">$1</span><span class="token string" style="color:#10B981">"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token assign-left variable" style="color:#3B82F6">OUTPUT_DIR</span><span class="token operator" style="color:#9CA3AF">=</span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">$(</span><span class="token string variable function" style="color:#F59E0B">dirname</span><span class="token string variable" style="color:#3B82F6"> </span><span class="token string variable string" style="color:#10B981">"</span><span class="token string variable string variable" style="color:#3B82F6">$EXCALIDRAW_FILE</span><span class="token string variable string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">)</span><span class="token string" style="color:#10B981">"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token comment" style="color:#6B7280"># Read the Excalidraw file and extract frame names</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token assign-left variable" style="color:#3B82F6">FRAME_NAMES</span><span class="token operator" style="color:#9CA3AF">=</span><span class="token variable" style="color:#3B82F6">$(</span><span class="token variable" style="color:#3B82F6">jq </span><span class="token variable parameter variable" style="color:#3B82F6">-r</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable string" style="color:#10B981">'.elements[] | select(.type == "frame") | .name // "frame-" + .id'</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable string" style="color:#10B981">"</span><span class="token variable string variable" style="color:#3B82F6">$EXCALIDRAW_FILE</span><span class="token variable string" style="color:#10B981">"</span><span class="token variable" style="color:#3B82F6">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token keyword" style="color:#3B82F6">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">[</span><span class="token plain"> </span><span class="token parameter variable" style="color:#3B82F6">-z</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">$FRAME_NAMES</span><span class="token string" style="color:#10B981">"</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">]</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"> </span><span class="token keyword" style="color:#3B82F6">then</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token builtin class-name" style="color:#F59E0B">echo</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"No frames found in </span><span class="token string variable" style="color:#3B82F6">$EXCALIDRAW_FILE</span><span class="token string" style="color:#10B981">"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token builtin class-name" style="color:#F59E0B">exit</span><span class="token plain"> </span><span class="token number" style="color:#EA580C">0</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token keyword" style="color:#3B82F6">fi</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token builtin class-name" style="color:#F59E0B">echo</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"Exporting frames from </span><span class="token string variable" style="color:#3B82F6">$EXCALIDRAW_FILE</span><span class="token string" style="color:#10B981">"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token comment" style="color:#6B7280"># Export each frame in light and dark mode</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token keyword" style="color:#3B82F6">while</span><span class="token plain"> </span><span class="token assign-left variable environment constant" style="color:#EA580C">IFS</span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token builtin class-name" style="color:#F59E0B">read</span><span class="token plain"> </span><span class="token parameter variable" style="color:#3B82F6">-r</span><span class="token plain"> frame_name</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"> </span><span class="token keyword" style="color:#3B82F6">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token keyword" style="color:#3B82F6">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">[</span><span class="token plain"> </span><span class="token parameter variable" style="color:#3B82F6">-n</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">$frame_name</span><span class="token string" style="color:#10B981">"</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">]</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"> </span><span class="token keyword" style="color:#3B82F6">then</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">              </span><span class="token builtin class-name" style="color:#F59E0B">echo</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"  Exporting frame: </span><span class="token string variable" style="color:#3B82F6">$frame_name</span><span class="token string" style="color:#10B981">"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">              </span><span class="token comment" style="color:#6B7280"># Sanitize frame name for use in filename</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">              </span><span class="token assign-left variable" style="color:#3B82F6">safe_name</span><span class="token operator" style="color:#9CA3AF">=</span><span class="token variable" style="color:#3B82F6">$(</span><span class="token variable builtin class-name" style="color:#F59E0B">echo</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable string" style="color:#10B981">"</span><span class="token variable string variable" style="color:#3B82F6">$frame_name</span><span class="token variable string" style="color:#10B981">"</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable operator" style="color:#9CA3AF">|</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable function" style="color:#F59E0B">sed</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable string" style="color:#10B981">'s/[&lt;&gt;:"/\\|?*]/-/g'</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable operator" style="color:#9CA3AF">|</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable function" style="color:#F59E0B">sed</span><span class="token variable" style="color:#3B82F6"> </span><span class="token variable string" style="color:#10B981">'s/\s+/-/g'</span><span class="token variable" style="color:#3B82F6">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">              </span><span class="token comment" style="color:#6B7280"># Export light mode</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">              excalirender </span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">$EXCALIDRAW_FILE</span><span class="token string" style="color:#10B981">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#3B82F6">--frame</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">$frame_name</span><span class="token string" style="color:#10B981">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#3B82F6">-o</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">${OUTPUT_DIR}</span><span class="token string" style="color:#10B981">/</span><span class="token string variable" style="color:#3B82F6">${safe_name}</span><span class="token string" style="color:#10B981">-light.svg"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">              </span><span class="token comment" style="color:#6B7280"># Export dark mode</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">              excalirender </span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">$EXCALIDRAW_FILE</span><span class="token string" style="color:#10B981">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#3B82F6">--frame</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">$frame_name</span><span class="token string" style="color:#10B981">"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#3B82F6">--dark</span><span class="token plain"> </span><span class="token parameter variable" style="color:#3B82F6">-o</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">${OUTPUT_DIR}</span><span class="token string" style="color:#10B981">/</span><span class="token string variable" style="color:#3B82F6">${safe_name}</span><span class="token string" style="color:#10B981">-dark.svg"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token keyword" style="color:#3B82F6">fi</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token keyword" style="color:#3B82F6">done</span><span class="token plain"> </span><span class="token operator" style="color:#9CA3AF">&lt;&lt;&lt;</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">$FRAME_NAMES</span><span class="token string" style="color:#10B981">"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token builtin class-name" style="color:#F59E0B">echo</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"  ✓ Exported all frames from </span><span class="token string variable" style="color:#3B82F6">$EXCALIDRAW_FILE</span><span class="token string" style="color:#10B981">"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          EOF</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token function" style="color:#F59E0B">chmod</span><span class="token plain"> +x /tmp/export_frames.sh</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token comment" style="color:#6B7280"># Process each changed file</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token keyword" style="color:#3B82F6">while</span><span class="token plain"> </span><span class="token assign-left variable environment constant" style="color:#EA580C">IFS</span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token builtin class-name" style="color:#F59E0B">read</span><span class="token plain"> </span><span class="token parameter variable" style="color:#3B82F6">-r</span><span class="token plain"> </span><span class="token function" style="color:#F59E0B">file</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"> </span><span class="token keyword" style="color:#3B82F6">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token keyword" style="color:#3B82F6">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">[</span><span class="token plain"> </span><span class="token parameter variable" style="color:#3B82F6">-n</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">$file</span><span class="token string" style="color:#10B981">"</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">]</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"> </span><span class="token keyword" style="color:#3B82F6">then</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">              </span><span class="token builtin class-name" style="color:#F59E0B">echo</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"Processing: </span><span class="token string variable" style="color:#3B82F6">$file</span><span class="token string" style="color:#10B981">"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">              /tmp/export_frames.sh </span><span class="token string" style="color:#10B981">"</span><span class="token string variable" style="color:#3B82F6">$file</span><span class="token string" style="color:#10B981">"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token keyword" style="color:#3B82F6">fi</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token keyword" style="color:#3B82F6">done</span><span class="token plain"> </span><span class="token operator" style="color:#9CA3AF">&lt;</span><span class="token plain"> /tmp/changed_files.txt</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">      - name: Commit exported SVGs</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">        if: steps.changed-files.outputs.has_changes </span><span class="token operator" style="color:#9CA3AF">==</span><span class="token plain"> </span><span class="token string" style="color:#10B981">'true'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">        run: </span><span class="token operator" style="color:#9CA3AF">|</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token function" style="color:#F59E0B">git</span><span class="token plain"> config </span><span class="token parameter variable" style="color:#3B82F6">--local</span><span class="token plain"> user.email </span><span class="token string" style="color:#10B981">"github-actions[bot]@users.noreply.github.com"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token function" style="color:#F59E0B">git</span><span class="token plain"> config </span><span class="token parameter variable" style="color:#3B82F6">--local</span><span class="token plain"> user.name </span><span class="token string" style="color:#10B981">"github-actions[bot]"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token comment" style="color:#6B7280"># Add all newly created SVG files</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token function" style="color:#F59E0B">git</span><span class="token plain"> </span><span class="token function" style="color:#F59E0B">add</span><span class="token plain"> **/*.svg </span><span class="token operator file-descriptor important" style="color:#EC4899">2</span><span class="token operator" style="color:#9CA3AF">&gt;</span><span class="token plain">/dev/null </span><span class="token operator" style="color:#9CA3AF">||</span><span class="token plain"> </span><span class="token function" style="color:#F59E0B">git</span><span class="token plain"> </span><span class="token function" style="color:#F59E0B">add</span><span class="token plain"> *.svg </span><span class="token operator file-descriptor important" style="color:#EC4899">2</span><span class="token operator" style="color:#9CA3AF">&gt;</span><span class="token plain">/dev/null </span><span class="token operator" style="color:#9CA3AF">||</span><span class="token plain"> </span><span class="token boolean" style="color:#EA580C">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token comment" style="color:#6B7280"># Check if there are any changes to commit</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token keyword" style="color:#3B82F6">if</span><span class="token plain"> </span><span class="token function" style="color:#F59E0B">git</span><span class="token plain"> </span><span class="token function" style="color:#F59E0B">diff</span><span class="token plain"> </span><span class="token parameter variable" style="color:#3B82F6">--staged</span><span class="token plain"> --quiet</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"> </span><span class="token keyword" style="color:#3B82F6">then</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token builtin class-name" style="color:#F59E0B">echo</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"No new SVG files to commit"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token keyword" style="color:#3B82F6">else</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token builtin class-name" style="color:#F59E0B">echo</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"Committing exported SVG files"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token function" style="color:#F59E0B">git</span><span class="token plain"> commit </span><span class="token parameter variable" style="color:#3B82F6">-m</span><span class="token plain"> </span><span class="token string" style="color:#10B981">"chore: export Excalidraw frames as SVGs</span><br></span><span class="token-line" style="color:#1F2937"><span class="token string" style="color:#10B981"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token string" style="color:#10B981">            - Exported frames from changed .excalidraw files</span><br></span><span class="token-line" style="color:#1F2937"><span class="token string" style="color:#10B981">            - Generated light and dark mode variants</span><br></span><span class="token-line" style="color:#1F2937"><span class="token string" style="color:#10B981"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token string" style="color:#10B981">            🤖 Generated with [Claude Code](https://claude.com/claude-code)</span><br></span><span class="token-line" style="color:#1F2937"><span class="token string" style="color:#10B981"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token string" style="color:#10B981">            Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">            </span><span class="token function" style="color:#F59E0B">git</span><span class="token plain"> push</span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain">          </span><span class="token keyword" style="color:#3B82F6">fi</span><br></span></code></pre></div></div>
</div></div></details>
<p>Awesome! Enough to continued working on my article.</p>
<h4 class="anchor anchorTargetStickyNavbar_l_sM" id="problems-with-approach-1">Problems with approach 1<a href="https://blog.lysk.tech/excalidraw-frame-export#problems-with-approach-1" class="hash-link" aria-label="Direct link to Problems with approach 1" title="Direct link to Problems with approach 1" translate="no">​</a></h4>
<p>After working with this approach for some time I faced various issues.</p>
<ol>
<li class="">The library I used had some rendering bugs (same as <a href="https://github.com/Timmmm/excalidraw_export/issues/6" target="_blank" rel="noopener noreferrer" class="">this</a> one)</li>
<li class="">The process involved spinning up an x86 based docker image I couldn't get running on my ARM-based Mac</li>
</ol>
<p>I circumvented 1.) with additional labels added but 2.) broke the whole concept. Not being able to run the export locally meant I needed to push the Excalidraw file to GitHub, wait for the pipeline to finish, and pull the new commit before I could see new images or changes in images reflected.</p>
<p>So the solution kind of worked but reviewing the blog post locally was only possible with outdated images.</p>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="a-new-idea-add-auto-export-to-excalidraw">A new Idea: Add auto-export to Excalidraw<a href="https://blog.lysk.tech/excalidraw-frame-export#a-new-idea-add-auto-export-to-excalidraw" class="hash-link" aria-label="Direct link to A new Idea: Add auto-export to Excalidraw" title="Direct link to A new Idea: Add auto-export to Excalidraw" translate="no">​</a></h3>
<p>What if Excalidraw's VSCode extension would check the open *.excalidraw file for changes and automatically export each frame as two separate SVG files - one in dark mode, one in light mode?</p>
<p>I took some time with Claude over the weekend to YOLO code. The result:</p>
<p>If I edit my Excalidraw in VSCode, all I need to do to make a section available for my blog post:</p>
<ol>
<li class="">wrap the elements with a frame</li>
<li class="">name the frame like <code>export_${image_name}</code></li>
</ol>
<p>The extension will pick up the frame, export it as SVG in dark and light mode, and save two SVGs named <code>${image_name}.light.exp.svg</code> and <code>${image_name}.dark.exp.svg</code> next to the Excalidraw file.</p>
<h4 class="anchor anchorTargetStickyNavbar_l_sM" id="live-preview-locally">Live preview locally<a href="https://blog.lysk.tech/excalidraw-frame-export#live-preview-locally" class="hash-link" aria-label="Direct link to Live preview locally" title="Direct link to Live preview locally" translate="no">​</a></h4>
<p>Now that those images are available locally and update whenever I change a frame in my Excalidraw, I can reference them via auto-complete and preview in the editor, see them rendered in the Preview tab.</p>
<p><img decoding="async" loading="lazy" alt="export" src="https://blog.lysk.tech/assets/images/use_exported_images.dark.exp-85113bdc0a31376a46189505f3f9cab1.svg#gh-dark-mode-only" width="1071" height="533" class="img_VXcA">
<img decoding="async" loading="lazy" alt="export" src="https://blog.lysk.tech/assets/images/use_exported_images.light.exp-c445da3239c1ad6a4cfbdddefa04e124.svg#gh-light-mode-only" width="1071" height="533" class="img_VXcA"></p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="conclusion">Conclusion<a href="https://blog.lysk.tech/excalidraw-frame-export#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>I am pretty happy with the result. I spent only a couple of hours including this writeup. Using the tool brings joy since it solves a real pain.</p>
<p>I can't wait to use it extensively in the articles in the making - <a href="https://blog.lysk.tech/sqlite-on-git-prologue" target="_blank" rel="noopener noreferrer" class="">SQLite on Git</a>.</p>
<p>One thing I'm not sure about, though. After talking to others about this approach I could see my approach bringing value to the original Excalidraw extension itself. But I wouldn't create a pull request - since I don't own the code - or rather, I don't want to take ownership. I'm thinking to open an issue, describe the problem and the solution to serve as inspiration instead.</p>
<p>If others find this useful and play around with it - I created artifacts for the release section in my <a href="https://github.com/martin-lysk/excalidraw-vscode" target="_blank" rel="noopener noreferrer" class="">GitHub fork</a> that allows others to download and use my extension. For now, that's enough!</p>]]></content:encoded>
            <category>blog</category>
            <category>excalidraw</category>
            <category>vscode</category>
        </item>
        <item>
            <title><![CDATA[SQLite on Git, Part I: The .git folder - Falling down the Rabbithole]]></title>
            <link>https://blog.lysk.tech/sqlite-on-git-part-1</link>
            <guid>https://blog.lysk.tech/sqlite-on-git-part-1</guid>
            <pubDate>Tue, 24 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Join me on a jump right into the rabbit hole on what we find when we look closer at the .git folder.]]></description>
            <content:encoded><![CDATA[<p>Join me on a jump right into the rabbit hole on what we find when we look closer at the .git folder.
We'll explore how git stores your code in <code>loose objects</code> and discover a flag in a 30yr old library used by git that could let us run databases inside git.</p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="what-has-happened-so-far">What has happened so far<a href="https://blog.lysk.tech/sqlite-on-git-part-1#what-has-happened-so-far" class="hash-link" aria-label="Direct link to What has happened so far" title="Direct link to What has happened so far" translate="no">​</a></h2>
<p>In the <a class="" href="https://blog.lysk.tech/sqlite-on-git-prologue"><strong>prologue</strong></a>, I explained why random access matters—the ability to read specific parts of files stored in Git without decompressing the entire thing—which is essential for running SQLite databases on top of Git's storage.</p>
<p>Now that we have done the sane reasoning part - let's go crazy.</p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="how-git-stores-your-data---the-git-folder">How git stores your data - the .git folder<a href="https://blog.lysk.tech/sqlite-on-git-part-1#how-git-stores-your-data---the-git-folder" class="hash-link" aria-label="Direct link to How git stores your data - the .git folder" title="Direct link to How git stores your data - the .git folder" translate="no">​</a></h2>
<img decoding="async" loading="lazy" align="right" width="250" src="https://upload.wikimedia.org/wikipedia/commons/4/42/The_White_Rabbit_%28Tenniel%29_-_The_Nursery_Alice_%281890%29_-_BL.jpg" class="img_VXcA">
<p>Whether you've been using git for 6 months or 6 years, you've probably never looked inside the <code>.git</code> folder. Let's change that.</p>
<p>I assume that you're using Git already. If you want to learn how to use it - you may want to check out <a href="https://git-scm.com/learn" target="_blank" rel="noopener noreferrer" class="">https://git-scm.com/learn</a> or <a href="https://www.youtube.com/watch?v=rH3zE7VlIMs" target="_blank" rel="noopener noreferrer" class="">Git Full Course</a> first. But what you're most likely less familiar with is the <code>.git</code> folder that lives in all your repos.</p>
<p>The following section will describe the parts of the .git data structure needed to understand the technique used to enable Random Access. It is inspired by the great work of the <a href="https://git-scm.com/book/en/v2/Git-Internals-Git-Objects" target="_blank" rel="noopener noreferrer" class="">pro git book</a> - by Scott Chacon. The parts excerpted and described here should be sufficient to follow me.</p>
<p>If you are firm with git's underlying data structure already - feel free to jump ahead to the compression section.</p>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="loose-objects---a-snapshot-of-the-content-of-the-file">Loose objects - a snapshot of the content of the file<a href="https://blog.lysk.tech/sqlite-on-git-part-1#loose-objects---a-snapshot-of-the-content-of-the-file" class="hash-link" aria-label="Direct link to Loose objects - a snapshot of the content of the file" title="Direct link to Loose objects - a snapshot of the content of the file" translate="no">​</a></h3>
<p>One misconception I had for a long time: Git <a href="https://git-scm.com/book/en/v2/Getting-Started-What-is-Git%3F?.html#_snapshots_not_differences" target="_blank" rel="noopener noreferrer" class="">does NOT store differences, it stores snapshots</a>. What does that mean? Whenever you do a commit, git has a copy of the files content at the point of commit.</p>
<p>Git creates the snapshot to its internal database in the moment when you add the file to the staging area - the action you do before you commit.</p>
<p>It does that by creating so-called <code>loose objects</code>. You find them inside of <code>.git/objects/</code></p>
<p>So let's play this through - follow those steps:</p>
<ol>
<li class="">We create an empty folder</li>
</ol>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
<p><code>mkdir my_ducktale_test_repo</code></p>
</summary><div><div class="collapsibleContent_ocAz">
</div></div></details>
<ol start="2">
<li class="">We change working directory into that folder</li>
</ol>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
<p>cd my_ducktale_test_repo</p>
</summary><div><div class="collapsibleContent_ocAz">
</div></div></details>
<ol start="3">
<li class="">We create a new repo</li>
</ol>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
<p><code>git init</code></p>
</summary><div><div class="collapsibleContent_ocAz">
Initialized empty Git repository in /Users/your_user/git_temp/.git/
</div></div></details>
<ol start="4">
<li class="">Let's look into the repo's <code>.git</code> folder - more specifically into its internal objects folder</li>
</ol>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
<p><code>ls -1 .git/objects</code></p>
</summary><div><div class="collapsibleContent_ocAz">
<p>info<br>
pack</p>
</div></div></details>
<p>As you can see, there are no objects yet - only the <code>info</code> and <code>pack</code> directories which are empty placeholders.</p>
<ol start="5">
<li class="">Create a <strong>readme.md</strong> file containing <em>Hello World</em></li>
</ol>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
<p><code>echo -n "Hello World" &gt; readme.md</code></p>
</summary><div><div class="collapsibleContent_ocAz">
<p><em>(The <code>-n</code> flag prevents adding a newline, which would change the hash)</em></p>
</div></div></details>
<p>You now have a fresh repo with one file that git doesn't know about just yet. A look into the current <code>.git</code> folder shows only two folders for now.</p>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
<p><code>ls -1 .git/objects</code></p>
</summary><div><div class="collapsibleContent_ocAz">
<p>info<br>
pack</p>
</div></div></details>
<ol start="6">
<li class="">To make git aware of the readme file we added before we have to add it to git:</li>
</ol>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
<p><code>git add readme.md</code></p>
</summary><div><div class="collapsibleContent_ocAz">
<p><em>(No output - git add runs silently)</em></p>
</div></div></details>
<ol start="7">
<li class="">Now let's take a look at that objects folder again.</li>
</ol>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
<p><code>ls -1 .git/objects</code></p>
</summary><div><div class="collapsibleContent_ocAz">
<p>5e<br>
info<br>
pack</p>
</div></div></details>
<p>We see that a new folder <code>5e</code> was created - let's look into that one as well.</p>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
<p><code>ls -1 .git/objects/5e</code></p>
</summary><div><div class="collapsibleContent_ocAz">
<p>1c309dae7f45e0f39b1bf3ac3cd9db12e7d689</p>
</div></div></details>
<p>What is that weirdly named file? Let's look into it.</p>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
<p><code>cat .git/objects/5e/1c309dae7f45e0f39b1bf3ac3cd9db12e7d689</code></p>
</summary><div><div class="collapsibleContent_ocAz">
<p><code>xK??OR04e?-P?H????/?I?R?</code></p>
</div></div></details>
<p>Cool! So git turned "Hello World" into that weirdly named compressed file.
But here's the problem: if this was a 1GB file, git would still need to decompress
the ENTIRE thing just to read the last byte. Remember the random access problem
from the prologue? That's what we're trying to solve.</p>
<p>Ok we have a weirdly named file with some weird content in it. Let's untangle that.</p>
<h4 class="anchor anchorTargetStickyNavbar_l_sM" id="the-weird-filename---an-address-for-the-content-of-the-file">The weird filename - an address for the content of the file<a href="https://blog.lysk.tech/sqlite-on-git-part-1#the-weird-filename---an-address-for-the-content-of-the-file" class="hash-link" aria-label="Direct link to The weird filename - an address for the content of the file" title="Direct link to The weird filename - an address for the content of the file" translate="no">​</a></h4>
<p>As outlined earlier git has a copy of every state that you commit. It stores the contents addressable by the content. What does that mean?</p>
<p>In the given case "Hello World" gets a "unique identifier" of 40 characters:
<code>5e1c309dae7f45e0f39b1bf3ac3cd9db12e7d689</code>. Git uses SHA1 - a hash function that's like
a digital fingerprint. Same content = same hash, always.</p>
<p><code>SHA1(Header+"Hello World") -&gt; 5e1c309dae7f45e0f39b1bf3ac3cd9db12e7d689</code></p>
<div class="markdown-alert markdown-alert-note" dir="auto">
<p class="markdown-alert-title" dir="auto">Note</p>
<p>We're gonna look into the Header a bit later</p>
</div>
<p>You can generate this unique identifier (hash) yourself by calling a command in git:</p>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
<p><code>git hash-object readme.md</code></p>
</summary><div><div class="collapsibleContent_ocAz">
<p><code>5e1c309dae7f45e0f39b1bf3ac3cd9db12e7d689</code></p>
</div></div></details>
<p>For git the file name doesn't play a role (at that stage). So piping the content into git's hash-object function directly produces the exact same hash.</p>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
<p><code>echo -n "Hello World" | git hash-object --stdin</code></p>
</summary><div><div class="collapsibleContent_ocAz">
<p><code>5e1c309dae7f45e0f39b1bf3ac3cd9db12e7d689</code></p>
</div></div></details>
<p>So if we have the content we know its unique identifier (hash) - and if we have the hash of a content - we know the path (or address) to find it. The address for "Hello World" is, <code>5e1c309dae7f45e0f39b1bf3ac3cd9db12e7d689</code> and the path to it is:
".git/objects/<strong>5e</strong>/<strong>1c309dae7f45e0f39b1bf3ac3cd9db12e7d689</strong>".</p>
<div class="markdown-alert markdown-alert-note" dir="auto">
<p class="markdown-alert-title" dir="auto">Note</p>
<p>Git splits the hash after the first two characters and uses those to distribute the files over subfolders named by those characters. git handles thousands of contents - this trick reduces the number of files per folder by a factor of 256.</p>
</div>
<h4 class="anchor anchorTargetStickyNavbar_l_sM" id="the-weird-characters-in-the-file-behind-the-address">The weird characters in the file behind the address<a href="https://blog.lysk.tech/sqlite-on-git-part-1#the-weird-characters-in-the-file-behind-the-address" class="hash-link" aria-label="Direct link to The weird characters in the file behind the address" title="Direct link to The weird characters in the file behind the address" translate="no">​</a></h4>
<p>Got it - those 40 characters point to that file but those question marks don't look like <code>Hello World</code>. The answer is pretty straightforward here - git compresses the data.</p>
<p>Luckily git comes with a tool to give us the content behind the address.</p>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
<p><code>git cat-file -p 5e1c309dae7f45e0f39b1bf3ac3cd9db12e7d689</code></p>
</summary><div><div class="collapsibleContent_ocAz">
<p><code>Hello World</code></p>
</div></div></details>
<p>Nice! We see the content of the file <code>Hello World</code> again.</p>
<p>What we learned so far: Git snapshots the state of the file and stores them under the address produced by a function that produces the address using the content. This concept is called "content addressable storage".</p>
<p>Think of it like a library where books are filed by their ISBN instead of by author.
If you know the ISBN (the hash), you know exactly where to find the book - no catalog
needed! This is the core of how git structures data, and it's brilliant because the
same content always produces the same address.</p>
<p>This means if we have the address of the content (only 40 characters) we can gain access to the content. This is the core piece of git structures data. How git uses the address internally is not important to understand the next steps and we will cover that in a later article.</p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="going-one-step-deeper---how-git-compresses-loose-objects">Going one step deeper - how git compresses loose objects<a href="https://blog.lysk.tech/sqlite-on-git-part-1#going-one-step-deeper---how-git-compresses-loose-objects" class="hash-link" aria-label="Direct link to Going one step deeper - how git compresses loose objects" title="Direct link to Going one step deeper - how git compresses loose objects" translate="no">​</a></h2>
<div class="markdown-alert markdown-alert-note" dir="auto">
<p class="markdown-alert-title" dir="auto">Note</p>
<p>Want to see how to decompress git objects with JavaScript? Skip this if you don't
have Node.js - I'll show you the result anyway!</p>
</div>
<p>So <code>git cat-file -p [hash]</code> provides us with the file content - served directly from the git folder.
This is great - but we want to only read a fraction. Since Git doesn't provide this out of the box let's check out how Git stores the data.</p>
<p>Git uses zlib to compress – <code>deflate</code> – the loose objects (compare <a href="https://git-scm.com/book/en/v2/Git-Internals-Git-Objects" target="_blank" rel="noopener noreferrer" class="">pro-git-book</a>).</p>
<p>So let's uncompress (inflate) the object - I'm gonna switch to JavaScript since this is the language most devs will be able to follow, it runs in the browser... many more arguments but the main reason is my Rust skills...</p>
<p>To run this code locally you need to install node <a href="https://nodejs.org/en/download" target="_blank" rel="noopener noreferrer" class="">installed</a> and pnpm <a href="https://pnpm.io/installation" target="_blank" rel="noopener noreferrer" class="">installed</a> first.</p>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
<p>npx node -e '</p>
<div class="language-typescript codeBlockContainer_huE5 theme-code-block" style="--prism-background-color:#F3F4F6;--prism-color:#1F2937"><div class="codeBlockContent_oko_"><pre tabindex="0" class="prism-code language-typescript codeBlock_BBgU thin-scrollbar" style="background-color:#F3F4F6;color:#1F2937"><code class="codeBlockLines_t9f9"><span class="token-line" style="color:#1F2937"><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">{</span><span class="token plain"> readFileSync </span><span class="token punctuation" style="color:#9CA3AF">}</span><span class="token plain"> </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token keyword" style="color:#3B82F6">require</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"fs"</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"></span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#9CA3AF">{</span><span class="token plain"> inflateSync </span><span class="token punctuation" style="color:#9CA3AF">}</span><span class="token plain"> </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token keyword" style="color:#3B82F6">require</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"zlib"</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"></span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> compressedData </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token function" style="color:#F59E0B">readFileSync</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">".git/objects/5e/1c309dae7f45e0f39b1bf3ac3cd9db12e7d689"</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"></span><span class="token keyword" style="color:#3B82F6">const</span><span class="token plain"> inflated </span><span class="token operator" style="color:#9CA3AF">=</span><span class="token plain"> </span><span class="token function" style="color:#F59E0B">inflateSync</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">compressedData</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain"></span><span class="token console class-name" style="color:#F59E0B">console</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">log</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token plain">inflated</span><span class="token punctuation" style="color:#9CA3AF">.</span><span class="token method function property-access" style="color:#F59E0B">toString</span><span class="token punctuation" style="color:#9CA3AF">(</span><span class="token string" style="color:#10B981">"utf-8"</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">)</span><span class="token punctuation" style="color:#9CA3AF">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#1F2937"><span class="token plain" style="display:inline-block"></span><br></span></code></pre></div></div>
<p>'</p>
</summary><div><div class="collapsibleContent_ocAz">
<p><code>blob 11Hello World</code></p>
</div></div></details>
<p>Ok the result <code>blob 11Hello World</code> looks way less cryptic. <code>Hello World</code> is obviously the content of the file but what is  <code>blob 11</code>? That's the <strong>header</strong> - and it's actually structured like this:</p>
<p><code>blob 11[null byte]Hello World</code></p>
<p>The header tells us two things:</p>
<ol>
<li class=""><strong>The type of the object</strong>, here <code>blob</code> (file content)</li>
<li class=""><strong>The size of the object</strong>, here <code>11</code> (11 bytes of content)</li>
</ol>
<p>Git adds a null byte (an invisible character) after the header to separate it from the actual content. This helps git allocate the right amount of memory and verify integrity when inflating the object.</p>
<div class="markdown-alert markdown-alert-note" dir="auto">
<p class="markdown-alert-title" dir="auto">Note</p>
<p>git knows 3 other types: <code>tree</code>, <code>commit</code>, and <code>tag</code>. Read more <a href="https://git-scm.com/book/en/v2/Git-Internals-Git-Objects" target="_blank" rel="noopener noreferrer" class="">here</a></p>
</div>
<p>The compression is the reason why reading from the middle or the end of those blobs is not possible. Zlib - the compression algorithm here always deflates from the start to the end and so does inflation work. Git must begin the deflation process always from the start to the point of interest. Since zlib is sequential, partial reads are impossible without decompressing everything up to the bytes you want to read.</p>
<p>Impossible? Huh - let's google that <code>random access zlib possible</code> revealed a very interesting approach by Hengs Li Random <a href="https://lh3.github.io/2014/07/05/random-access-to-zlib-compressed-files" target="_blank" rel="noopener noreferrer" class="">access to zlib compressed files</a>. Turns out Genomic researchers who deal with files &gt;100GB found a way to use a parameter called Z_FULL_FLUSH to deflate data which then allows random seek into the compressed file.</p>
<p>The key insight: <code>Z_FULL_FLUSH</code> forces zlib to reset its compression state at regular intervals, creating checkpoints where decompression can start independently—like chapters in a book instead of one continuous stream.</p>
<p>Oh my god... what if I could use the same mechanism with objects in Git?</p>
<p>That's a lead. What followed on this discovery was a sleepless night a deep dive into zLib, an implementation of a block based compression library.</p>
<p>In <a class="" href="https://blog.lysk.tech/sqlite-on-git-part-2"><strong>Part II</strong></a> we're gonna look behind the curtain - look deeper into zLib, look at the implementation of my <a href="https://github.com/martin-lysk/talepack/tree/main/packages/zlib-random-access" target="_blank" rel="noopener noreferrer" class="">block-based compression library</a> and use it to random seek into a loose object that is compatible with Git. Stay tuned!</p>]]></content:encoded>
            <category>git</category>
            <category>zlib</category>
            <category>sqlite</category>
        </item>
        <item>
            <title><![CDATA[SQLite on Git, Prologue: Why do we need random access in git]]></title>
            <link>https://blog.lysk.tech/sqlite-on-git-prologue</link>
            <guid>https://blog.lysk.tech/sqlite-on-git-prologue</guid>
            <pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In the upcoming series of blogposts I'm going to share the results of my research over the last years.]]></description>
            <content:encoded><![CDATA[<p>In the upcoming series of blogposts I'm going to share the results of my research over the last years.</p>
<p><strong>The result:</strong> A way to enable <strong>random read</strong> and <strong>write access</strong> in <strong>git</strong>'s storage, driving a version controlled fs that allows to run versioned sqlite database on top of git's internal storage.</p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="prologue">Prologue<a href="https://blog.lysk.tech/sqlite-on-git-prologue#prologue" class="hash-link" aria-label="Direct link to Prologue" title="Direct link to Prologue" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="random-what---an-analogy">Random what? - an analogy<a href="https://blog.lysk.tech/sqlite-on-git-prologue#random-what---an-analogy" class="hash-link" aria-label="Direct link to Random what? - an analogy" title="Direct link to Random what? - an analogy" translate="no">​</a></h3>
<p>Do you recall the <strong>walkman</strong> and how much fun it was to find that one track on your mix tape? The <strong>Discman</strong> (with anti-shock ;-) is random access.</p>
<p>⁠<img decoding="async" loading="lazy" alt="The grandfather of the iPod." src="https://blog.lysk.tech/assets/images/walkman-orange-69faec044dddc0f9c7cc73b7835e1962.jpg" width="4140" height="2760" class="img_VXcA"></p>
<p>Photo by <a href="https://unsplash.com/@floschmaezz" target="_blank" rel="noopener noreferrer" class="">Florian Schmetz</a> on Unsplash</p>
<p><em>Too Young?</em> You are watching this livestream and you're waiting for a specific topic that should get covered today. The recording is random access.</p>
<p><code>Random access</code> is a core feature of every filesystem - It allows you to jump to your favorite second in a song (<em>.mp3 file) or a scene in your Movie (</em>.mpeg file), skim through huge PDF files. It is the primitive that <strong>enables SQLite</strong> – a file based database – to return that one row you are interested in from a gigabyte-sized file even on small devices.</p>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="the-problem---git-doesnt-have-random-access">The Problem - git doesn't have random access<a href="https://blog.lysk.tech/sqlite-on-git-prologue#the-problem---git-doesnt-have-random-access" class="hash-link" aria-label="Direct link to The Problem - git doesn't have random access" title="Direct link to The Problem - git doesn't have random access" translate="no">​</a></h3>
<p>In git it's only possible to read and write a file as a whole - if you would want to access only parts of your file, like skipping to the last chapter of a movie inside of a video file, when stored in git, you have to read the whole file once  - it's like your Walkman's fast forward button - compared to Discman's next button - slow. File formats that are designed on the concept of random access face a bottleneck - and fall back to the good old "fast" forward button.</p>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="why-random-access-in-git-in-the-first-place">Why Random access in git in the first place?<a href="https://blog.lysk.tech/sqlite-on-git-prologue#why-random-access-in-git-in-the-first-place" class="hash-link" aria-label="Direct link to Why Random access in git in the first place?" title="Direct link to Why Random access in git in the first place?" translate="no">​</a></h3>
<p><strong>The original motivation behind the research:</strong> Sqlite inside git to get a local first, distributed Database with version control. Think of it as <a href="https://www.dolthub.com/blog/2021-09-17-database-version-control/" target="_blank" rel="noopener noreferrer" class="">dolt</a> but inside of your repository - alongside your code.</p>
<div class="markdown-alert markdown-alert-note" dir="auto">
<p class="markdown-alert-title" dir="auto">Note</p>
<p>The need to store structured data in git came from my previous work on Inlang. This involved various crazy approaches like <a href="https://www.loom.com/share/6cc974f9045c40bf87c167af30222ee0" target="_blank" rel="noopener noreferrer" class="">mergeable file formats</a> in git. We concluded we need a database - and shouldn't reinvent one.</p>
</div>
<p>What if you could combine the query capabilities of a database with the version control and distribution of git? What if your SQLite database could live alongside your code, tracked through commits and branches, mergeable and distributable like the rest of your repository?</p>
<p>This requires a filesystem layer backed by <strong>block storage with random access</strong>. Git as a storage layer misses exactly that.</p>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="can-it-be-added">Can it be added?<a href="https://blog.lysk.tech/sqlite-on-git-prologue#can-it-be-added" class="hash-link" aria-label="Direct link to Can it be added?" title="Direct link to Can it be added?" translate="no">​</a></h3>
<p>This question sent me down a rabbit hole: looking at how researchers in nucleotide sequence alignments deal with a similar problem, finding a flag in a thirty year old spec of a widely used compression algorithm, unearthing nerdy conversation histories from git creator Linus Torvalds and the maintainers of git and a lot of digital duct tape - and finally how this could solve some scaling issues Git faces today.</p>
<p>If this sounds interesting - <a class="" href="https://blog.lysk.tech/sqlite-on-git-part-1">follow me 🐇</a>.</p>
<details class="details_EMZ_ alert alert--info details_wZd7" data-collapsed="true"><summary>
Combining a filesystem with a solid git backed storage actually solves a much broader problem. Read more here
</summary><div><div class="collapsibleContent_ocAz">
<!-- -->
<ul>
<li class="">Problem <a href="https://www.legitcontrol.com/docs/concepts/problem" target="_blank" rel="noopener noreferrer" class="">https://www.legitcontrol.com/docs/concepts/problem</a></li>
<li class="">Idea <a href="https://www.legitcontrol.com/docs/concepts/idea" target="_blank" rel="noopener noreferrer" class="">https://www.legitcontrol.com/docs/concepts/idea</a></li>
<li class="">Legit <a href="https://www.legitcontrol.com/docs/concepts/filesystem-api" target="_blank" rel="noopener noreferrer" class="">https://www.legitcontrol.com/docs/concepts/filesystem-api</a></li>
<li class="">In action <a href="https://www.legitcontrol.com/docs/concepts/chat-app" target="_blank" rel="noopener noreferrer" class="">https://www.legitcontrol.com/docs/concepts/chat-app</a></li>
<li class="">Beyond your Disc <a href="https://www.legitcontrol.com/docs/concepts/user-experience" target="_blank" rel="noopener noreferrer" class="">https://www.legitcontrol.com/docs/concepts/user-experience</a></li>
</ul>
<p>Turns out the filesystem sees kind of a Renaissance:</p>
<ul>
<li class="">Your Company is a Filesystem - <a href="https://x.com/mernit/status/2021324284875153544" target="_blank" rel="noopener noreferrer" class="">https://x.com/mernit/status/2021324284875153544</a> and</li>
<li class="">Your company is not a filesystem <a href="https://x.com/anvisha/status/2022062725354967551" target="_blank" rel="noopener noreferrer" class="">https://x.com/anvisha/status/2022062725354967551</a></li>
</ul>
</div></div></details>]]></content:encoded>
            <category>vfs</category>
            <category>git</category>
            <category>sqlite</category>
        </item>
        <item>
            <title><![CDATA[Making NFS3 reactive]]></title>
            <link>https://blog.lysk.tech/nfs3-event-side-channel</link>
            <guid>https://blog.lysk.tech/nfs3-event-side-channel</guid>
            <pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Network File System (NFS) provides a powerful way to built a vfs on OSx, but it has a fundamental limitation when it comes to event asdasdpropagation. This article explores how event delegation works in NFS, the challenges with remote changes, and an approach to ensure proper notificatioasdn delivery to client applications using a side-channel.]]></description>
            <content:encoded><![CDATA[<p>Network File System (NFS) <strong>provides</strong> a powerful way to built a vfs on OSx, but it has a fundamental limitation when it comes to event asdasdpropagation. This article explores how event delegation works in NFS, the challenges with remote changes, and an approach to ensure proper notificatioasdn delivery to client applications using a side-channel.</p>
<p>The topic outlined here is part of a bigger research on a Gerneral Network Filesystem (GNFS) together with the braid.org group - If you want to learn more checkout <a href="https://braid.org/apps/gnfs" target="_blank" rel="noopener noreferrer" class="">https://braid.org/apps/gnfs</a></p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="standard-file-system-event-flow">Standard File System Event Flow<a href="https://blog.lysk.tech/nfs3-event-side-channel#standard-file-system-event-flow" class="hash-link" aria-label="Direct link to Standard File System Event Flow" title="Direct link to Standard File System Event Flow" translate="no">​</a></h2>
<p>Before diving into NFS specifics, let's understand how file system events work in a local environment:</p>
<p><img decoding="async" loading="lazy" alt="Regular FS events" src="https://blog.lysk.tech/assets/images/fs_event_file_write-dark-34d5c80f7ed98cb5e7c6492ec5211cc3.svg#gh-dark-mode-only" width="1604" height="732" class="img_VXcA"> <img decoding="async" loading="lazy" alt="Regular FS events" src="https://blog.lysk.tech/assets/images/fs_event_file_write-light-64ef6703e5f9d14c528ceda4e6df878a.svg#gh-light-mode-only" width="1604" height="732" class="img_VXcA"></p>
<ol>
<li class=""><strong>Application Write</strong>: An application (e.g., Emacs) writes to a file</li>
<li class=""><strong>File System Update</strong>: The file system modifies the inode content</li>
<li class=""><strong>FSEvent Trigger</strong>: On macOS, the FSEvent system detects this change</li>
<li class=""><strong>Event Propagation</strong>: FSEvents propagates an event with the flag <code>itemModified</code> (or similar)</li>
<li class=""><strong>Observer Notification</strong>: Registered observers (Finder, Quick Look, etc.) react to these changes</li>
</ol>
<p>This flow ensures changes made by Emacs are immediately propagated to applications listening to file system changes lik Finder's Quick Look and other applications.</p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="nfs-event-delegation-architecture">NFS Event Delegation Architecture<a href="https://blog.lysk.tech/nfs3-event-side-channel#nfs-event-delegation-architecture" class="hash-link" aria-label="Direct link to NFS Event Delegation Architecture" title="Direct link to NFS Event Delegation Architecture" translate="no">​</a></h2>
<p>When working with files on an NFS-mounted drive, the event flow becomes more complex. The delegation pattern involves multiple layers:</p>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="local-change-flow-client-initiated">Local Change Flow (Client-Initiated)<a href="https://blog.lysk.tech/nfs3-event-side-channel#local-change-flow-client-initiated" class="hash-link" aria-label="Direct link to Local Change Flow (Client-Initiated)" title="Direct link to Local Change Flow (Client-Initiated)" translate="no">​</a></h3>
<p>When a change originates from the client itself, the flow works as expected:</p>
<p><img decoding="async" loading="lazy" alt="FS events - client side write" src="https://blog.lysk.tech/assets/images/fs_event_nfs_clientside_write-dark-92e425b979e07cc79df9933acd309fb4.svg#gh-dark-mode-only" width="1600" height="1387" class="img_VXcA"> <img decoding="async" loading="lazy" alt="FS events - client side write" src="https://blog.lysk.tech/assets/images/fs_event_nfs_clientside_write-light-3ac18bb8dbd09cc445d6afde20d0885d.svg#gh-light-mode-only" width="1600" height="1387" class="img_VXcA"></p>
<p>In this scenario, the complete event chain functions correctly. When you work locally on an NFS drive, changes made in Emacs will eventually reach Finder and other observers.</p>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="the-problem-remote-changes">The Problem: Remote Changes<a href="https://blog.lysk.tech/nfs3-event-side-channel#the-problem-remote-changes" class="hash-link" aria-label="Direct link to The Problem: Remote Changes" title="Direct link to The Problem: Remote Changes" translate="no">​</a></h2>
<p>An issue arises when changes originate from outside the client's local filesystem:</p>
<p><img decoding="async" loading="lazy" alt="FS events - client side write" src="https://blog.lysk.tech/assets/images/nfs_remote_change-dark-6cfc6b8f0a5ebb05f6530c53e5568571.svg#gh-dark-mode-only" width="1678" height="1692" class="img_VXcA"> <img decoding="async" loading="lazy" alt="FS events - client side write" src="https://blog.lysk.tech/assets/images/nfs_remote_change-light-7d6cb3366c2ab356a50b080b4a1d8ba5.svg#gh-light-mode-only" width="1678" height="1692" class="img_VXcA"></p>
<p><strong>The Breakdown</strong>: The NFS3 protocol lacks a function to propagate changes on the server back to the client. There is no reverse notification channel, meaning the client remains unaware of changes happening remote.</p>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="workarounds-and-limitations">Workarounds and Limitations<a href="https://blog.lysk.tech/nfs3-event-side-channel#workarounds-and-limitations" class="hash-link" aria-label="Direct link to Workarounds and Limitations" title="Direct link to Workarounds and Limitations" translate="no">​</a></h3>
<p>Applications have developed various workarounds to cope with this limitation:</p>
<ul>
<li class=""><strong>Polling</strong>: Some applications poll at regular intervals to check for changes</li>
<li class=""><strong>Focus-based Checks</strong>: Applications like VSCode check for file changes when the window regains focus</li>
</ul>
<p><strong>Limitations</strong>:</p>
<ul>
<li class="">The NFS client may caches file attirbutes locally even with polling the client may misses changes</li>
<li class="">Some applications do not poll - Finder, for example, relies entirely on attribute change events</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="the-solution-event-side-channel">The Solution: Event Side Channel<a href="https://blog.lysk.tech/nfs3-event-side-channel#the-solution-event-side-channel" class="hash-link" aria-label="Direct link to The Solution: Event Side Channel" title="Direct link to The Solution: Event Side Channel" translate="no">​</a></h2>
<p>To address this architectural limitation, below, I describe a way to use a "side channel" approach to address these limitations and ensures remote changes properly propagate to client observers.</p>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="architecture">Architecture<a href="https://blog.lysk.tech/nfs3-event-side-channel#architecture" class="hash-link" aria-label="Direct link to Architecture" title="Direct link to Architecture" translate="no">​</a></h3>
<p>The side channel inserts an additional layer into the NFS server's event processing:</p>
<p><img decoding="async" loading="lazy" alt="FS events - client side write" src="https://blog.lysk.tech/assets/images/nfs_sidechannel_write_file-dark-9194293f1d826c328eaf1001f5db6976.svg#gh-dark-mode-only" width="1618" height="1775" class="img_VXcA"> <img decoding="async" loading="lazy" alt="FS events - client side write" src="https://blog.lysk.tech/assets/images/nfs_sidechannel_write_file-light-75382752aa78146c0d1c5afa674477ba.svg#gh-light-mode-only" width="1618" height="1775" class="img_VXcA"></p>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="how-it-works">How It Works<a href="https://blog.lysk.tech/nfs3-event-side-channel#how-it-works" class="hash-link" aria-label="Direct link to How It Works" title="Direct link to How It Works" translate="no">​</a></h3>
<ol>
<li class="">
<p><strong>Remote Detection</strong>: A remote actor performs a change remotely - here a <code>put</code> on the backing state</p>
</li>
<li class="">
<p><strong>Backing State Update</strong>: the NFS server reconises the change - The backing state notifies the server via <code>send()</code></p>
</li>
<li class="">
<p><strong>Side Channel Activation</strong>: Instead of doing nothing, the side channel:</p>
<ul>
<li class="">Generates a synthetic file write operation on the file system</li>
<li class="">registers a sidechannle write on the NFS Server</li>
<li class="">This write targets the specific file that was remotely changed</li>
</ul>
</li>
<li class="">
<p><strong>NFS Client Processing</strong>: The NFS Client forwards the write operation to the NFS Server</p>
</li>
<li class="">
<p><strong>NFS Server Processing</strong>: The NFS Server receives the write operation and</p>
<ul>
<li class="">It recognizes this as a side channel operation</li>
<li class=""><strong>Does not</strong> forward the write back to the backing state (preventing loops)</li>
<li class="">And returns a success result for the write operation, together with the latest attributes of the changed file state from the backing state</li>
</ul>
</li>
<li class="">
<p><strong>Event Generation</strong>: The NFS client, having seen a "write" on the file, now triggers an FSEvents like the write was comming from just another local application</p>
</li>
<li class="">
<p><strong>Observer Notification</strong>: All registered observers receive the change notification</p>
</li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_l_sM" id="key-characteristics">Key Characteristics<a href="https://blog.lysk.tech/nfs3-event-side-channel#key-characteristics" class="hash-link" aria-label="Direct link to Key Characteristics" title="Direct link to Key Characteristics" translate="no">​</a></h3>
<ul>
<li class=""><strong>Transparent to Applications</strong>: The change appears exactly as if it originated locally</li>
<li class=""><strong>No Redundant Operations</strong>: The side channel write is intercepted before reaching the backing state</li>
<li class=""><strong>Event Loop Prevention</strong>: By intercepting the write, we prevent infinite loops</li>
<li class=""><strong>Standard Event Semantics</strong>: Applications receive standard FSEvents with proper flags</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="benefits">Benefits<a href="https://blog.lysk.tech/nfs3-event-side-channel#benefits" class="hash-link" aria-label="Direct link to Benefits" title="Direct link to Benefits" translate="no">​</a></h2>
<p>This side channel approach ensures that:</p>
<ol>
<li class=""><strong>Consistency</strong>: Remote changes appear identical to local changes from the perspective of client applications</li>
<li class=""><strong>Real-time Updates</strong>: No need for polling or delayed checks</li>
<li class=""><strong>Universal Coverage</strong>: All FSEvent observers receive notifications, regardless of whether they implement polling</li>
<li class=""><strong>Standard Compliance</strong>: The solution works within existing file system event frameworks without requiring application changes</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_l_sM" id="conclusion">Conclusion<a href="https://blog.lysk.tech/nfs3-event-side-channel#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>The NFS event delegation side channel can bridges a critical gap in distributed file system event propagation. By leveraging the existing client-side event generation mechanisms through synthetic writes, remote changes are properly notify all observers, maintaining the expected file system behavior across local and network boundaries.</p>
<p>:</p>]]></content:encoded>
            <category>vfs</category>
            <category>nfs3</category>
        </item>
    </channel>
</rss>