<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.2.0">Jekyll</generator><link href="https://junjizhi.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://junjizhi.com/" rel="alternate" type="text/html" /><updated>2026-06-14T20:06:57+00:00</updated><id>https://junjizhi.com/feed.xml</id><title type="html">Junji Zhi</title><subtitle>Engineer. Blogger.</subtitle><author><name>Junji Zhi</name></author><entry><title type="html">Turning a graphql-ruby Endpoint into an MCP Server the Right Way</title><link href="https://junjizhi.com/all/technical/2026/06/14/turning-a-graphql-ruby-endpoint-into-an-mcp-server-the-right-way/" rel="alternate" type="text/html" title="Turning a graphql-ruby Endpoint into an MCP Server the Right Way" /><published>2026-06-14T19:56:43+00:00</published><updated>2026-06-14T19:56:43+00:00</updated><id>https://junjizhi.com/all/technical/2026/06/14/turning-a-graphql-ruby-endpoint-into-an-mcp-server-the-right-way</id><content type="html" xml:base="https://junjizhi.com/all/technical/2026/06/14/turning-a-graphql-ruby-endpoint-into-an-mcp-server-the-right-way/">&lt;p&gt;Recently I wanted to wire one of my Rails apps into an MCP client so an LLM agent could call my GraphQL API directly. The app already had a solid graphql-ruby setup, real resolvers, authorization context, complexity limits, the works. The MCP path seemed obvious: annotate some fields, generate tool definitions, done.&lt;/p&gt;

&lt;p&gt;That instinct is wrong. I built a POC to figure out what the right abstraction actually is, and it turned out to be more interesting than I expected. There’s a clean architecture answer, a useful CI trick, and at least one subtle bug that will make your MCP tools silently fail on any error path. This post walks through all three.&lt;/p&gt;

&lt;p&gt;The repo is at &lt;a href=&quot;https://github.com/junjizhi/ruby-graphql-mcp-poc&quot;&gt;ruby-graphql-mcp-poc&lt;/a&gt; if you want to follow along.&lt;/p&gt;

&lt;h3 id=&quot;the-wrong-abstraction-field-level-exposure&quot;&gt;The Wrong Abstraction: Field-Level Exposure&lt;/h3&gt;

&lt;p&gt;When you first think about “expose my GraphQL API as MCP tools,” the natural unit feels like a field. You have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;field :products&lt;/code&gt; on your query type, make that a tool. You have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;field :rename_product&lt;/code&gt; on your mutation type, make that a tool too.&lt;/p&gt;

&lt;p&gt;The problem is that a field knows its arguments and its return &lt;em&gt;type&lt;/em&gt;, but it does not define which nested fields to return. For &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;products&lt;/code&gt; returning &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[Product]&lt;/code&gt;, the field definition tells you &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Product&lt;/code&gt; has &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;price_cents&lt;/code&gt;, maybe &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;category&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;variants&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reviews&lt;/code&gt;… but it says nothing about which of those to actually select. Someone has to decide, and auto-expanding doesn’t work:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Graphs are cyclic.&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Product -&amp;gt; Category -&amp;gt; products -&amp;gt; Product -&amp;gt; ...&lt;/code&gt; will loop.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Lists are unbounded.&lt;/strong&gt; Selecting every field on every nested object on every result item is a context-window disaster.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Fields have wildly different costs.&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; is a database column. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;recommendedProducts&lt;/code&gt; might call an ML service.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Authorization differs per field.&lt;/strong&gt; Your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;internal_cost_basis&lt;/code&gt; field might be restricted to admin users. An auto-generated selection set would either expose it or silently drop it, neither is right.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So field-level exposure forces you to either hard-code selection sets somewhere (which is just writing operations in disguise) or leave these problems unsolved.&lt;/p&gt;

&lt;h3 id=&quot;the-right-unit-a-committed-graphql-operation&quot;&gt;The Right Unit: A Committed GraphQL Operation&lt;/h3&gt;

&lt;p&gt;The right boundary is a named, committed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.graphql&lt;/code&gt; operation file. Here’s what that looks like in the POC:&lt;/p&gt;

&lt;div class=&quot;language-graphql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# List products, optionally filtering by a term in the product name.&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ListProducts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;priceCents&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-graphql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Rename one product and return its updated public fields.&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RenameProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;!,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;!)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;renameProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;priceCents&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each operation file gives you everything you need to generate an MCP tool automatically:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Variables&lt;/strong&gt; become the MCP tool’s input schema (compiled from the variable definitions)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Selection set&lt;/strong&gt; becomes the output schema (compiled by walking the selection against the live schema types)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Leading comment&lt;/strong&gt; becomes the tool description&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Operation name&lt;/strong&gt; becomes the tool name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mapping is clean and deterministic. The code that does the compilation sits in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;McpGraphql::InputSchemaCompiler&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;McpGraphql::OutputSchemaCompiler&lt;/code&gt;, two services that walk GraphQL AST nodes and emit JSON Schema.&lt;/p&gt;

&lt;p&gt;This is also independently where Apollo landed with their MCP Server. Their external Rust proxy uses the same operation-first design. The interesting thing for Ruby shops is doing it &lt;em&gt;in-process&lt;/em&gt; through graphql-ruby itself, reusing your real schema, your real resolvers, your real auth context, and your real complexity limits. You’re not standing up a sidecar or duplicating your schema description, you’re just adding a transport.&lt;/p&gt;

&lt;p&gt;The tool execution is correspondingly simple:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;MCP&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Tool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;define&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;tool_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;description: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;input_schema: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;input_schema&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;output_schema: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;output_schema&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;server_context: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;document: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;operation_name: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;variables: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;transform_keys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:to_s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;context: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;invoked_via_mcp: &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;server_context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;nil?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_h&lt;/span&gt;

  &lt;span class=&quot;no&quot;&gt;MCP&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Tool&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;type: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;text: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;generate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;structured_content: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The schema executes the operation normally. MCP is just the discovery and invocation transport on top.&lt;/p&gt;

&lt;h3 id=&quot;treating-mcp-exposure-as-a-contract-subject-to-drift-review&quot;&gt;Treating MCP Exposure as a Contract Subject to Drift Review&lt;/h3&gt;

&lt;p&gt;Here’s where things get interesting from an engineering process perspective.&lt;/p&gt;

&lt;p&gt;Because the input and output JSON Schemas are &lt;em&gt;generated&lt;/em&gt; from (live schema + committed operation) and never hand-written, you can make schema drift structurally impossible to ignore. The POC generates a deterministic manifest at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config/mcp_tools.json&lt;/code&gt; that includes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A SHA-256 of the full schema definition&lt;/li&gt;
  &lt;li&gt;Per-operation SHAs of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.graphql&lt;/code&gt; source&lt;/li&gt;
  &lt;li&gt;The full generated input and output JSON Schemas for each tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/rails mcp:generate&lt;/code&gt; task writes this file. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/rails mcp:check&lt;/code&gt; task is the CI gate:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;task&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;check: :environment&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;generated&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;McpGraphql&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Manifest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_json&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;abort&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Missing &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;manifest_path&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;. Run bin/rails mcp:generate.&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;manifest_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exist?&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;abort&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;manifest_path&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; is stale. Run bin/rails mcp:generate and commit the result.&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;manifest_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;generated&lt;/span&gt;

  &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;MCP operations are valid and &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;manifest_path&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; is current.&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;rescue&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;McpGraphql&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ValidationError&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;abort&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;MCP operation validation failed:&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Let me show you what this looks like in practice. Say you rename &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Product.name&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Product.title&lt;/code&gt; in the schema but leave &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list_products.graphql&lt;/code&gt; untouched. Running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mcp:check&lt;/code&gt; fails with:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;MCP operation validation failed:
Field 'name' doesn't exist on type 'Product'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You fix the operation to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;title&lt;/code&gt;. But &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mcp:check&lt;/code&gt; still fails:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;config/mcp_tools.json is stale. Run bin/rails mcp:generate and commit the result.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You have to run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mcp:generate&lt;/code&gt;, review the diff (the output schema’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; property became &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;title&lt;/code&gt;), and commit it. That diff shows up in code review. Anyone reviewing the PR can see exactly what changed in the MCP contract.&lt;/p&gt;

&lt;p&gt;This “drift-as-CI” framing is the most practical takeaway from this whole project. The committed manifest is your MCP API contract, version-controlled like any other contract, with an automated gate that catches breaks before they ship.&lt;/p&gt;

&lt;h3 id=&quot;the-subtle-bug-why-your-output-schema-will-break-on-error-paths&quot;&gt;The Subtle Bug: Why Your Output Schema Will Break on Error Paths&lt;/h3&gt;

&lt;p&gt;This is the part that took me a while to figure out, and I haven’t seen it written up anywhere.&lt;/p&gt;

&lt;p&gt;The naive approach to generating output schemas is to walk the operation’s selection set and type each field by its GraphQL nullability. A non-null field becomes a required property. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data&lt;/code&gt; is a required object containing the operation result. This passes every happy-path demo.&lt;/p&gt;

&lt;p&gt;Then you turn on the MCP SDK’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;validate_tool_call_results: true&lt;/code&gt; and call a tool that hits an error path.&lt;/p&gt;

&lt;p&gt;Concrete example from the POC: calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rename_product&lt;/code&gt; with a non-existent ID makes the resolver raise &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GraphQL::ExecutionError(&quot;Product not found&quot;)&lt;/code&gt;. GraphQL handles this gracefully, it returns:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;data&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;errors&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Product not found&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;locations&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;renameProduct&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But the naively generated output schema said &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data&lt;/code&gt; must be a non-null object with a required &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;renameProduct&lt;/code&gt; property. Result validation rejects GraphQL’s own response. The MCP client gets an opaque &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-32603 Internal error&lt;/code&gt; instead of “Product not found.”&lt;/p&gt;

&lt;p&gt;An LLM agent hits error paths constantly, non-existent IDs, validation failures, permission errors. If every error path produces a cryptic internal error instead of the actual GraphQL error message, you’ve built a demo, not something an agent can actually use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix, and why it’s principled&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GraphQL has a null propagation rule: a non-null field that errors nulls its nearest nullable ancestor. When there is no nullable ancestor between a non-null field and the root, the propagation bubbles all the way up to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data&lt;/code&gt; itself. So &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data&lt;/code&gt; &lt;em&gt;must&lt;/em&gt; accept null even when every field you selected is declared non-null.&lt;/p&gt;

&lt;p&gt;The positions that need an extra null branch in the output schema are therefore:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Nullable fields, which the compiler already handles naturally&lt;/li&gt;
  &lt;li&gt;The root &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data&lt;/code&gt; envelope, which has no nullable ancestor to absorb propagation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix is tiny:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;ss&quot;&gt;data: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;anyOf: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data_schema&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;type: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;null&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;ss&quot;&gt;errors: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;type: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;array&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;items: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;type: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;object&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;ss&quot;&gt;extensions: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;type: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;object&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data&lt;/code&gt; becomes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;anyOf: [&amp;lt;the success shape&amp;gt;, {type: null}]&lt;/code&gt;, and the envelope allows &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;errors&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extensions&lt;/code&gt;. After the fix, the error path returns the real GraphQL error envelope, and the tool call succeeds (with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;isError: false&lt;/code&gt;, correct, because a GraphQL field error is a successful tool invocation that returned an error envelope, not a transport failure).&lt;/p&gt;

&lt;p&gt;The lesson: GraphQL nullability and MCP output-schema nullability are not the same thing. You have to model error propagation, not just the type system.&lt;/p&gt;

&lt;h3 id=&quot;whats-not-here-the-gap-to-production&quot;&gt;What’s Not Here: The Gap to Production&lt;/h3&gt;

&lt;p&gt;I want to be honest about what this POC is. It runs against a toy in-memory product catalog with two operations. Before I’d package this as a gem and tell anyone to run it in production, I’d want:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real auth threading.&lt;/strong&gt; The POC stubs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server_context&lt;/code&gt; into a boolean flag in the GraphQL context. A real shop needs to pull credentials from the MCP session and build the same authorization context their regular GraphQL requests use, same current user, same role checks, same per-field authorization. That plumbing is application-specific but the POC shows where to thread it (the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;context:&lt;/code&gt; argument to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;schema.execute&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relay connections.&lt;/strong&gt; Most serious graphql-ruby APIs are cursor-paginated everywhere with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edges/nodes/pageInfo&lt;/code&gt; patterns. Mapping a connection into a bounded tool output an LLM can consume without blowing its context window is an unsolved design question. Do you unwrap the nodes? Do you include &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pageInfo&lt;/code&gt; for multi-call pagination? Do you set a max-items cap in the operation? I don’t have a clean answer yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom scalar registry.&lt;/strong&gt; The compiler currently falls back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{type: &quot;string&quot;}&lt;/code&gt; for unrecognized scalars. A production scalar registry needs to map &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Date&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DateTime&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BigDecimal&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JSON&lt;/code&gt; to their real JSON Schema equivalents, otherwise you lose precision in both directions and the LLM has no schema guidance for how to format inputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fragments, interfaces, unions.&lt;/strong&gt; The output compiler raises if it encounters anything other than field selections. Real schemas use inline fragments on union types constantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result-size and token budgeting.&lt;/strong&gt; Nothing in this POC prevents a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ListProducts&lt;/code&gt; call from returning 10,000 products and blowing an agent’s context window. This probably wants a combination of operation-level pagination defaults, a result-size limit in the tool wrapper, or a token-count check before returning.&lt;/p&gt;

&lt;p&gt;I’m listing these as an invitation, not a disclaimer. The core architecture, operations as the capability unit, generated manifest as the contract, in-process execution against the real schema, feels right. The gaps are engineering work, not design problems.&lt;/p&gt;

&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;

&lt;p&gt;If you’re thinking about MCP exposure for a graphql-ruby endpoint:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Field is the wrong boundary.&lt;/strong&gt; Operation is the right one.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Committed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.graphql&lt;/code&gt; files&lt;/strong&gt; are your capability allowlist. Variables become inputs, selection set becomes output, leading comment becomes description.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Commit a generated manifest&lt;/strong&gt; and gate CI on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mcp:check&lt;/code&gt;. Schema drift becomes a reviewable code change, not a runtime surprise.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Make &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data&lt;/code&gt; nullable in your output schema.&lt;/strong&gt; GraphQL null propagation means errors can null &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data&lt;/code&gt; even when every field you selected is non-null. The naive generator will break on every error path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full POC is at &lt;a href=&quot;https://github.com/junjizhi/ruby-graphql-mcp-poc&quot;&gt;ruby-graphql-mcp-poc&lt;/a&gt;. I’d be curious to hear from anyone who’s tackled the Relay connections question or has thoughts on the scalar registry design. Thanks for reading.&lt;/p&gt;</content><author><name>Junji Zhi</name></author><category term="All" /><category term="Technical" /><category term="GraphQL" /><category term="Ruby" /><category term="Rails" /><category term="MCP" /><category term="AI" /><category term="LLM" /><category term="graphql-ruby" /><summary type="html">Field-level exposure is the wrong abstraction for turning a graphql-ruby API into MCP tools. Here is why a committed GraphQL operation is the right unit, how to make schema drift a CI failure instead of a runtime surprise, and the output-schema bug that silently breaks every error path.</summary></entry><entry><title type="html">Use ChatGPT to extract transactions CSV from bank statement PDFs</title><link href="https://junjizhi.com/all/2024/01/07/use-chatgpt-to-extract-transactions-csv-from-bank-statement-pdfs/" rel="alternate" type="text/html" title="Use ChatGPT to extract transactions CSV from bank statement PDFs" /><published>2024-01-07T04:13:39+00:00</published><updated>2024-01-07T04:13:39+00:00</updated><id>https://junjizhi.com/all/2024/01/07/use-chatgpt-to-extract-transactions-csv-from-bank-statement-pdfs</id><content type="html" xml:base="https://junjizhi.com/all/2024/01/07/use-chatgpt-to-extract-transactions-csv-from-bank-statement-pdfs/">&lt;p&gt;In this article, I'm going to share my experience of extracting
transactions data from bank statements (PDF) and convert them into CSVs.
I use a combination of command line tools and ChatGPT to help process
the semi-structured text data.&lt;/p&gt;

&lt;h3 id=&quot;context&quot;&gt;Context&lt;/h3&gt;

&lt;p&gt;I have the habit of tracking my own expenses and I have a large Google
Sheet to list all my expenses for the month, and I build charts from
them. While some Canadidna banks offer CSV downloads, others (e.g., BMO)
generate statements in PDF formats. Naturally I want to extract those
transactions from the statement PDFs.&lt;/p&gt;

&lt;p&gt;Certainly I don't want to enter the expenses manually. I want to
automate this tedious task.&lt;/p&gt;

&lt;h3 id=&quot;my-solution&quot;&gt;My solution&lt;/h3&gt;

&lt;p&gt;tl;dr: &lt;a href=&quot;https://linux.die.net/man/1/ps2ascii&quot;&gt;ps2ascii&lt;/a&gt; + ChatGPT (with
a simple prompt).&lt;/p&gt;

&lt;p&gt;Explanation: I use ps2ascii to extract the raw text from bank statement
PDF, and then I copy the text to a ChatGPT prompt and then ask it to
extract transactions and format them in CSV format.&lt;/p&gt;

&lt;h3 id=&quot;here-is-the-detailed-step&quot;&gt;Here is the detailed step.&lt;/h3&gt;

&lt;p&gt;Installing ps2ascii (in macos):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;ghostsript
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Feed the PDF path to this the command:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ps2ascii jan-28.pdf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You would see that it extract all the text from the PDF. Copy the text
output, and then write a ChatGPT prompt like:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Extract the transactions from following text and format them into csv
format, order by date.&lt;/p&gt;

  &lt;p&gt;&quot;&quot;&quot; &amp;lt;paste your copied text here&amp;gt; &quot;&quot;&quot;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With this prompt, I was able to get ChatGPT to output the transactions
reliably enough.&lt;/p&gt;

&lt;h3 id=&quot;fine-tuning-the-prompt&quot;&gt;Fine tuning the prompt&lt;/h3&gt;

&lt;p&gt;You can further tune the output format by giving examples. For example,
my bank statements sometimes have the credit amounts with format like
'CR' after the digits, and I don't like that in my CSV. I like either
positive or negative numbers, so I changed the prompt to:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;extract the transactions from following text and format them into csv
format, order by date. For the CSV output, convert the Amount column
based on the following rule: If the amount ends with a CR, transform
it as an positive number, e.g., &quot;160.38 CR&quot; will be transformed into
&quot;+160.38&quot;, otherwise remain unchanged. Also, I don't need the
Posting date column:&lt;/p&gt;

  &lt;p&gt;&quot;&quot;&quot; &amp;lt;paste your copied text here&amp;gt; &quot;&quot;&quot;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Note: This is the only prompt engineering / tuning I need to do to get
the output I need. ChatGPT is awesome.&lt;/p&gt;

&lt;h3 id=&quot;whats-next&quot;&gt;What's next&lt;/h3&gt;

&lt;p&gt;From this tiny example, you can see how I use a combo of command line
tools and ChatGPT to extract the valuable data from PDFs for my own
purpose. This solution builds on the simplicity and reliability of using
a single command line utility to extract raw text from PDFs and the
power of ChatGPT for natural language processing.&lt;/p&gt;

&lt;p&gt;I'm happy with the results I got. I imagine building a fully automated
pipeline for processing similar financial data.&lt;/p&gt;</content><author><name>Junji Zhi</name></author><category term="All" /><summary type="html">In this article, I'm going to share my experience of extracting transactions data from bank statements (PDF) and convert them into CSVs. I use a combination of command line tools and ChatGPT to help process the semi-structured text data.</summary></entry><entry><title type="html">Implementing Relay-Style Paginated Fields in Ruby: An Opinionated Guide</title><link href="https://junjizhi.com/all/2023/08/12/implementing-relaystyled-paginated-fields-in-ruby-an-opinionated-guide/" rel="alternate" type="text/html" title="Implementing Relay-Style Paginated Fields in Ruby: An Opinionated Guide" /><published>2023-08-12T13:39:57+00:00</published><updated>2023-08-12T13:39:57+00:00</updated><id>https://junjizhi.com/all/2023/08/12/implementing-relaystyled-paginated-fields-in-ruby-an-opinionated-guide</id><content type="html" xml:base="https://junjizhi.com/all/2023/08/12/implementing-relaystyled-paginated-fields-in-ruby-an-opinionated-guide/">&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;

&lt;p&gt;If you are using GraphQL as the API layer, one of the common performance issues are the unpaginated fields. These fields return an unbounded list of nodes, which increases the size of network payloads and leads to slow loading pages. GraphQL clients have no choice but to fetch the entire list of nodes, even if they may not need all of them.&lt;/p&gt;

&lt;p&gt;If you dig deeper, the problem is because of a lack of well-documented pagination practices and tooling to support that. When implementing a new API field, engineering teams default to the (unbounded) &lt;a href=&quot;https://graphql.org/learn/pagination/#plurals&quot;&gt;plurals&lt;/a&gt; fields, rather than the (paginated) &lt;a href=&quot;https://graphql.org/learn/pagination/#slicing&quot;&gt;slicing&lt;/a&gt; approach.&lt;/p&gt;

&lt;p&gt;In this blog post, we will dive into the world of GraphQL pagination.The lack of well-documented pagination practices and tools can be a daunting challenge for engineering teams. You can use this as a guide on implementing the Relay-style cursor-based paginated GraphQL fields with graphql-ruby connections.&lt;/p&gt;

&lt;p&gt;Drawing from my own experiences of building a GraphQL backend using graphql-ruby and supporting a React/NextJS client, we aim to lower the conceptual barriers and make pagination adoption a breeze for your team. Also, this guide can help teams avoid implementing their own variants of pagination and move towards a consistent codebase.&lt;/p&gt;

&lt;h3 id=&quot;background-what-does-a-paginated-field-look-like&quot;&gt;Background: What does a paginated field look like?&lt;/h3&gt;

&lt;p&gt;From the GraphQL client’s perspective, a paginated field is a special type of object with subfields like &lt;em&gt;edge&lt;/em&gt;, &lt;em&gt;node&lt;/em&gt;, etc.&lt;/p&gt;

&lt;p&gt;For example, you can query a list of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onboarding employees &lt;/code&gt;of a company:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  company {
    onboardingEmployees(first: 10, after: &quot;RW1wbG95ZWU6NmEyMmEzMDMtZGY5Ny00NDdjLTg2ZGEtOTE5ZmExZTc5NTVi&quot;) {
      edges {
        cursor
        node {
          name
        }
      }
      pageInfo {
        hasNextPage
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And it returns data with such a shape:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;company&quot;: {
      &quot;onboardingEmployees&quot;: {
        &quot;edges&quot;: [
          {
            &quot;cursor&quot;: &quot;RW1wbG95ZWU6NmEyMmEzMDMtZGY5Ny00NDdjLTg2ZGEtOTE5ZmExZTc5NTVi&quot;,
            &quot;node&quot;: {
              &quot;name&quot;: &quot;John Snow&quot;,
            }
          },
          {
            &quot;cursor&quot;: &quot;RW1wbG95ZWU6NGNjM2I3OGEtOGFhOS00NzAzLThlODQtYWYyNDkyZWQ1YWUx&quot;,
            &quot;node&quot;: {
              &quot;name&quot;: &quot;Jane Doe&quot;,
            },
          }            
        ],
        &quot;pageInfo&quot;: {
          &quot;hasNextPage&quot;: false,
        },
      },
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The actual people’s data live under each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node &lt;/code&gt;which is nested under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edges&lt;/code&gt; list. GraphQL clients (e.g., a React.js component) would have logic to read the data shape accordingly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Edges&lt;/em&gt; and &lt;em&gt;nodes&lt;/em&gt; are the graph theory concepts. Apollo has &lt;a href=&quot;https://www.apollographql.com/blog/graphql/explaining-graphql-connections/&quot;&gt;an interesting article&lt;/a&gt; if you are curious about that.&lt;/p&gt;

&lt;p&gt;The actual pagination part is the query &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onboardingEmployees(first: 10, after: &quot;&amp;lt;opaque-relay-cursor&amp;gt;&quot;)&lt;/code&gt;. You can pass in &lt;em&gt;pagination arguments&lt;/em&gt; like&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt; first/after&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;last/before&lt;/code&gt;. This is better than the unbounded list fields because it allows you to control _how many _and _where _of lists you get.&lt;/p&gt;

&lt;p&gt;In terms of cursors, they are up to the backend to parse and resolve to a record in the model. Usually, it is a uuid appended with a type. More on that in later sections.&lt;/p&gt;

&lt;p&gt;Also, there is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pageInfo &lt;/code&gt;field that returns the page information data, including whether the current page has a previous or next page. Although the official spec does not prohibit it, we discourage API implementers to return aggregate fields under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pageInfo&lt;/code&gt; , like total number of pages.&lt;/p&gt;

&lt;h2 id=&quot;implementing-a-new-paginated-graphql-field&quot;&gt;Implementing a new paginated GraphQL field&lt;/h2&gt;

&lt;h3 id=&quot;overview&quot;&gt;Overview&lt;/h3&gt;

&lt;p&gt;The overall steps to implement a new paginated field are as follows:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Add a new field with connection type&lt;/li&gt;
  &lt;li&gt;Decide the pagination algorithms (meaty)&lt;/li&gt;
  &lt;li&gt;Implement the connection class with methods&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;field-and-type-naming-convention&quot;&gt;Field and type naming convention&lt;/h3&gt;

&lt;p&gt;To make our paginated field names more consistent as well as leverage what graphql-ruby does for us, we use the following naming convention:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Thing to name&lt;/th&gt;
      &lt;th&gt;Convention&lt;/th&gt;
      &lt;th&gt;Example&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Graphql field&lt;/td&gt;
      &lt;td&gt;Noun in plural. We don’t need modifiers since we can infer from the field type that it is a connection field that accepts pagination arguments.&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;field :onboarding_employees&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Connection class name&lt;/td&gt;
      &lt;td&gt;Ends with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Connection&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OnboardingEmployeesConnection&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;When the connection class type ends with Connection, we get the first/after/before/last pagination arguments &lt;a href=&quot;https://graphql-ruby.org/pagination/using_connections#make-connection-fields&quot;&gt;defined for us for free&lt;/a&gt;, and we don’t have to add the connection: true option to the field definition&lt;/p&gt;

&lt;p&gt;To the put the pieces together, we suggest defining such a field in our schema:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;field :onboarding_employees,   Connections::OnboardingPeopleConnection.connection_type, null: false do
  argument :my_awesome_filter, String
end

# Implement the field and wire up the custom Connection class
def onboarding_employees(my_awesome_filter:, first:, after:, last:, before:)
  Connections::OnboardingPeopleConnection.new(
    context: context,
    # … other args
  )
end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here, we assume you build custom connections by default. We don’t encourage using the built-in ActiveRecordRelationConnection because it requires exposing ActiveRecord models, which breaks modularity and does not work well in a scaled-up Rails app.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://graphql-ruby.org/pagination/custom_connections&quot;&gt;graphql-ruby&lt;/a&gt; also provides the&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt; GraphQL::Pagination::ArrayConnection&lt;/code&gt; helper class and &lt;a href=&quot;https://graphql-ruby.org/api-doc/2.0.5/GraphQL/Pagination/ArrayConnection.html&quot;&gt;convenience methods&lt;/a&gt;. One implementation choice is to Inherit the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ArrayConnection&lt;/code&gt;. Doing so gives us a shorter class implementation. But you should consider the performance implications of having to deal with a long list. Ruby is keeping that list in memory. Also, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ArrayConnection&lt;/code&gt;’s cursor is array index based, not uuid based. We should consider that if we want a unified cursor across different paginated fields.&lt;/p&gt;

&lt;h3 id=&quot;pagination-field-arguments&quot;&gt;Pagination field arguments&lt;/h3&gt;

&lt;h4 id=&quot;standard-arguments-firstafter-and-lastbefore&quot;&gt;Standard arguments: first/after and last/before&lt;/h4&gt;

&lt;p&gt;The arguments usually come in pairs. You can supply either or both pairs. The back end will return the page results based on pagination algorithms discussed below.&lt;/p&gt;

&lt;p&gt;The order must be consistent between first/after and last/before pairs.&lt;/p&gt;

&lt;h4 id=&quot;opinion-returning-the-last-page-with-beforenull&quot;&gt;Opinion: Returning the last page with before:null&lt;/h4&gt;

&lt;p&gt;Some UI components require the backend to support jumping to the last page, for example, &lt;a href=&quot;https://mui.com/material-ui/react-table/&quot;&gt;Material data tables&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To support this use case, we recommend a work-around: Clients can pass&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt; last: &amp;lt;PageSize&amp;gt;&lt;/code&gt; and&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt; before: null&lt;/code&gt; to the field, and API returns the last page of the result set.&lt;/p&gt;

&lt;p&gt;This is slightly diverging from the &lt;a href=&quot;https://relay.dev/graphql/connections.htm#ApplyCursorsToEdges()&quot;&gt;Relay connection specs&lt;/a&gt;, which defines: When before is null, we don’t apply it to the result set. We think this is a reasonable compromise.&lt;/p&gt;

&lt;h3 id=&quot;additional-field-arguments&quot;&gt;Additional field arguments&lt;/h3&gt;

&lt;p&gt;Besides pagination arguments, you can define more custom arguments alongside them. &lt;a href=&quot;https://relay.dev/graphql/connections.htm&quot;&gt;Relay connection spec&lt;/a&gt; does not limit the number of arguments for the field, so you can support as many arguments as you see fit.&lt;/p&gt;

&lt;p&gt;We often see arguments like filter, sorting order to further control the returned data, e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onboardingPeoplePaginated(includedPersonTypes: [&quot;employee&quot;, &quot;contractor&quot;])&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;However, custom pagination arguments don’t come for free. You need to write code in your GraphQL resolver to read them and handle the results accordingly. It is worth bearing in mind that custom arguments could result in more complexity in your connection class definition.&lt;/p&gt;

&lt;h3 id=&quot;connection-class-structure&quot;&gt;Connection class structure&lt;/h3&gt;

&lt;p&gt;From the &lt;a href=&quot;https://graphql-ruby.org/pagination/custom_connections#connection-wrapper&quot;&gt;graphql-ruby&lt;/a&gt; doc, a custom connection class needs to define four methods:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;#nodes, which returns a paginated slice of @items based on the given arguments&lt;/li&gt;
  &lt;li&gt;#has_next_page, which returns true if there are items after the ones in #nodes&lt;/li&gt;
  &lt;li&gt;#has_previous_page, which returns true if there are items before the ones in #nodes&lt;/li&gt;
  &lt;li&gt;#cursor_for(item), which returns a String to serve as the cursor for item&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We recommend following the same structure in your Connection class:&lt;/p&gt;

&lt;p&gt;&lt;img width=&quot;788&quot; alt=&quot;image&quot; src=&quot;https://i.imgur.com/nwaXw3O.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Most interface method are just thin wrapper and returns instance variables like @node, @has_next_page&lt;/li&gt;
  &lt;li&gt;Pagination algorithms are implemented in private method #load_nodes&lt;/li&gt;
  &lt;li&gt;We usually cache the results of #load_nodes to avoid recomputing the nodes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;pagination-algorithms&quot;&gt;Pagination algorithms&lt;/h3&gt;

&lt;p&gt;Conceptually, pagination means cutting a set of things into &lt;em&gt;slices&lt;/em&gt; and returning one of them.&lt;/p&gt;

&lt;p&gt;Zooming in. The cutting or slicing part is basically applying the first / after / last / before / &lt;custom&gt; arguments into:&lt;/custom&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;the set we operate on (filtered in what way, ordered by what column, both are optional)&lt;/li&gt;
  &lt;li&gt;Where we start the cut&lt;/li&gt;
  &lt;li&gt;Where we end the cut&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First &amp;amp; after is one pair of arguments, and last &amp;amp; before is another. Clients can supply either one pair of arguments.&lt;/p&gt;

&lt;p&gt;For the actual implementation, there are generally two choices:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Slicing in ActiveRecord or SQL&lt;/li&gt;
  &lt;li&gt;Slicing an Ruby array&lt;/li&gt;
  &lt;li&gt;Hybrid of 1) and 2)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We discuss their pros and cons separately.&lt;/p&gt;

&lt;h4 id=&quot;1-slicing-in-activerecord-or-sql&quot;&gt;1) Slicing in ActiveRecord or SQL&lt;/h4&gt;

&lt;p&gt;For 1), it boils down to constructing a ActiveRecord query based on the arguments we pass in.&lt;/p&gt;

&lt;p&gt;The cursors will be translated into SQL limit and offset params.&lt;/p&gt;

&lt;p&gt;With this approach, the implementation is usually a bit long. But it is more performant because we are leveraging the database to do the heavy lifting for us, which can have all the help like indexing and caching. And it is usually much faster than approach 2).&lt;/p&gt;

&lt;h4 id=&quot;2-slicing-an-ruby-array&quot;&gt;2) Slicing an Ruby array&lt;/h4&gt;

&lt;p&gt;This implementation basically gets back the entire set of records from database or other sources. Then we apply sorting, filtering, and slicing like we operate on any other Ruby array.&lt;/p&gt;

&lt;p&gt;The advantage of doing so is that the algorithm is relatively shorter and easier to maintain.&lt;/p&gt;

&lt;p&gt;The disadvantage is that we don’t have the Database to do the heavy lifting for us. This is especially problematic when the set gets long and we have to keep everything in memory. Therefore, this approach only works for a relatively stable set that we know won’t grow long.&lt;/p&gt;

&lt;h4 id=&quot;hybrid-of-1--and-2&quot;&gt;Hybrid of 1)  and 2)&lt;/h4&gt;

&lt;p&gt;The hybrid implementation usually comes in the form of filtering / orderging in database, getting back a list, and then performing array slicing. So it shares the same concerns as Approach 2).&lt;/p&gt;

&lt;h3 id=&quot;cursor-encoding-in-base64&quot;&gt;Cursor encoding in base64&lt;/h3&gt;

&lt;p&gt;We propose to follow the Relay style and encode the cursor in base64. For example, Shopify uses such an ID in &lt;a href=&quot;https://shopify.dev/api/usage/pagination-graphql#the-pageinfo-field&quot;&gt;their paginated API&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;eyJsYXN0X2lkIjo3MDE3MjQ0MTY0MTUyLCJsYXN0X3ZhbHVlIjoiNzAxNzI0NDE2NDE1MiJ9
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After decoded, it is:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{&quot;last_id&quot;:7017244164152,&quot;last_value&quot;:&quot;7017244164152&quot;}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The encoded data is usually implementation specific to the field. That means we can define our own data structure and encode it as a base64 string at the end.&lt;/p&gt;

&lt;p&gt;Note that it’s okay for developers to decode relay cursors for troubleshooting purposes. But the intention of encoding is to make it &lt;em&gt;opaque&lt;/em&gt; to API clients, meaning the cursor should not mean anything other than its value. In other words, clients should not decode it while building UIs.&lt;/p&gt;

&lt;p&gt;Finally, &lt;a href=&quot;https://graphql-ruby.org/pagination/cursors.html&quot;&gt;graphql-ruby provides the base64 encoding helpers by default&lt;/a&gt;, so we could use #encode like below:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;def cursor_for(employee)
  # base64 encoded. The payload format is custom made
  encode(&quot;employee:#{employee.uuid}&quot;)
end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;

&lt;p&gt;GraphQL fields that return an unbounded list of items is problematic because it gives API clients no choice but to always fetch the entire list. Engineering teams may be aware of the problem, but there is generally a lack of documentation about how to build paginated fields.&lt;/p&gt;

&lt;p&gt;This post is a hands-on guide about how to implement the &lt;a href=&quot;https://relay.dev/graphql/connections.htm#ApplyCursorsToEdges()&quot;&gt;Relay-style&lt;/a&gt; cursor-based paginated GraphQL fields with &lt;a href=&quot;https://graphql-ruby.org/&quot;&gt;graphly-ruby&lt;/a&gt;. The guide covers a range of implementation topics, including field naming conventions, connection class structure, pagination algorithms and cursor encoding. These guidelines should reduce the adoption barriers and make it easier for engineering teams to build performant yet flexible GraphQL APIs!&lt;/p&gt;</content><author><name>Junji Zhi</name></author><category term="All" /><summary type="html">Introduction</summary></entry><entry><title type="html">Elixir TIL: Start Plug-based Apps without Stopping</title><link href="https://junjizhi.com/all/2022/06/09/elixir-til-start-plugbased-apps-without-stopping/" rel="alternate" type="text/html" title="Elixir TIL: Start Plug-based Apps without Stopping" /><published>2022-06-09T14:40:48+00:00</published><updated>2022-06-09T14:40:48+00:00</updated><id>https://junjizhi.com/all/2022/06/09/elixir-til-start-plugbased-apps-without-stopping</id><content type="html" xml:base="https://junjizhi.com/all/2022/06/09/elixir-til-start-plugbased-apps-without-stopping/">&lt;p&gt;If you are writing a Dockerfile for your Elixir app, the final &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CMD&lt;/code&gt;
line may look like:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;CMD [&quot;mix&quot;, &quot;phx.server&quot;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But for Elixir apps that use &lt;a href=&quot;https://hexdocs.pm/plug/readme.html&quot;&gt;Plug&lt;/a&gt; only without Phoenix, we don't have
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix
phx.server&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The solution comes from &lt;a href=&quot;https://hexdocs.pm/mix/1.12/Mix.Tasks.Run.html&quot;&gt;mix
doc:&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mix run &lt;span class=&quot;nt&quot;&gt;--no-halt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is the production script. For starting the app locally, you can just do
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iex -S mix&lt;/code&gt; which attaches a repl session to the running server as well.&lt;/p&gt;</content><author><name>Junji Zhi</name></author><category term="All" /><summary type="html">If you are writing a Dockerfile for your Elixir app, the final CMD line may look like:</summary></entry><entry><title type="html">Read it Later on Emacs, Information Intake and Writing</title><link href="https://junjizhi.com/all/2022/02/06/read-it-later-on-emacs-information-intake-and-writing/" rel="alternate" type="text/html" title="Read it Later on Emacs, Information Intake and Writing" /><published>2022-02-06T18:01:24+00:00</published><updated>2022-02-06T18:01:24+00:00</updated><id>https://junjizhi.com/all/2022/02/06/read-it-later-on-emacs-information-intake-and-writing</id><content type="html" xml:base="https://junjizhi.com/all/2022/02/06/read-it-later-on-emacs-information-intake-and-writing/">&lt;p&gt;Recently I read &lt;a href=&quot;https://fortelabs.co/blog/the-secret-power-of-read-it-later-apps&quot;&gt;Tiago Forte's post about Read-it-Later
flow&lt;/a&gt;.
It piqued my interest.&lt;/p&gt;

&lt;p&gt;I've used several tools to manage my information intake:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://junjizhi.com/til/2021/10/07/elfeed-rss/&quot;&gt;elfeed&lt;/a&gt; for
following interesting sites that has RSS support&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://support.mozilla.org/en-US/kb/firefox-reader-view-clutter-free-web-pages&quot;&gt;Firefox Reader
View&lt;/a&gt;
to remove clutters of any articles on my browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, not every interesting sites offers RSS. I still open many browser tabs, especially when browsing
&lt;a href=&quot;https://news.ycombinator.com/&quot;&gt;Hacker News&lt;/a&gt; feeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://getpocket.com/home&quot;&gt;Pocket&lt;/a&gt; solves the problem of
remembering my feeds&lt;/strong&gt; and allowing me to come back to them, without fear
of missing out.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Note: Pocket is &lt;a href=&quot;https://www.mozilla.org/en-US/firefox/pocket/&quot;&gt;backed by Mozilla&lt;/a&gt; which is trustworthy enough for me. I also use Firebox which has the handy &lt;a href=&quot;https://support.mozilla.org/en-US/kb/disable-or-re-enable-pocket-for-firefox&quot;&gt;Pocket button built-in&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What’s more interesting is, &lt;strong&gt;Emacs brings the tooling to the next level&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I think so after I discover 
&lt;a href=&quot;https://github.com/alphapapa/pocket-reader.el&quot;&gt;pocket-reader.el&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/alphapapa/pocket-reader.el/master/screenshots/default-theme.png&quot; alt=&quot;pocket-read.el demo&quot; /&gt;
&lt;em&gt;(image source: &lt;a href=&quot;https://github.com/alphapapa/pocket-reader.el&quot;&gt;pocket-reader.el&lt;/a&gt;, For the Emacs setup, check out my Emacstil.com blog
&lt;a href=&quot;https://emacstil.com/til/2022/02/06/pocket-reader-on-emacs/&quot;&gt;here&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Not only Emacs offers the clutter-free UI for reading text, it
integrates seamlessly with my existing note-taking tools, like &lt;a href=&quot;https://orgmode.org/&quot;&gt;org-mode&lt;/a&gt;
and &lt;a href=&quot;https://github.com/alphapapa/org-web-tools&quot;&gt;org-web-tools&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With these handy tools, &lt;strong&gt;reading text, taking notes, absorbing
information and writing about them is all in ONE place&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;After reading some productivity books, and trying some note-taking flow,
I come to realize: &lt;a href=&quot;id:7e21d3f2-269f-41cf-823b-cd057d96369f&quot;&gt;Any tools that get me to write more is
good&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Writing is producing. And every output, even produced years ago, is
inspect-able, grow-able, improvable, and reusable.&lt;/p&gt;

&lt;p&gt;Emacs turns out to be that versatile tool that I can customize and morph to fit
this philosophy.&lt;/p&gt;

&lt;p&gt;And with
&lt;a href=&quot;https://github.com/alphapapa/pocket-reader.el&quot;&gt;pocket-reader.el&lt;/a&gt;, it
converges my information intake down to the familiar Emacs interface, and prompts me to write more.&lt;/p&gt;

&lt;p&gt;I’m excited to adopt the flow and see where it leads!&lt;/p&gt;</content><author><name>Junji Zhi</name></author><category term="All" /><summary type="html">Recently I read Tiago Forte's post about Read-it-Later flow. It piqued my interest.</summary></entry><entry><title type="html">AI as Note Taking Pal: An Experiment</title><link href="https://junjizhi.com/all/2022/01/01/ai-as-note-taking-pal-an-experiment/" rel="alternate" type="text/html" title="AI as Note Taking Pal: An Experiment" /><published>2022-01-01T20:02:06+00:00</published><updated>2022-01-01T20:02:06+00:00</updated><id>https://junjizhi.com/all/2022/01/01/ai-as-note-taking-pal-an-experiment</id><content type="html" xml:base="https://junjizhi.com/all/2022/01/01/ai-as-note-taking-pal-an-experiment/">&lt;p&gt;I did an experiment to ask OpenAI to finish one
&lt;a href=&quot;https://en.wikipedia.org/wiki/Zettelkasten&quot;&gt;Zettelkasten&lt;/a&gt; note. The goal is to
enrich my notes. Turns out, AI generated content is not always directly usable.
But the same content can be useful in surprising ways, which may enrich our
thinking and writing at the end.&lt;/p&gt;

&lt;h1 id=&quot;the-experiment&quot;&gt;The Experiment&lt;/h1&gt;

&lt;p&gt;A little background here. While working on
&lt;a href=&quot;https://github.com/junjizhi/aide.el&quot;&gt;aide.el&lt;/a&gt;, I decide to dogfood
the tool on my work-in-progress (WIP)
&lt;a href=&quot;https://www.orgroam.com/&quot;&gt;Org-roam&lt;/a&gt; notes.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Zettelkasten&quot;&gt;Zettelkasten&lt;/a&gt; method
requires each note to be atomic, and be as thorough about one topic as
possible.&lt;/p&gt;

&lt;p&gt;I wanted to ask AI to finish writing my note. Here's the note that I used as a &lt;a href=&quot;https://beta.openai.com/docs/guides/completion/prompt-design&quot;&gt;prompt&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;:PROPERTIES:
:ID:       5521f179-7a27-4760-9329-b0d97568f927
:END:
#+title: Ruby fibers

* Definitions
** Ruby v1.9+ only
** alternatives to threads
** has its own scheduling
* fiber vs thread
** fiber hands over its control; thread executes continually
** You can resume a fiber
** Not preemptive: Only one fiber at a time; Do our own scheduling
** Faster: Creating lots of fibers is cheaper than threads
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It was an old note I took when reading about &lt;a href=&quot;https://ruby-doc.org/core-3.1.0/Fiber.html&quot;&gt;Ruby
Fiber&lt;/a&gt;. After opening the Org
buffer, I ran &lt;a href=&quot;https://github.com/junjizhi/aide.el&quot;&gt;M-x
aide-openai-completion-buffer-insert&lt;/a&gt;
command, which read the entire buffer and sent to OpenAI as a &lt;a href=&quot;https://beta.openai.com/docs/guides/completion/prompt-design&quot;&gt;prompt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Note that this note text itself is not a &lt;a href=&quot;https://generative.ink/posts/methods-of-prompt-programming/#ground&quot;&gt;good
prompt,&lt;/a&gt;
so I may not get any good results back.&lt;/p&gt;

&lt;p&gt;But that’s the point of this experiment. &lt;strong&gt;I want to see how well AI can complement my
everyday note taking flow, with as little friction as possible&lt;/strong&gt;. So I
picked a &lt;em&gt;real&lt;/em&gt; note.&lt;/p&gt;

&lt;h1 id=&quot;response-from-openai&quot;&gt;Response from OpenAI&lt;/h1&gt;

&lt;p&gt;Here's what I got back from the OpenAI:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
** Lighter: We don't need to copy the stack
** Less state: Less variables, less locks
** You can't change the current fiber
** You can't block a fiber
** You can't join a fiber
** You can't wait for a fiber
** You can't signal a fiber
** You can't change the priority of a fiber
** You can't change the priority of a thread
** You can't change the priority of a process
** You can't change the priority of a thread group
** You can't change the priority of a process group
** You can't change the priority of a process group leader
** You can't change the priority of a process group leader
** You can't change the priority of a process group
** You can't change the priority of a process
** You can't change the priority of a thread
** You can't change the priority of a thread group
** You can't change the
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I limit the tokens to 200, so the response seems cut off.&lt;/p&gt;

&lt;h1 id=&quot;mixed-results&quot;&gt;Mixed results&lt;/h1&gt;

&lt;p&gt;Not surprisingly, the results are mixed.&lt;/p&gt;

&lt;p&gt;The good things are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;AI is able to follow the similar heading structure and wording
initially. E.g., &quot;Lighter: xxxx&quot;, a characteristic followed by a
short description&lt;/li&gt;
  &lt;li&gt;AI is able to list all the keywords related to process / thread /
fiber, like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;state&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lock&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stack&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;The sentences are readable in a way that the statements are
verifiable, like &quot;You can't change the current fiber&quot;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bad are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;It felt like AI gave up at the end. It simply enumerates all the
keywords and repeat itself about the priority changing&lt;/li&gt;
  &lt;li&gt;Some statement looks suspicious, like
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Less state: Less variables, less locks&lt;/code&gt;, which I don't know the
answer right on top of my head.&lt;/li&gt;
  &lt;li&gt;AI makes the common grammar mistakes, like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Less variables&lt;/code&gt; should be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Fewer
variables&lt;/code&gt; (countable)&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;not-so-good-content-can-be-useful-too&quot;&gt;Not-so-good content can be useful, too&lt;/h1&gt;

&lt;p&gt;Overall, the AI generated content is not directly usable without human
involvement.&lt;/p&gt;

&lt;p&gt;But that doesn't mean the content is not useful at all. Upon a closer
look, we can leverage the content in a few ways:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;AI gives me cues or keywords to write about. Reading those statements is
like having a brainstorming session. This also ties to the fact that, AI is
trained on existing text corpus, so it reminds me about what
keywords are popular to cover, like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;state&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lock&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;priority&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;It creates fact statements, which prompt me to think about those
questions, like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;You can't change the current fiber&lt;/code&gt; and verify the truth.
&lt;strong&gt;It is like a child asking innocent questions&lt;/strong&gt;. By going through
them, I enrich the understanding of the problem at hand.&lt;/li&gt;
  &lt;li&gt;AI generated content is a reminder of my current writing style. AI
elaborates (and amplifies) with high precision about my notes
structure, which I did not realize before. This can help improve my
style.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;concluding-thoughts&quot;&gt;Concluding thoughts&lt;/h1&gt;

&lt;p&gt;In this experiment, I fed one of my WIP &lt;a href=&quot;https://www.orgroam.com/&quot;&gt;Org-roam&lt;/a&gt;
notes to AI and asked to complete it with its knowledge. The results I got back
are mixed. AI follows my writing style really well, includes the right keywords
and writes verifiable related (not necessarily true) facts. But the text is
directly usable without human intervention.&lt;/p&gt;

&lt;p&gt;Nevertheless, the not-so-good AI generated content can be useful. If we examine
the generated text carefully, the process can act like a brainstorming session.
I think of new ideas and new directions to reason about, while still on topic.
The content also serves as a reminder of current writing style.&lt;/p&gt;

&lt;p&gt;While AI may not be our best writing pal yet, it is a new tool to collaborate with,
and can eventually enrich our thinking and writing.&lt;/p&gt;

&lt;p&gt;After the experiment, I felt more  excited about &lt;a href=&quot;https://thesephist.com/posts/ai-collaborator/&quot;&gt;AI as
a creative collaborator&lt;/a&gt; space!&lt;/p&gt;</content><author><name>Junji Zhi</name></author><category term="All" /><category term="ai" /><category term="openai" /><category term="gpt-3" /><category term="org" /><category term="org-roam" /><category term="zettelkasten" /><category term="note-taking" /><summary type="html">I did an experiment to ask OpenAI to finish one Zettelkasten note. The goal is to enrich my notes. Turns out, AI generated content is not always directly usable. But the same content can be useful in surprising ways, which may enrich our thinking and writing at the end.</summary></entry><entry><title type="html">“Memory Heap” the Misnomer</title><link href="https://junjizhi.com/all/2021/11/07/memory-heap-the-misnomer/" rel="alternate" type="text/html" title="“Memory Heap” the Misnomer" /><published>2021-11-07T13:57:19+00:00</published><updated>2021-11-07T13:57:19+00:00</updated><id>https://junjizhi.com/all/2021/11/07/memory-heap-the-misnomer</id><content type="html" xml:base="https://junjizhi.com/all/2021/11/07/memory-heap-the-misnomer/">&lt;p&gt;Do you know that memory heap has nothing to do with heap as a data
structure?&lt;/p&gt;

&lt;p&gt;Clearly many don't. Even the top ranked article confuses these two:&lt;/p&gt;

&lt;p&gt;&lt;img width=&quot;906&quot; alt=&quot;image&quot; src=&quot;https://user-images.githubusercontent.com/2715151/140647686-43020110-240d-44e0-aa23-c19f13277c6c.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img width=&quot;856&quot; alt=&quot;image&quot; src=&quot;https://user-images.githubusercontent.com/2715151/140647702-360cc600-a3f7-4a94-97ff-959af328b890.png&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;definitions&quot;&gt;Definitions&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Heap&lt;/strong&gt; is a tree-like data structure which its root as either min/max. It
can be used as a priority queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory heap&lt;/strong&gt; is simply a bunch of memory for programs to allocate and
de-allocate. It is NOT a priority queue or using heap as a data structure.&lt;/p&gt;

&lt;h2 id=&quot;why-the-misnomer&quot;&gt;Why the misnomer?&lt;/h2&gt;

&lt;p&gt;From &lt;a href=&quot;https://stackoverflow.com/q/1699057&quot;&gt;StackOverflow&lt;/a&gt;, it seems like
an unfortunate misnomer because the word &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heap&lt;/code&gt; is commonly
used in English.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;memory pool&lt;/strong&gt; would have been a better name for &lt;strong&gt;memory heap&lt;/strong&gt;.&lt;/p&gt;</content><author><name>Junji Zhi</name></author><category term="All" /><summary type="html">Do you know that memory heap has nothing to do with heap as a data structure?</summary></entry><entry><title type="html">Use elfeed to Manage All My RSS feeds</title><link href="https://junjizhi.com/til/2021/10/07/elfeed-rss/" rel="alternate" type="text/html" title="Use elfeed to Manage All My RSS feeds" /><published>2021-10-07T01:29:55+00:00</published><updated>2021-10-07T01:29:55+00:00</updated><id>https://junjizhi.com/til/2021/10/07/elfeed-rss</id><content type="html" xml:base="https://junjizhi.com/til/2021/10/07/elfeed-rss/">&lt;h2 id=&quot;rss-is-back&quot;&gt;RSS is back!&lt;/h2&gt;

&lt;p&gt;To my surprise, many websites support RSS. Sites like &lt;a href=&quot;https://www.bigbinary.com/blog&quot;&gt;BigBinary / Rubyland&lt;/a&gt;, &lt;a href=&quot;http://pragmaticemacs.com/&quot;&gt;Pragmatic Emacs&lt;/a&gt;, or &lt;a href=&quot;https://planet.emacslife.com/&quot;&gt;Planet Emacslife&lt;/a&gt;, and &lt;a href=&quot;https://lethain.com/&quot;&gt;Irrational Exuberance&lt;/a&gt; about the engineering leadership.&lt;/p&gt;

&lt;p&gt;The problem with good content distributed in separate sites is that, it’s not easy to keep up with all the new stuffs. Plus, different sites have different styles, some even have lots of ads, the reading experience is just not enjoyable.&lt;/p&gt;

&lt;p&gt;I used &lt;a href=&quot;https://blog.feedly.com/tag/rss/&quot;&gt;Feebly&lt;/a&gt;. Before that, I used &lt;a href=&quot;https://en.wikipedia.org/wiki/Google_Reader&quot;&gt;Google Reader&lt;/a&gt; before Google killed it. (Side note: Google seems to being &lt;a href=&quot;https://techcrunch.com/2021/05/19/undead-again-google-brings-back-rss/&quot;&gt;bringing back&lt;/a&gt; RSS).&lt;/p&gt;

&lt;p&gt;But none of the RSS readers is comparable to &lt;a href=&quot;https://github.com/skeeto/elfeed&quot;&gt;elfeed&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/2715151/136307315-6442d308-2cdf-4a08-96d7-b86b3f2b8d44.gif&quot; alt=&quot;2021-10-06 21 28 59&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;a-few-things-i-like-about-elfeed&quot;&gt;A few things I like about elfeed:&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Search is instant by pressing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s&lt;/code&gt; key&lt;/li&gt;
  &lt;li&gt;Render the RSS content minimally, and stays in Emacs, with the option to go to the original URL&lt;/li&gt;
  &lt;li&gt;Fetching, reading, marking something as read, are all one key stroke, in THE Emacs away.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;setting-it-up&quot;&gt;Setting it up&lt;/h2&gt;

&lt;p&gt;Installing and configuring elfeed is easy in Doom Emacs. You just &lt;a href=&quot;https://github.com/hlissner/doom-emacs/tree/develop/modules/app/rss&quot;&gt;enable/uncomment&lt;/a&gt; the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rss&lt;/code&gt; app in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;init.el&lt;/code&gt;, and configure the RSS sources either with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(setq elfeed-feeds ...)&lt;/code&gt; directly, or with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+org&lt;/code&gt; and configure in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/org/elfeed.org&lt;/code&gt; (I preferred the latter).&lt;/p&gt;

&lt;h2 id=&quot;final-words&quot;&gt;Final words&lt;/h2&gt;

&lt;p&gt;For me, &lt;a href=&quot;https://github.com/skeeto/elfeed&quot;&gt;elfeed&lt;/a&gt; is another killer Emacs app after &lt;a href=&quot;https://github.com/skeeto/elfeed&quot;&gt;org mode&lt;/a&gt; and &lt;a href=&quot;https://magit.vc/&quot;&gt;Magit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With elfeed, sites fall into two groups: Those that have RSS, and others that don’t. For the first group, any update is delivered to my elfeed where I can enjoy reading in a distractionless Emacs frame.&lt;/p&gt;

&lt;p&gt;Happy reading!&lt;/p&gt;</content><author><name>Junji Zhi</name></author><category term="til" /><summary type="html">RSS is back!</summary></entry><entry><title type="html">Personal RSS Reader with Project Venus</title><link href="https://junjizhi.com/all/experience/2021/09/29/project-venus/" rel="alternate" type="text/html" title="Personal RSS Reader with Project Venus" /><published>2021-09-29T00:00:00+00:00</published><updated>2021-09-29T00:00:00+00:00</updated><id>https://junjizhi.com/all/experience/2021/09/29/project-venus</id><content type="html" xml:base="https://junjizhi.com/all/experience/2021/09/29/project-venus/">&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;This post is inspired by &lt;a href=&quot;https://planet.emacslife.com/&quot;&gt;Project Emacslife&lt;/a&gt;
where the Emacsers’ feeds are aggregated in one place.&lt;/p&gt;

&lt;p&gt;From there I
discover &lt;a href=&quot;https://github.com/rubys/venus&quot;&gt;Project Venus&lt;/a&gt;. It is a feed reader that I can spin up locally.&lt;/p&gt;

&lt;p&gt;And I decided to give it a try.&lt;/p&gt;

&lt;h2 id=&quot;installation-and-setup&quot;&gt;Installation and Setup&lt;/h2&gt;

&lt;p&gt;The &lt;a href=&quot;https://intertwingly.net/code/venus/docs/installation.html&quot;&gt;Venus page&lt;/a&gt; is helpful. The gist is that &lt;strong&gt;it crawls the RSS feed based on your config and generates a bunch of static HTML files&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;However, after cloning the repo, I tried &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python runtests.py&lt;/code&gt;, and some tests failed.&lt;/p&gt;

&lt;p&gt;I was lazy to fix all the dependencies. Luckily, I found a &lt;a href=&quot;https://gitlab.com/OpenAlt/planet-venus-container&quot;&gt;docker image&lt;/a&gt; that solves this problem perfectly.&lt;/p&gt;

&lt;p&gt;You can follow its &lt;a href=&quot;https://gitlab.com/OpenAlt/planet-venus-container&quot;&gt;README&lt;/a&gt;, tweak the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config.ini&lt;/code&gt;, create the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;public&lt;/code&gt; directory. And run the following command:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ docker run -v ${PWD}:${PWD} --workdir ${PWD} --rm=true registry.gitlab.com/openalt/planet-venus-container:latest
INFO:planet.runner:Socket timeout set to 20 seconds
INFO:planet.runner:Building work queue
INFO:planet.runner:Updating feed https://sachachua.com/blog/category/emacs-news/feed/
INFO:planet.runner:Loading cached data
DEBUG:planet.runner:Processing template /opt/planet-venus/themes/common/atom.xml.xslt using xslt
DEBUG:planet.runner:Processing template /opt/planet-venus/themes/common/foafroll.xml.xslt using xslt
DEBUG:planet.runner:Processing template /opt/planet-venus/themes/classic_fancy/index.html.tmpl using tmpl
DEBUG:planet.runner:Processing template /opt/planet-venus/themes/common/opml.xml.xslt using xslt
DEBUG:planet.runner:Processing template /opt/planet-venus/themes/common/rss10.xml.tmpl using tmpl
DEBUG:planet.runner:Processing template /opt/planet-venus/themes/common/rss20.xml.tmpl using tmpl
INFO:planet.runner:Copying /opt/planet-venus/themes/classic_fancy/planet.css to public/planet.css
INFO:planet.runner:Copying /opt/planet-venus/themes/classic_fancy/../common/images/feed-icon-10x10.png to public/images/feed-icon-10x10.png
INFO:planet.runner:Copying /opt/planet-venus/themes/classic_fancy/../common/images/logo.png to public/images/logo.png
INFO:planet.runner:Copying /opt/planet-venus/themes/classic_fancy/../common/images/planet.png to public/images/planet.png
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And boom. We now have a generated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;public/index.html&lt;/code&gt; that we can view on the browser:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/2715151/135297034-51d3f1dd-8cfc-4994-95a8-37b3857d0218.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;

&lt;p&gt;No ads. No tracking. Just the plain old RSS reader that I missed from Web 2.0!&lt;/p&gt;</content><author><name>Junji Zhi</name></author><category term="All" /><category term="Experience" /><category term="rss" /><category term="project venus" /><category term="rss feed" /><category term="atom feed" /><summary type="html">How to set up a personal RSS reader with open-source Project Venus</summary></entry><entry><title type="html">A Software Development Systems Model</title><link href="https://junjizhi.com/all/experience/2021/09/23/software-dev-systems-model/" rel="alternate" type="text/html" title="A Software Development Systems Model" /><published>2021-09-23T00:00:00+00:00</published><updated>2021-09-23T00:00:00+00:00</updated><id>https://junjizhi.com/all/experience/2021/09/23/software-dev-systems-model</id><content type="html" xml:base="https://junjizhi.com/all/experience/2021/09/23/software-dev-systems-model/">&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;Recently I’ve been reading &lt;a href=&quot;https://www.amazon.ca/Thinking-Systems-Primer-Donella-Meadows/dp/1603580557&quot;&gt;Thinking in Systems: A Primer&lt;/a&gt; and &lt;a href=&quot;https://junjizhi.com/all/experience/2021/09/19/emacs-systems-thinking/&quot;&gt;playing&lt;/a&gt; with Emacs to do system modeling.&lt;/p&gt;

&lt;p&gt;Reflecting on my past software development experience, I came up with the following model:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/2715151/134600912-f7c141e6-d274-4e65-8167-334e09c39c70.png&quot; alt=&quot;dev-modeling&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;org99a2f6c&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;some-explanations&quot;&gt;Some explanations&lt;/h2&gt;

&lt;p&gt;From a systems thinking perspective, a stock is an element that is measurable, and could have inflows and outflows. An example is &lt;em&gt;capital&lt;/em&gt; in &lt;a href=&quot;https://junjizhi.com/all/experience/2021/09/19/emacs-systems-thinking/&quot;&gt;the investment system&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the software development cycle, I used the &lt;em&gt;Features Delivered&lt;/em&gt; as a stock, where &lt;em&gt;feature development&lt;/em&gt; is the inflow that increases the delivered.&lt;/p&gt;

&lt;p&gt;Here, I differentiate feature development from other flows like &lt;em&gt;bug fixes&lt;/em&gt;, &lt;em&gt;refactoring work&lt;/em&gt;, or &lt;em&gt;paying tech debt&lt;/em&gt;. In my experience, most Agile teams adopt this type of rough categorization.&lt;/p&gt;

&lt;p&gt;As we deliver more features, it yields more business output. The product line is expanding, and people usually want more features, so it drives up the business demands, which would push for more development. This forms a &lt;em&gt;reinforcement loop&lt;/em&gt; (denoted by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;R&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;You may argue this is a bit stretched, because more output does not always yield more demands. But my experience told me output and demands are highly correlated.&lt;/p&gt;

&lt;p&gt;If the entire system is left unchecked, a reinforcement loop will always drive itself and continuously drives up the stock.&lt;/p&gt;

&lt;p&gt;We all know that’s impossible. I don’t think any team can push features endlessly. There got to be a limit somewhere.&lt;/p&gt;

&lt;p&gt;That’s why I model the tech debt as a &lt;em&gt;self-balancing loop&lt;/em&gt;: More feature delivered, it incurs more tech debt, which leads to bugs or incidents, which eventually drags the feature development rate.&lt;/p&gt;

&lt;p&gt;At the same time, engineer happiness is affected by bugs or incidents. Fixing bugs or putting out fire all day is not fun for any one. This also has negative impact on the feature development.&lt;/p&gt;

&lt;p&gt;Interestingly, there is a weak reinforcement loop between &lt;em&gt;Engineer Happiness&lt;/em&gt; and &lt;em&gt;Feature Delivered&lt;/em&gt;. Shipping new shiny features is generally a boost of emotions, the model considers that as well.&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;orga02a7cd&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;This is my exploratory work to apply systems thinking to explain the software development activities. The model attempts to define the relationships between features development velocity, engineer happiness, bugs/incidents.&lt;/p&gt;

&lt;p&gt;Three loops are identified, two reinforcement and one self-balancing. These loops explain why feature devs are not infinite, and what may drag the development velocity down.&lt;/p&gt;

&lt;p&gt;Of course, this is a simplified view of software development. There are lots of factors not included here, like team experience, QA, design, PM, etc. So consider this as a starting point.&lt;/p&gt;

&lt;p&gt;Thanks for your read!o&lt;/p&gt;</content><author><name>Junji Zhi</name></author><category term="All" /><category term="Experience" /><category term="emacs" /><category term="mermaid" /><category term="systems thinking" /><category term="stock diagram" /><category term="stock flow" /><category term="flowchart" /><summary type="html">This is my exploratory work to apply systems thinking to explain the software development activities. The model attempts to define the relationships between features development velocity, engineer happiness, bugs/incidents.</summary></entry></feed>