<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="./feed.xsl" type="text/xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">

<channel>
<title>Susam Pal</title>
<link>https://susam.net/</link>
<atom:link rel="self" type="application/rss+xml" href="https://susam.net/feed.xml"/>
<description>Susam's Feed</description>

<item>
<title>Wander 0.3.0</title>
<link>https://susam.net/code/news/wander/0.3.0.html</link>
<guid isPermaLink="false">wnzth</guid>
<pubDate>Wed, 25 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  Wander 0.3.0 is the third release of Wander, a small, decentralised,
  self-hosted web console that lets visitors to your website explore
  interesting websites and pages recommended by a community of
  independent website owners.  To try it, go
  to <a href="../../../wander/">susam.net/wander/</a>.
</p>
<p>
  This release brings small but important bug fixes.  The previous
  release, <a href="0.2.0.html">version 0.2.0</a> introduced a number
  of new features.  Unfortunately, two of them caused issues for some
  users.  A new feature in the previous release was
  the <code>ignore</code> list feature.  The <code>ignore</code> list
  defines console URLs and page URLs that the console never uses while
  discovering page recommendations.  While this feature works fine,
  due to a bug in the implementation, the <strong>Console</strong>
  dialog fails to load in consoles that do not define
  any <code>ignore</code> list.  This has now been fixed.
</p>
<p>
  There was another issue due to which the <code>&lt;iframe&gt;</code>
  that displays discovered websites and pages could not load certain
  websites.  In particular, any website that relied on same-origin
  context to load its own resources failed to load in the console.
  This has been fixed as well.  Please see
  <a href="https://codeberg.org/susam/wander/issues/7">codeberg.org/susam/wander/issues/7</a>
  for a detailed discussion on this issue.
</p>
<p>
  Apart from these two important fixes, there are a few other minor
  fixes too pertaining to preventing horizontal scrolling in small
  devices and preventing duplicate recommendations from appearing too
  close to each other.  Please
  see <a href="https://codeberg.org/susam/wander/src/branch/main/CHANGES.md">CHANGES.md</a>
  for a detailed changelog.
</p>
<p>
  To learn more about Wander, how it works and how to set it up,
  please read the project README at
  <a href="https://codeberg.org/susam/wander#readme">codeberg.org/susam/wander</a>.
  To try it out right now, go to <a href="../../../wander/{{ index
  }}">susam.net/wander/</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/wander/0.3.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Wander 0.2.0</title>
<link>https://susam.net/code/news/wander/0.2.0.html</link>
<guid isPermaLink="false">wnztz</guid>
<pubDate>Tue, 24 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  Wander 0.2.0 is the second release of Wander, a small,
  decentralised, self-hosted web console that lets visitors to your
  website explore interesting websites and pages recommended by a
  community of independent personal website owners.  To try it, go
  to <a href="../../../wander/">susam.net/wander</a>.
</p>
<p>
  This release brings a number of improvements.  When I released
  version 0.1.0, it was the initial version of the software I was
  using for my own website.  Naturally, I was the only user initially
  and I only added trusted web pages to the recommendation list of my
  console.  But ever since I
  <a href="https://news.ycombinator.com/item?id=47422759">announced
  this project on Hacker News</a>, it has received a good amount of
  attention.  It has been less than a week since I announced it there
  but over 30 people have set up a Wander console on their personal
  websites.  There are now over a hundred web pages being recommended
  by this network of consoles.  With the growth in the number of
  people who have set up Wander console, came several feature
  requests, most of which have been implemented already.  This release
  makes these new features available.
</p>
<p>
  Since Wander 0.2.0, the <code>wander.js</code> file of remote
  consoles is executed in a sandbox <code>iframe</code> to ensure that
  it has no side effects on the parent Wander console page.
  Similarly, the pages recommended by the network are also loaded into
  a sandbox <code>iframe</code>.
</p>
<p>
  This release also brings several customisation features.  Console
  owners can customise their Wander console by adding custom CSS or
  JavaScript.  Console owners can also block certain URLs from ever
  being recommended on their console.  This is especially important in
  providing a good wandering experience to visitors.  Since this
  network is completely decentralised, console owners can add any web
  page they like to their console.  Sometimes they inadvertently add
  pages that do not load successfully in the console due to frame
  embedding restrictions.  This leads to an uneven wandering
  experience because these page recommendations occasionally make it
  to other consoles where they fail to load.  Console owners can now
  block such URLs in their console to decrease the likelihood of these
  failed page loads.  This helps make the wandering experience smoother.
</p>
<p>
  Another significant feature in this release is the
  expanded <strong>Console</strong> dialog box.  This dialog box now
  shows various details about the console and the current wandering
  session.  For example, it shows the console's configuration:
  recommended pages, ignored URLs and linked consoles.  It also shows
  a wandering history screen where you can see each link that was
  recommended to you along with the console that recommendation came
  from.  There is another screen that shows all the consoles
  discovered during the discovery process.  Those who care about how
  Wander works would find this dialog box quite useful.  To check it
  out, go to
  <a href="../../../wander/">my Wander console</a> and
  explore.
</p>
<p>
  To learn more about Wander, how it works and how to set it up,
  please read the project README at
  <a href="https://codeberg.org/susam/wander#readme">codeberg.org/susam/wander</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/wander/0.2.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Wander the Small Web</title>
<link>https://susam.net/wander/</link>
<guid isPermaLink="false">wtswb</guid>
<pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  I have put together a small tool to explore the small web of
  personal websites.  It is called <em>Wander</em>.  Please
  visit <a href="https://susam.net/wander/">susam.net/wander/</a> to
  try out my Wander console.
</p>
<p>
  There are only a few pages in it right now, so you cannot use it to
  browse the small web endlessly yet.  It is just a beginning and I
  hope it grows.  If you like this idea and want more websites to
  explore, you can set up your own Wander console so that I can link
  to it from mine.  That is how the Wander network grows.
</p>
<p>
  Please take a look at <a href="https://susam.net/wander/">susam.net/wander/</a>
  and <a href="https://susam.net/form/comment/?p=wander">let me
  know</a> what you think.
</p>
<p>
  If you have your own website, please consider joining this community
  by hosting your own Wander console.  To do so, visit
  <a href="https://codeberg.org/susam/wander#readme">codeberg.org/susam/wander</a>
  and follow the instructions there.  Thank you!
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/wander/">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Wander 0.1.0</title>
<link>https://susam.net/code/news/wander/0.1.0.html</link>
<guid isPermaLink="false">wnzoz</guid>
<pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  Wander 0.1.0 is the first release of Wander, a small, decentralised,
  self-hosted web console that lets visitors to your website explore
  interesting websites and pages recommended by a community of
  independent personal website owners.
</p>
<p>
  Anyone with a personal website can take this tool and host an
  instance of a Wander console.  Each Wander console loads personal
  websites and pages recommended by the Wander community.  Further,
  each Wander console can link to other Wander consoles, forming a
  lightweight, decentralised network for browsing the small web of
  personal websites.
</p>
<p>
  Setting up an instance of a Wander console involves copying just two
  static files from the Wander project at
  <a href="https://codeberg.org/susam/wander">codeberg.org/susam/wander</a>.
  The most interesting aspect of the Wander console is that discovery
  of new links from other consoles happens on the client side in the
  user's web browser.  As a website owner, you do not need to set up
  any server-side components beyond a basic web server.  In fact, you
  can host a Wander console on GitHub Pages or Codeberg Pages too.
</p>
<p>
  To learn more about Wander, how it works and how to set it up,
  please read the project README at
  <a href="https://codeberg.org/susam/wander#readme">codeberg.org/susam/wander</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/wander/0.1.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Git Checkout, Reset and Restore</title>
<link>https://susam.net/git-checkout-reset-restore.html</link>
<guid isPermaLink="false">gcrrp</guid>
<pubDate>Thu, 12 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  I have always used the <code>git checkout</code> and <code>git
  reset</code> commands to reset my working tree or index but since
  Git 2.23 there has been a <code>git restore</code> command available
  for these purposes.  In this post, I record how some of the 'older'
  commands I use map to the new ones.  Well, the new commands aren't
  exactly new since Git 2.23 was released in 2019, so this post is
  perhaps six years too late.  Even so, I want to write this down for
  future reference.  It is worth noting that the old and new commands
  are not always equivalent.  I'll talk more about this briefly as we
  discuss the commands.  However, they can be used to perform similar
  tasks.  Some of these tasks are discussed below.
</p>
<h2 id="contents">Contents<a href="#contents"></a></h2>
<ul>
  <li><a href="#experimental-setup">Experimental Setup</a></li>
  <li><a href="#reset-working-directory">Reset the Working Tree</a></li>
  <li><a href="#reset-index">Reset the Index</a></li>
  <li><a href="#reset-working-directory-and-index">Reset the Working Tree and Index</a></li>
  <li><a href="#summary">Summary</a></li>
</ul>
<h2 id="experimental-setup">Experimental Setup<a href="#experimental-setup"></a></h2>
<p>
  To experiment quickly, we first create an example Git repository.
</p>
<pre><code>mkdir foo/; cd foo/; touch a b c
git init; git add a b c; git commit -m hello</code></pre>
<p>
  Now we make changes to the files and stage some of the changes.  We
  then add more unstaged changes to one of the staged files.
</p>
<pre><code>date | tee a b c d; git add a b d; echo &gt; b</code></pre>
<p>
  At this point, the working tree and index look like this:
</p>
<pre><samp>$ <kbd>git status</kbd>
On branch main
Changes to be committed:
  (use "git restore --staged &lt;file&gt;..." to unstage)
        <span class="c2">modified:   a
        modified:   b
        new file:   d</span>

Changes not staged for commit:
  (use "git add &lt;file&gt;..." to update what will be committed)
  (use "git restore &lt;file&gt;..." to discard changes in working directory)
        <span class="c4">modified:   b
        modified:   c</span></samp></pre>
<p>
  File <code>a</code> has staged changes.  File <code>b</code> has
  both staged and unstaged changes.  File <code>c</code> has only
  unstaged changes.  File <code>d</code> is a new staged file.  In
  each experiment below, we will work with this setup.
</p>
<p>
  All results discussed in this post were obtained using Git 2.47.3 on
  Debian 13.2 (Trixie).
</p>
<h2 id="reset-working-directory">Reset the Working Tree<a href="#reset-working-directory"></a></h2>
<p>
  As a reminder, we will always use the following command between
  experiments to ensure that we restore the experimental setup each
  time:
</p>
<pre><code>date | tee a b c d; git add a b d; echo &gt; b</code></pre>
<p>
  To discard the changes in the working tree and reset the files in
  the working tree from the index, I typically run:
</p>
<pre><code>git checkout .</code></pre>
<p>
  However, the modern way to do this is to use the following command:
</p>
<pre><code>git restore .</code></pre>
<p>
  Both commands leave the working tree and the index in the following
  state:
</p>
<pre><samp>$ <kbd>git status</kbd>
On branch main
Changes to be committed:
  (use "git restore --staged &lt;file&gt;..." to unstage)
        <span class="c2">modified:   a
        modified:   b
        new file:   d</span></samp></pre>
<p>
  Both commands operate only on the working tree.  They do not alter
  the index.  Therefore the staged changes remain intact in the index.
</p>
<h2 id="reset-index">Reset the Index<a href="#reset-index"></a></h2>
<p>
  Another common situation is when we have staged some changes but
  want to unstage them.  First, we restore the experimental setup:
</p>
<pre><code>date | tee a b c d; git add a b d; echo &gt; b</code></pre>
<p>
  I normally run the following command to do so:
</p>
<pre><code>git reset</code></pre>
<p>
  The modern way to do this is:
</p>
<pre><code>git restore -S .</code></pre>
<p>
  Both commands leave the working tree and the index in the following
  state:
</p>
<pre><samp>$ <kbd>git status</kbd>
On branch main
Changes not staged for commit:
  (use "git add &lt;file&gt;..." to update what will be committed)
  (use "git restore &lt;file&gt;..." to discard changes in working directory)
        <span class="c4">modified:   a
        modified:   b
        modified:   c</span>

Untracked files:
  (use "git add &lt;file&gt;..." to include in what will be committed)
        <span class="c4">d</span>

no changes added to commit (use "git add" and/or "git commit -a")</samp></pre>
<p>
  The <code>-S</code> (<code>--staged</code>) option tells <code>git
  restore</code> to operate on the index (not the working tree) and
  reset the index entries for the specified files to match the
  version in <code>HEAD</code>.  The unstaged changes remain intact as
  modified files in the working tree.  With the <code>-S</code>
  option, no changes are made to the working tree.
</p>
<p>
  From the arguments we can see that the old and new commands are not
  exactly equivalent.  Without any arguments, the <code>git
  reset</code> command resets the entire index to <code>HEAD</code>,
  so all staged changes become unstaged.  Similarly, when we run
  <code>git restore -S</code> without specifying a commit, branch or
  tag using the <code>-s</code> (<code>--source</code>) option, it
  defaults to resetting the index from <code>HEAD</code>.
  The <code>.</code> at the end ensures that all paths under the
  current directory are affected.  When we run the command at the
  top-level directory of the repository, all paths are affected and
  the entire index gets reset.  As a result, both the old and the new
  commands accomplish the same result.
</p>
<h2 id="reset-working-directory-and-index">Reset the Working Tree and Index<a href="#reset-working-directory-and-index"></a></h2>
<p>
  Once again, we restore the experimental setup.
</p>
<pre><code>date | tee a b c d; git add a b d; echo &gt; b</code></pre>
<p>
  This time we not only want to unstage the changes but also discard
  the changes in the working tree.  In other words, we want to reset
  both the working tree and the index from <code>HEAD</code>.  This is
  a dangerous operation because any uncommitted changes discarded in
  this manner cannot be restored using Git.
</p>
<pre><code>git reset --hard</code></pre>
<p>
  The modern way to do this is:
</p>
<pre><code>git restore -WS .</code></pre>
<p>
  The working tree is now clean:
</p>
<pre><samp>$ <kbd>git status</kbd>
On branch main
nothing to commit, working tree clean</samp></pre>
<p>
  The <code>-W</code> (<code>--worktree</code>) option makes the
  command operate on the working tree.  The <code>-S</code>
  (<code>--staged</code>) option resets the index as described in the
  previous section.  As a result, this command unstages any changes
  and discards any modifications in the working tree.
</p>
<p>
  Note that when neither of these options is specified,
  <code>-W</code> is implied by default.  That's why the
  bare <code>git restore .</code> command in the previous section
  discards the changes in the working tree.
</p>
<h2 id="summary">Summary<a href="#summary"></a></h2>
<p>
  The following table summarises how the three pairs of commands
  discussed above affect the working tree and the index, assuming the
  commands are run at the top-level directory of a repository.
</p>
<div style="overflow: auto">
  <table class="grid" style="margin: 0">
    <thead>
      <tr>
        <th>Old</th>
        <th>New</th>
        <th>Working Tree</th>
        <th>Index</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td class="pre"><code>git checkout .</code></td>
        <td class="pre"><code>git restore .</code></td>
        <td>Reset to match the index.</td>
        <td>No change.</td>
      </tr>
      <tr>
        <td class="pre"><code>git reset</code></td>
        <td class="pre"><code>git restore -S .</code></td>
        <td>No change.</td>
        <td>Reset to match <code>HEAD</code>.</td>
      </tr>
      <tr>
        <td class="pre"><code>git reset --hard</code></td>
        <td class="pre"><code>git restore -SW .</code></td>
        <td>Reset to match <code>HEAD</code>.</td>
        <td>Reset to match <code>HEAD</code>.</td>
      </tr>
    </tbody>
  </table>
</div>
<p>
  The <code>git restore</code> command is meant to provide a clearer
  interface for resetting the working tree and the index.  I still use
  the older commands out of habit.  Perhaps I will adopt the new ones
  in another six years, but at least I have the mapping written down
  now.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/git-checkout-reset-restore.html">Read on website</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a> |
  <a href="https://susam.net/tag/how-to.html">#how-to</a>
</p>
]]>
</description>
</item>
<item>
<title>HN Skins 0.4.0</title>
<link>https://susam.net/code/news/hnskins/0.4.0.html</link>
<guid isPermaLink="false">hnsfr</guid>
<pubDate>Tue, 10 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  HN Skins 0.4.0 is a minor update to HN Skins, a web browser
  userscript that adds custom themes to Hacker News and lets you
  browse HN with a variety of visual styles.  This release introduces
  a small fix to preserve the commemorative black bar that
  occasionally appears at the top of the page.
</p>
<p>
  When a notable figure in technology or science passes away, Hacker
  News places a thin black bar at the top of the page in tribute.
  Previously some skins could obscure this element.  This update
  ensures that the bar remains visible and clearly noticeable.  In
  dark themed skins, the black bar is rendered as a lighter shade of
  grey so that it maintains sufficient contrast and remains
  conspicuous.
</p>
<p>
  Today Hacker News has
  <a href="https://news.ycombinator.com/item?id=47324054">a story
  about Tony Hoare passing away</a>, which made me notice that the
  commemorative black bar was not rendered properly with some skins.
  This prompted me to investigate the issue and implement the fix
  included in this release.
</p>
<p>
  Screenshots showing how the bar appears with different skins are
  available at
  <a href="https://susam.github.io/blob/img/hnskins/0.4.0/">susam.github.io/blob/img/hnskins/0.4.0/</a>.
</p>
<p>
  To install HN Skins,
  visit <a href="https://github.com/susam/hnskins#readme">github.com/susam/hnskins</a>
  and follow the instructions there.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/hnskins/0.4.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/programming.html">#programming</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>HN Skins 0.3.0</title>
<link>https://susam.net/code/news/hnskins/0.3.0.html</link>
<guid isPermaLink="false">hnsth</guid>
<pubDate>Sat, 07 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  HN Skins 0.3.0 is a minor update to HN Skins, a web browser
  userscript that adds custom themes to Hacker News and allows you to
  browse HN with a variety of visual styles.  This release includes
  fixes for a few issues that slipped through earlier versions.  For
  example, the comment input textbox now uses the same font face and
  size as the rest of the active theme.  The colour of visited links
  has also been slightly muted to make it easier to distinguish them
  from unvisited links.  In addition, some skins have been renamed:
  Teletype is now called Courier and Nox is now called Midnight.
</p>
<p>
  Further, the font face of several monospace based themes is now set
  to <code>monospace</code> instead of <code>courier</code>.  This
  allows the browser's preferred monospace font to be used.  The font
  face of the Courier skin (formerly known as Teletype) remains set
  to <code>courier</code>.  This will never change because the sole
  purpose of this skin is to celebrate this legendary font.
</p>
<p>
  To view screenshots of HN Skins or install it, visit
  <a href="https://github.com/susam/hnskins#readme">github.com/susam/hnskins</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/hnskins/0.3.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/programming.html">#programming</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>HN Skins 0.2.0</title>
<link>https://susam.net/code/news/hnskins/0.2.0.html</link>
<guid isPermaLink="false">hnskt</guid>
<pubDate>Sun, 01 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  HN Skins 0.2.0 is a minor update of HN Skins.  It comes a day after
  its <a href="0.1.0.html">initial release</a> in order to fine tune a
  few minor issues with the styles in the initial release.  HN Skins
  is a web browser userscript that adds custom themes to Hacker News
  and allows you to browse HN with different visual styles.
</p>
<p>
  This update removes excessive vertical space below the 'reply'
  links, sorts the skin options alphabetically in the selection dialog
  and fixes the background colour of the navigation bar in the
  Terminal skin by changing it from a dark grey to a dark green.
</p>
<p>
  Soon after making this release, I discovered a few other minor
  issues, such as the Cafe and Terminal themes using Courier when I
  intended them to use the system monospace font.  This has already
  been fixed in the development version currently available on GitHub.
  However, I will make a formal release later.
</p>
<p>
  See the <a href="https://github.com/susam/hnskins/blob/main/CHANGES.md">changelog</a>
  for more details.  To see some screenshots of HN Skins or to install
  it, visit <a href="https://github.com/susam/hnskins#readme">github.com/susam/hnlinks</a>
  and follow the instructions there.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/hnskins/0.2.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/programming.html">#programming</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>HN Skins 0.1.0</title>
<link>https://susam.net/code/news/hnskins/0.1.0.html</link>
<guid isPermaLink="false">hnsko</guid>
<pubDate>Sat, 28 Feb 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  HN Skins 0.1.0 is the initial release of HN Skins, a browser
  userscript that adds custom themes to Hacker News (HN).  It allows
  you to browse HN in style with a selection of visual skins.
</p>
<p>
  To use HN Skins, first install a userscript manager such as
  Greasemonkey, Tampermonkey or Violentmonkey in your web browser.
  Once installed, you can install HN Skins from
  <a href="https://github.com/susam/hnskins#readme">github.com/susam/hnskins</a>.
</p>
<p>
  The source code is available under the terms of the MIT licence.
  For usage instructions and screenshots, please visit
  <a href="https://github.com/susam/hnskins#readme">github.com/susam/hnskins</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/hnskins/0.1.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/programming.html">#programming</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Feb '26 Notes</title>
<link>https://susam.net/26b.html</link>
<guid isPermaLink="false">ntfts</guid>
<pubDate>Fri, 27 Feb 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  Since last month, I have been collecting brief notes on ideas and
  references that caught my attention during each month but did not
  make it into full articles.  Some of these fragments may eventually
  grow into standalone posts, though most will probably remain as they
  are.  At the very least, this approach allows me to keep a record of
  them.
</p>
<p>
  Most of <a href="26a.html">last month's notes</a> grew out of my
  reading of <em>Algebraic Graph Theory</em> by Godsil and Royle.  I
  am still exploring and learning this subject.  This month, however,
  I dove into another book with the same title but this book is
  written by Norman Biggs.  As a result, many of the notes that follow
  are drawn from Biggs's treatment of the topic.
</p>
<p>
  Since I already had a good understanding of the subject from the
  earlier book, I decided to skip the first fourteen chapters of the
  new book.  I began with Chapter 15, which discusses automorphisms of
  graphs and then moved on to the following chapters on graph
  symmetries.  My main reason for picking up Biggs's book was to
  understand Tutte's well known result that any \( s \)-arc-transitive
  finite cubic graph must satisfy \( s \le 5.  \)  While I did not
  reach that chapter this month, I made substantial progress with the
  book.  I hope to work through the proof of Tutte's theorem next
  month.
</p>
<h2 id="contents">Contents<a href="#contents"></a></h2>
<ol>
  <li><a href="#degree-of-vertices-in-an-orbit">Degree of Vertices in an Orbit</a></li>
  <li><a href="#regular-non-vertex-transitive-graphs">Regular Non-Vertex-Transitive Graphs</a></li>
  <li><a href="#vertex-transitive-but-not-edge-transitive">Vertex-Transitive But Not Edge-Transitive</a></li>
  <li><a href="#edge-transitive-but-not-vertex-transitive">Edge-Transitivex But Not Vertex-Transitive</a></li>
  <li><a href="#bipartiteness-as-a-necessary-condition">Bipartiteness as a Necessary Condition</a></li>
  <li><a href="#graph-with-an-automorphism-group">Graph with an Automorphism Group</a></li>
  <li><a href="#permutation-groups-need-not-be-automorphism-groups">Permutation Groups Need Not Be Automorphism Groups</a></li>
  <li><a href="#symmetric-graphs">Symmetric Graphs</a></li>
</ol>
<h2 id="degree-of-vertices-in-an-orbit">Degree of Vertices in an Orbit<a href="#degree-of-vertices-in-an-orbit"></a></h2>
<p>
  If two vertices of a graph belong to the same orbit, then they have
  the same degree.  In other words, for a graph \( X, \) if \( x, y
  \in V(X) \) and there is an automorphism \( \alpha \) such that \(
  \alpha(x) = y, \) then \( \deg(x) = \deg(y).  \)
</p>
<p>
  The proof is quite straightforward.  Let

  \begin{align*}
    N(x) &amp;= \{ v_1, \dots, v_r \}, \\
    N(y) &amp;= \{ w_1, \dots, w_s \}
  \end{align*}

  represent the neighbours of \( x \) and \( y \) respectively.
  Therefore we have

  \[
    x \sim v_1, \; \dots, \; x \sim v_r.
  \]

  Since an automorphism preserves adjacency, we get

  \[
    \alpha(x) \sim \alpha(v_1), \; \dots, \;
    \alpha(x) \sim \alpha(v_r).
  \]

  Substituting \( \alpha(x) = y, \) we get

  \[
    y \sim \alpha(v_1), \; \dots, \; y \sim \alpha(v_r).
  \]

  Thus

  \[
    \alpha(N(x))
    = \{ \alpha(v_1), \; \dots, \; \alpha(v_r) \}
    \subseteq N(y).
  \]

  A similar argument works in reverse as well.  By the definition of
  automorphism, if \( \alpha \) is an automorphism, so is \(
  \alpha^{-1}.  \)  From the definition of \( N(y) \) above, we have

  \[
    y \sim w_1, \; \dots, \; y \sim w_s.
  \]

  Therefore

  \[
    \alpha^{-1}(y) \sim \alpha^{-1}(w_1), \; \dots, \;
    \alpha^{-1}(y) \sim \alpha^{-1}(w_s).
  \]

  This is equivalent to

  \[
    x \sim \alpha^{-1}(w_1), \; \dots, \; x \sim \alpha^{-1}(w_s).
  \]

  Thus

  \[
    \alpha^{-1}(N(y))
    = \{ \alpha^{-1}(w_1), \; \dots, \; \alpha^{-1}(w_s) \}
    \subseteq N(x)
  \]

  This can be rewritten as

  \[
    \{ \alpha^{-1}(w_1), \; \dots, \; \alpha^{-1}(w_s) \}
    \subseteq \{ v_1, \dots, v_r \}.
  \]

  Therefore

  \[
    N(y)
    = \{ w_1, \dots, w_s \}
    \subseteq \{ \alpha(v_1), \dots, \alpha(v_r) \}
    = \alpha(N(x)).
  \]

  We have shown that \( \alpha(N(x)) \subseteq N(y) \) and \( N(y)
  \subseteq \alpha(N(x)).  \)  Thus

  \[
    \alpha(N(x)) = N(y).
  \]

  Thus

  \[
    \lvert N(y) \rvert = \lvert \alpha(N(x)) \rvert = r.
  \]

  Therefore both \( x \) and \( y \) have \( r \) neighbours each.
  Hence \( \deg(x) = \deg(y).  \)
</p>
<h2 id="regular-non-vertex-transitive-graphs">Regular Non-Vertex-Transitive Graphs<a href="#regular-non-vertex-transitive-graphs"></a></h2>
<p>
  The <a href="https://en.wikipedia.org/wiki/Frucht_graph">Frucht graph</a>
  and the
  <a href="https://en.wikipedia.org/wiki/Folkman_graph">Folkman
  graph</a> are examples of graphs that are \( k \)-regular but not
  vertex-transitive.  In fact, the Folkman graph is a semi-symmetric
  graph, i.e. it is regular and edge-transitive but not
  vertex-transitive.
</p>
<h2 id="vertex-transitive-but-not-edge-transitive">Vertex-Transitive But Not Edge-Transitive<a href="#vertex-transitive-but-not-edge-transitive"></a></h2>
<p>
  The circular ladder graph \( CL_3, \) i.e. the triangular prism
  graph, is vertex-transitive but not edge-transitive.
</p>
<p>
  Every vertex has the same local structure.  Every vertex has degree
  \( 3 \) and it lies on exactly one of the two triangles and it has
  exactly one 'vertical' edge connecting it to the corresponding edge
  on the other triangle.  Any vertex can be sent to any other by an
  automorphism.
</p>
<p>
  Since triangle edges are in a triangle and vertical edges are in no
  triangle, no automorphism can send a triangle edge to a vertical
  edge or vice versa.  Therefore the graph is not edge-transitive.
</p>
<h2 id="edge-transitive-but-not-vertex-transitive">Edge-Transitivex But Not Vertex-Transitive<a href="#edge-transitive-but-not-vertex-transitive"></a></h2>
<p>
  The complete bipartite graphs \( K_{m,n} \) with \( m \ne n \) are
  edge-transitive but not vertex-transitive.
</p>
<p>
  Every edge connects one vertex from the \( m \)-part to one vertex
  from the \( n \)-part.  Any permutation of vertices inside the \( m
  \)-part preserves adjacency.  Similarly, any permutation of vertices
  inside the \( n \)-part preserves adjacency.
</p>
<p>
  Take two arbitrary edges

  \[
    uv, \; u'v' \in E(K_{m,n})
  \]

  where \( u, u' \) are vertices that lie in the \( m \)-part and \(
  v, v' \) are vertices that lie in the \( n \)-part.  Permute
  vertices within the \( m \)-part to send \( u \) to \( u'.  \)
  Similarly, permute vertices within the \( n \)-part to send \( v \)
  to \( v'.  \)  This gives an automorphism that sends the edge \( uv
  \) to \( u'v'.  \)  In this manner we can find an automorphism that
  sends any edge to any other.  Therefore, \( K_{m,n} \) is
  edge-transitive.
</p>
<p>
  However, \( K_{m,n} \) is not vertex-transitive since no
  automorphism can send a vertex in the \( m \)-part to a vertex in
  the \( n \)-part since the vertices in the \( m \)-part have degree
  \( n \) and the vertices in the \( n \)-part have degree \( m.  \)
</p>
<h2 id="bipartiteness-as-a-necessary-condition">Bipartiteness as a Necessary Condition<a href="#bipartiteness-as-a-necessary-condition"></a></h2>
<p>
  If a connected graph is edge-transitive but not vertex-transitive,
  then it must be bipartite.
</p>
<h2 id="graph-with-an-automorphism-group">Graph with an Automorphism Group<a href="#graph-with-an-automorphism-group"></a></h2>
<p>
  In 1938, Frucht proved that for every finite abstract group \( G, \)
  there exists a graph whose automorphism group is isomorphic to \( G
 .  \)
</p>
<p>
  Remarkably, this result remains valid even when we restrict our
  attention to cubic graphs.  That is, for every finite abstract group
  \( G, \) there exists a cubic graph whose automorphism group is
  isomorphic to \( G.  \)  Moreover, the result has been extended to
  graphs satisfying various additional graph-theoretical properties,
  such as \( k \)-connectivity, \( k \)-regularity and prescribed
  chromatic number.
</p>
<h2 id="permutation-groups-need-not-be-automorphism-groups">Permutation Groups Need Not Be Automorphism Groups<a href="#permutation-groups-need-not-be-automorphism-groups"></a></h2>
<p>
  Consider the following specialised version of the problem discussed
  in the previous section: Given a permutation group on a set \( X, \)
  must there exist a graph with vertex set \( X \) whose automorphism
  group is precisely that permutation group?
</p>
<p>
  The answer is no.  Consider the cyclic group \( C_3 \) acting on \(
  X = \{ a, b, c \}.  \)  There is no graph \( \Gamma \) with \(
  V(\Gamma) = X \) and \( \operatorname{Aut}(\Gamma) \cong C_3.  \)  If
  we take \( \Gamma = K_3, \) then \( C_3 \subset S_3 =
  \operatorname{Aut}(K_3) \) but \( C_3 \ne \operatorname{Aut}(K_3)
 .  \)
</p>
<h2 id="symmetric-graphs">Symmetric Graphs<a href="#symmetric-graphs"></a></h2>
<p>
  It is interesting that while we study graph symmetry through
  concepts such as graph automorphisms, vertex-transitivity,
  edge-transitivity, etc. the name <em>symmetric graph</em> is
  reserved for graphs that are \( 1 \)-arc-transitive.  A
  vertex-transitive graph or an edge-transitive graph need not be
  \(1\)-arc-transitive and therefore need not be symmetric.
</p>
<p>
  However, every \( s \)-arc-transitive graph is \(1 \)-arc-transitive
  for \( s \ge 1.  \)  Consequently, every \( s \)-arc-transitive graph
  is symmetric.  Moreover, every distance-transitive graph is also \(
  1 \)-arc-transitive and hence symmetric.
</p>
<p>
  Formally, we say that a graph \( \Gamma \) is \( 1 \)-arc-transitive
  (or equivalently, symmetric) if for all \( 1 \)-arcs \( uv \) and \(
  u'v' \) of \( \Gamma, \) there is an automorphism \( \alpha \in
  \operatorname{Aut}(\Gamma) \) such that \( \alpha(uv) = u'v'.  \)
</p>
<p>
  Stated in more basic terms, we can say that \( \Gamma \) is
  symmetric if for all \( u, v, u', v' \in V(\Gamma) \) satisfying \(
  u \sim v \) and \( u' \sim v', \) there exists \( \alpha \in
  \operatorname{Aut}(\Gamma) \) such that \( \alpha(u) = u' \) and \(
  \alpha(v) = v'.  \)
</p>
<p>
  Switching gears now, we say that \( \Gamma \) is distance-transitive
  if for all \( u, v, u', v' \in V(\Gamma) \) satisfying \( d(u, v) =
  d(u', v'), \) there exists \( \alpha \in \operatorname{Aut}(\Gamma)
  \) such that \( \alpha(u) = u' \) and \( \alpha(v) = v'.  \)  Since
  all \( 1 \)-arcs \( uv \) and \( u'v' \) satisfy \( d(u, v) = d(u',
  v') = 1, \) distance-transitivity implies that there is an
  automorphism that sends \( uv \) to \( u'v'.  \)  Therefore a
  distance-transitive graph is also \( 1 \)-arc-transitive.
</p>
<p>
  To summarise, a graph must possess a certain degree of symmetry in
  order to be called symmetric.  It turns out that merely having a
  non-trivial automorphism group is not sufficient.  Even being
  vertex-transitive or edge-transitive is not enough for a graph to be
  called symmetric.  The graph needs to be at least \( 1
  \)-arc-transitive to be called symmetric.
</p>
<p>
  Another interesting aspect of this terminology is that the property
  of being asymmetric is not the exact opposite of being symmetric.
  For example, a vertex-transitive graph need not be symmetric.
  However, that does not make it asymmetric.  A graph is called
  asymmetric if it has no non-trivial automorphisms, i.e. its
  automorphism group contains only the identity permutation.  Thus, if
  a graph has at least two vertices and is vertex-transitive, it must
  admit a non-trivial automorphism that maps one vertex to another.
  So while such a vertex-transitive may not be symmetric, it isn't
  asymmetric either.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/26b.html">Read on website</a> |
  <a href="https://susam.net/tag/monthly.html">#monthly</a> |
  <a href="https://susam.net/tag/mathematics.html">#mathematics</a>
</p>
]]>
</description>
</item>
<item>
<title>Nerd Quiz #4</title>
<link>https://susam.net/code/news/nq/4.0.0.html</link>
<guid isPermaLink="false">nqfou</guid>
<pubDate>Sun, 22 Feb 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  Nerd Quiz #4 is the fourth instalment of Nerd Quiz, a single page
  HTML application that challenges you to measure your inner geek with
  a brief quiz.  Each question in the quiz comes from everyday moments
  of reading, writing, thinking, learning and exploring.
</p>
<p>
  This release introduces five new questions drawn from a range of
  topics, including computing history, graph theory and Unix.
  Visit <a href="../../../nq.html#4">Nerd Quiz</a> to try the quiz.
</p>
<p>
  A community discussion page is
  <a href="../../../comments/nq.html">available here</a>.  You are
  very welcome to share your score or discuss the questions there.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/nq/4.0.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/miscellaneous.html">#miscellaneous</a> |
  <a href="https://susam.net/tag/game.html">#game</a>
</p>
]]>
</description>
</item>
<item>
<title>Deep Blue: Chess vs Programming</title>
<link>https://susam.net/deep-blue.html</link>
<guid isPermaLink="false">dblue</guid>
<pubDate>Sun, 15 Feb 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  I remember how dismayed Kasparov was after losing the 1997 match to
  IBM's Deep Blue, although his views on Deep Blue became more
  balanced with time and he accepted that we had entered a new era in
  which computers would outperform grandmasters at chess.
</p>
<p>
  Still, chess players can take comfort in the fact that chess is
  still played between humans.  Players make their name and fame by
  beating other humans because playing against computers is no longer
  interesting as a competition.
</p>
<p>
  Many software developers would like to have similar comfort.  But
  that comfort is harder to find, because unlike chess, building
  prototypes or PoCs is not seen as a sport or art form.  It is mostly
  seen as a utility.  So while brain-coding a PoC may still be
  intellectually satisfying for the programmer, to most other people
  it only matters that the thing works.  That means that programmers
  do not automatically get the same protected space that chess players
  have, where the human activity itself remains valued even after
  machines become stronger.  The activity programmers enjoy may
  continue but the recognition and economic value attached to it may
  shrink.
</p>
<p>
  So I think the big adjustment software developers have to make is
  this: The craft will still exist and we will still enjoy doing it
  but the credit and value will increasingly go to those who define
  problems well, connect systems, make good product decisions and make
  technology useful in messy real-world situations.  It has already
  been this way for a while and will only become more so as time goes
  by.
</p>
<hr>
<p>
  <em>
    This note reproduces a recent comment I posted in a Lobsters forum
    thread about LLM-assisted software development at
    at <a href="https://lobste.rs/s/qmjejh#c_4bgez9">lobste.rs/s/qmjejh</a>.
  </em>
</p>
<p>
  <em>
    See also:
    <a href="inverse-laws-of-robotics.html">Three Inverse Laws of AI and Robotics</a>.
  </em>
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/deep-blue.html">Read on website</a> |
  <a href="https://susam.net/tag/miscellaneous.html">#miscellaneous</a>
</p>
]]>
</description>
</item>
<item>
<title>Soju User Delete Hash</title>
<link>https://susam.net/soju-user-delete-hash.html</link>
<guid isPermaLink="false">sudhs</guid>
<pubDate>Sat, 14 Feb 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  In <a href="from-znc-to-soju.html">my last post</a>, I talked about
  switching from ZNC to Soju as my IRC bouncer.  One thing that caught
  my attention while creating and deleting Soju users was that the
  delete command asks for a confirmation, like so:
</p>
<pre><samp>$ <kbd>sudo sojuctl user delete soju</kbd>
To confirm user deletion, send "user delete soju 4664cd"
$ <kbd>sudo sojuctl user delete soju 4664cd</kbd>
deleted user "soju"</samp></pre>
<p>
  That confirmation token for a specific user never changes, no matter
  how many times we create or delete it.  The confirmation token is
  not saved in the Soju database, as can be confirmed here:
</p>
<pre><samp>$ <kbd>sudo sqlite3 -table /var/lib/soju/main.db 'SELECT * FROM User'</kbd>
+----+----------+--------------------------------------------------------------+-------+----------+------+--------------------------+---------+--------------------------+--------------+
| id | username |                           password                           | admin | realname | nick |        created_at        | enabled | downstream_interacted_at | max_networks |
+----+----------+--------------------------------------------------------------+-------+----------+------+--------------------------+---------+--------------------------+--------------+
| 1  | soju     | $2a$10$yRj/oYlR2Zwd8YQxZPuAQuNo2j7FVJWeNdIAHF2MinYkKLmBjtf0y | 0     |          |      | 2026-02-16T13:49:46.119Z | 1       |                          | -1           |
+----+----------+--------------------------------------------------------------+-------+----------+------+--------------------------+---------+--------------------------+--------------+</samp></pre>
<p>
  Surely, then, the confirmation token is derived from the user
  definition?  Yes, indeed it is.  This can be confirmed at the
  <a href="https://codeberg.org/emersion/soju/src/commit/v0.10.1/service.go#L1185-L1203">source
  code here</a>.  Quoting the most relevant part from the source code:
</p>
<pre><code>hashBytes := sha1.Sum([]byte(username))
hash := fmt.Sprintf("%x", hashBytes[0:3])</code></pre>
<p>
  Indeed if we compute the same hash ourselves, we get the same token:
</p>
<pre><samp>$ <kbd>printf soju | sha1sum | head -c6</kbd>
4664cd</samp></pre>
<p>
  This allows us to automate the two step Soju user deletion process
  in a single command:
</p>
<pre><code>sudo sojuctl user delete soju "$(printf soju | sha1sum | head -c6)"</code></pre>
<p>
  But of course, the implementation of the confirmation token may
  change in future and Soju helpfully outputs the deletion command
  with the confirmation token when we first invoke it without the
  token, so it is perhaps more prudent to just take that output and
  feed it back to Soju, like so:
</p>
<pre><code>sudo sojuctl $(sudo sojuctl user delete soju | sed 's/.*"\(.*\)"/\1/')</code></pre>
<!-- ### -->
<p>
  <a href="https://susam.net/soju-user-delete-hash.html">Read on website</a> |
  <a href="https://susam.net/tag/shell.html">#shell</a> |
  <a href="https://susam.net/tag/irc.html">#irc</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a> |
  <a href="https://susam.net/tag/how-to.html">#how-to</a>
</p>
]]>
</description>
</item>
<item>
<title>From ZNC to Soju</title>
<link>https://susam.net/from-znc-to-soju.html</link>
<guid isPermaLink="false">fztsj</guid>
<pubDate>Thu, 12 Feb 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  I have recently switched from ZNC to Soju as my IRC bouncer and I am
  already quite pleased with it.  I usually run my bouncer on a Debian
  machine, where Soju is well packaged and runs smoothly right after
  installation.  By contrast, the ZNC package included with Debian 13
  (Trixie) and earlier fails to start after installation because of a
  missing configuration file.  As a result, I was forced to maintain
  my own configuration file along with a necessary PEM bundle, copy
  them to the Debian system and carefully set the correct file
  permissions before I could run ZNC successfully.  None of this is
  necessary with Soju, since installing it from the Debian package
  repository automatically sets up the configuration and certificate
  files.  I no longer have to manage any configuration or certificate
  files myself.
</p>
<h2 id="setup">Setup<a href="#setup"></a></h2>
<p>
  It is quite straightforward to install and set up Soju on Debian.
  The following two commands install Soju:
</p>
<pre><code>sudo apt-get update
sudo apt-get -y install soju</code></pre>
<p>
  Then setting up an IRC connection involves another two commands:
</p>
<pre><code>sudo sojuctl user create -username soju -password YOUR_SOJU_PASSWORD
sudo sojuctl user run soju network create -name bnc1 -addr irc.libera.chat -nick YOUR_NICK -pass YOUR_NICK_PASSWORD</code></pre>
<p>
  Here, <code>YOUR_SOJU_PASSWORD</code> is a placeholder for a new
  password you must choose for your Soju user.  Finally, we restart
  Soju as follows:
</p>
<pre><code>sudo systemctl restart soju</code></pre>
<h2 id="database">Database<a href="#database"></a></h2>
<p>
  What previously involved maintaining several files that had to be
  installed and configured on each machine running ZNC is now reduced
  to the two <code>sojuctl</code> commands above.  Still, the
  configuration needs to live somewhere.  In fact, the
  two <code>sojuctl</code> commands introduce earlier store the
  configuration in a SQLite database.  Here is a glimpse of what the
  database looks like:
</p>
<pre><samp>$ <kbd>sudo sqlite3 /var/lib/soju/main.db '.tables'</kbd>
Channel              MessageFTS_data      ReadReceipt
DeliveryReceipt      MessageFTS_docsize   User
Message              MessageFTS_idx       WebPushConfig
MessageFTS           MessageTarget        WebPushSubscription
MessageFTS_config    Network
$ <kbd>sudo sqlite3 /var/lib/soju/main.db 'SELECT * from User'</kbd>
1|soju|$2a$10$mM5Qcz8.OPMi9lyWDxPRh.bNxzq7jtLdxcoPl09AYTnqcmLmEqzSO|0|||2026-02-17T23:24:24.926Z|1||-1
$ <kbd>sudo sqlite3 /var/lib/soju/main.db 'SELECT * from Network'</kbd>
1|bnc1|1|irc.libera.chat|YOUR_NICK||||YOUR_NICK_PASSWORD|||||||1|1</samp></pre>
<h2 id="client">Client Configuration<a href="#client"></a></h2>
<p>
  Finally, the IRC client can be configured to connect to port 6697 on
  the system running Soju.  Here is an example of how this can be done
  in Irssi:
</p>
<pre><code>/network add -nick YOUR_NICK -user soju/bnc1 net1
/server add -tls -network bnc1 YOUR_SOJU_HOST 6697 YOUR_SOJU_PASSWORD
/connect net1
</code></pre>
<p>
  You can also set up multiple connections to IRC networks through the
  same Soju instance.  All you need to do is repeat
  the <code>sojuctl</code> commands to create additional networks such
  as <code>bnc2</code>, <code>bnc3</code> and so on, then repeat the
  configuration in your IRC client using new network names such as
  <code>net2</code>, <code>net3</code>, etc.  These network names are
  entirely user defined, so you can choose any names you like.  The
  names <code>bnc2</code>, <code>net2</code> and so on are only
  examples.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/from-znc-to-soju.html">Read on website</a> |
  <a href="https://susam.net/tag/irc.html">#irc</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a> |
  <a href="https://susam.net/tag/how-to.html">#how-to</a>
</p>
]]>
</description>
</item>
<item>
<title>Twenty Five Years of Computing</title>
<link>https://susam.net/twenty-five-years-of-computing.html</link>
<guid isPermaLink="false">tfyoc</guid>
<pubDate>Fri, 06 Feb 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  Last year, I completed 20 years in professional software
  development.  I wanted to write a post to mark the occasion back
  then, but couldn't find the time.  This post is my attempt to make
  up for that omission.  In fact, I have been involved in software
  development for a little longer than 20 years.  Although I had
  my <a href="fd-100.html">first taste</a> of computer programming as
  a child, it was only when I entered university about 25 years ago
  that I seriously got into software development.  So I'll start my
  stories from there.  These stories are less about software and more
  about people.  Unlike many posts of this kind, this one offers no
  wisdom or lessons.  It only offers a collection of stories.  I hope
  you'll like at least a few of them.
</p>
<h2 id="contents">Contents<a href="#contents"></a></h2>
<ul>
  <li><a href="#viewing-the-source">Viewing the Source</a></li>
  <li><a href="#reset-vector">The Reset Vector</a></li>
  <li><a href="#man-in-the-middle">Man in the Middle</a></li>
  <li><a href="#sphagetti-code">Sphagetti Code</a></li>
  <li><a href="#animated-television-widgets">Animated Television Widgets</a></li>
  <li><a href="#good-blessings">Good Blessings</a></li>
  <li><a href="#the-ctf-scoreboard">The CTF Scoreboard</a></li>
</ul>
<h2 id="viewing-the-source">Viewing the Source<a href="#viewing-the-source"></a></h2>
<p>
  The first story takes place in 2001, shortly after I joined
  university.  One evening, I went to the university computer
  laboratory to browse the World Wide Web.  Out of curiosity, I typed
  <code>susam.com</code> into the address bar and landed on
  <a href="https://web.archive.org/web/20010721163153/http://susam.com/">its
  home page</a>.  I remember the text and banner looking much larger
  back then.  Display resolutions were lower, so the text and banner
  covered almost half the screen.  I knew very little about the
  Internet then and I was just trying to make sense of it.  I remember
  wondering what it would take to create my own website, perhaps at
  <code>susam.com</code>.  That's when an older student who had been
  watching me browse over my shoulder approached and asked if I had
  created the website.  I told him I hadn't and that I had no idea how
  websites were made.  He asked me to move aside, took my seat and
  clicked View &gt; Source in Internet Explorer.  He then explained
  how websites are made of HTML pages and how those pages are simply
  text instructions.
</p>
<p>
  Next, he opened Notepad and wrote a simple HTML page that looked
  something like this:
</p>
<pre><code>&lt;BODY&gt;&lt;FONT COLOR="RED"&gt;HELLO&lt;/FONT&gt;&lt;/BODY&gt;</code></pre>
<p>
  Yes, we had a <code>FONT</code> tag back then and it was common
  practice to write HTML tags in uppercase.  He then opened the page
  in a web browser and showed how it rendered.  After that, he
  demonstrated a few more features such as changing the font face and
  size, centring the text and altering the page's background colour.
  Although the tutorial lasted only about ten minutes, it made the Web
  feel far less mysterious and much more fascinating.
</p>
<p>
  That person had an ulterior motive though.  After the tutorial, he
  never returned the seat to me.  He just continued browsing the Web
  and waited for me to leave.  I was too timid to ask for my seat
  back.  Seats were limited, so I returned to my dorm room both
  disappointed that I couldn't continue browsing that day and excited
  about all the websites I might create with this newfound knowledge.
  I could never register <code>susam.com</code> for myself though.
  That domain was always used by some business selling Turkish
  cuisines.  Eventually, I managed to get the next best thing:
  a <code>.net</code> domain of my own.  That brief encounter in the
  university laboratory set me on a lifelong path of creating and
  maintaining personal websites.
</p>
<h2 id="reset-vector">The Reset Vector<a href="#reset-vector"></a></h2>
<p>
  The second story also comes from my university days.  One afternoon,
  I was hanging out with my mates in the computer laboratory.  In
  front of me was an MS-DOS machine powered by an Intel 8086
  microprocessor, on which I was writing a lift control program in
  assembly.  In those days, it was considered important to
  deliberately practise solving made-up problems as a way of honing
  our programming skills.  As I worked on my program, my mind drifted
  to a small detail about the 8086 microprocessor that we had recently
  learnt in a lecture.  Our professor had explained that, when the
  8086 microprocessor is reset, execution begins with CS:IP set to
  FFFF:0000.  So I murmured to anyone who cared to listen, 'I wonder
  if the system will reboot if I jump to FFFF:0000.'  I then
  opened <code>DEBUG.EXE</code> and jumped to that address.
</p>
<pre><samp>C:\&gt;<kbd>DEBUG</kbd>
-<kbd>G =FFFF:0000</kbd></samp></pre>
<p>
  The machine rebooted instantly.  One of my friends, who topped the
  class every semester, had been watching over my shoulder.  As soon
  as the machine restarted, he exclaimed, 'How did you do that?'  I
  explained that the reset vector is located at physical address FFFF0
  and that the CS:IP value FFFF:0000 maps to that address in real
  mode.  After that, I went back to working on my lift control program
  and didn't think much more about the incident.
</p>
<p>
  About a week later, the same friend came to my dorm room.  He sat
  down with a grave look on his face and asked, 'How did you know to
  do that?  How did it occur to you to jump to the reset vector?'  I
  must have said something like, 'It just occurred to me.  I
  remembered that detail from the lecture and wanted to try it out.'
  He then said, 'I want to be able to think like that.  I come top of
  the class every semester, but I don't think the way you do.  I would
  never have thought of taking a small detail like that and testing it
  myself.'  I replied that I was just curious to see whether what we
  had learnt actually worked in practice.  He responded, 'And that's
  exactly it.  It would never occur to me to try something like that.
  I feel disappointed that I keep coming top of the class, yet I am
  not curious in the same way you are.  I've decided I don't want to
  top the class anymore.  I just want to explore and experiment with
  what we learn, the way you do.'
</p>
<p>
  That was all he said before getting up and heading back to his dorm
  room.  I didn't take it very seriously at the time.  I couldn't
  imagine why someone would willingly give up the accomplishment of
  coming first every semester.  But he kept his word.  He never topped
  the class again.  He still ranked highly, often within the top ten,
  but he kept his promise of never finishing first again.  To this
  day, I feel a mix of embarrassment and pride whenever I recall that
  incident.  With a single jump to the processor's reset entry point,
  I had somehow inspired someone to step back from academic
  competition in order to have more fun with learning.  Of course,
  there is no reason one cannot do both.  But in the end, that was his
  decision, not mine.
</p>
<h2 id="man-in-the-middle">Man in the Middle<a href="#man-in-the-middle"></a></h2>
<p>
  In my first job after university, I was assigned to a technical
  support team where part of my work involved running an installer to
  deploy a specific component of an e-banking product for customers,
  usually large banks.  As I learnt to use the installer, I realised
  how fragile it was.  The installer, written in Python, often failed
  because of incorrect assumptions about the target environment and
  almost always required some manual intervention to complete
  successfully.  During my first week on the project, I spent much of
  my time stabilising the installer and writing a step-by-step user
  guide explaining how to use it.  The result was well received by
  both my seniors and management.  To my surprise, the user guide
  received more praise than the improvements I made to the installer.
  While the first few weeks were productive, I soon realised I would
  not find the work fulfilling for long.  I wrote to management a few
  times to ask whether I could transfer to a team where I could work
  on something more substantial.
</p>
<p>
  My emails were initially met with resistance.  After several rounds
  of discussion, someone who had heard about my situation reached out
  and suggested a team whose manager might be interested in
  interviewing me.  The team was based in a different city.  I was
  young and willing to relocate wherever I could find good work, so I
  immediately agreed to the interview.
</p>
<p>
  This was in 2006, when video conferencing was not yet common.  On
  the day of the interview, the hiring manager called me on my office
  desk phone.  He began by introducing the team, which was called
  <em>Archie</em>, short for <em>architecture</em>.  The team
  developed and maintained the web framework and core architectural
  components on which the entire e-banking product was built.  The
  product had existed long before open source frameworks such as
  Spring or Django came into existence, so features such as API
  routing, authentication and authorisation layers, cookie management,
  etc. were all implemented in-house as Java Servlets and JavaServer
  Pages (JSP).  Since the software was used in banking environments,
  it also had to pass security testing and regular audits to minimise
  the risk of serious flaws.
</p>
<p>
  The interview began well.  He asked several questions related to
  software security, such as what SQL injection is, how it can be
  prevented and how one might design a web framework that mitigates
  cross-site scripting attacks.  He also asked me a few programming
  questions, most which I answered pretty well.  Towards the end,
  however, he asked how we could prevent MITM attacks.  I had never
  heard the term, so I admitted that I did not know what MITM meant.
  He then asked, 'Man in the middle?' but I still had no idea what
  that meant or whether it was even a software engineering concept.
  He replied, 'Learn everything you can about PKI and MITM.  We need
  to build a digital signatures feature for one of our corporate
  banking products.  That's the first thing we'll work on.'
</p>
<p>
  Over the next few weeks, I studied RFCs and documentation related to
  public key infrastructure, public key cryptography standards and
  related topics.  At first, the material felt intimidating, but after
  spending time each evening reading whatever relevant literature I
  could find, things gradually began to make sense.  Concepts that
  initially seemed complex and overwhelming eventually felt intuitive
  and elegant.  I relocated to the new city a few weeks later and
  delivered the digital signatures feature about a month after joining
  the team.  We used the open source Bouncy Castle library to
  implement the feature.  After that project, I worked on other parts
  of the product too.  The most rewarding part was knowing that the
  code I was writing became part of a mature product used by hundreds
  of banks and millions of users.  It was especially satisfying to see
  the work pass security testing and audits and be considered ready
  for release.
</p>
<p>
  That was my first real engineering job.  My manager also turned out
  to be an excellent mentor.  Working with him helped me develop new
  skills and his encouragement gave me confidence that stayed with me
  for years.  Nearly two decades have passed since then, yet the
  product is still in service and continues to be actively developed.
  In fact, in my current phase of life I sometimes encounter it as a
  customer.  Occasionally, I open the browser's developer tools to
  view the page source where I can still see traces of the HTML
  generated by code I wrote almost twenty years ago.
</p>
<h2 id="sphagetti-code">Sphagetti Code<a href="#sphagetti-code"></a></h2>
<p>
  Around 2007 or 2008, I began working on a proof of concept for
  developing widgets for an OpenTV set-top box.  The work involved
  writing code in a heavily trimmed-down version of C.  One afternoon,
  while making good progress on a few widgets, I noticed that they
  would occasionally crash at random.  I tried tracking down the bugs,
  but I was finding it surprisingly difficult to understand my own
  code.  I had managed to produce some truly spaghetti code full of
  dubious pointer operations that were almost certainly responsible
  for the crashes, yet I could not pinpoint where exactly things were
  going wrong.
</p>
<p>
  Ours was a small team of four people, each working on an independent
  proof of concept.  The most senior person on the team acted as our
  lead and architect.  Later that afternoon, I showed him my progress
  and explained that I was still trying to hunt down the bugs causing
  the widgets to crash.  He asked whether he could look at the code.
  After going through it briefly and probably realising that it was a
  bit of a mess, he asked me to send him the code as a tarball, which
  I promptly did.
</p>
<p>
  He then went back to his desk to study the code.  I remember
  thinking that there was no way he was going to find the problem
  anytime soon.  I had been debugging it for hours and barely
  understood what I had written myself; it was the worst spaghetti
  code I had ever produced.  With little hope of a quick solution, I
  went back to debugging on my own.
</p>
<p>
  Barely five minutes later, he came back to my desk and asked me to
  open a specific file.  He then showed me exactly where the pointer
  bug was.  It had taken him only a few minutes not only to read my
  tangled code but also to understand it well enough to identify the
  fault and point it out.  As soon as I fixed that line, the crashes
  disappeared.  I was genuinely in awe of his skill.
</p>
<p>
  I have always loved computing and programming, so I had assumed I
  was already fairly good at it.  That incident, however, made me
  realise how much further I still had to go before I could consider
  myself a good software developer.  I did improve significantly in
  the years that followed and today I am far better at managing
  software complexity than I was back then.
</p>
<h2 id="animated-television-widgets">Animated Television Widgets<a href="#animated-television-widgets"></a></h2>
<p>
  In another project from that period, we worked on another set-top
  box platform that supported Java Micro Edition (Java ME) for widget
  development.  One day, the same architect from the previous story
  asked whether I could add animations to the widgets.  I told him
  that I believed it should be possible, though I'd need to test it to
  be sure.  Before continuing with the story, I need to explain how
  the different stakeholders in the project were organised.
</p>
<p>
  Our small team effectively played the role of the software vendor.
  The final product going to market would carry the brand of a major
  telecom carrier, offering direct-to-home (DTH) television services,
  with the set-top box being one of the products sold to customers.
  The set-top box was manufactured by another company.  So the project
  was a partnership between three parties: our company as the software
  vendor, the telecom carrier and the set-top box manufacturer.  The
  telecom carrier wanted to know whether widgets could be animated on
  screen with smooth slide-in and slide-out effects.  That was why the
  architect approached me to ask whether it could be done.
</p>
<p>
  I began working on animating the widgets.  Meanwhile, the architect
  and a few senior colleagues attended a business meeting with all the
  partners present.  During the meeting, he explained that we were
  evaluating whether widget animations could be supported.  The
  set-top box manufacturer immediately dismissed the idea, saying,
  'That's impossible.  Our set-top box does not support animation.'
  When the architect returned and shared this with us, I replied, 'I
  do not understand.  If I can draw a widget, I can animate it too.
  All it takes is clearing the widget and redrawing it at slightly
  different positions repeatedly.  In fact, I already have a working
  version.'  I then showed a demo of the animated widgets running on
  the emulator.
</p>
<p>
  The following week, the architect attended another partners' meeting
  where he shared updates about our animated widgets.  I was not
  personally present, so what follows is second-hand information
  passed on by those who were there.  I learnt that the set-top box
  company reacted angrily.  For some reason, they were unhappy that we
  had managed to achieve results using their set-top box and APIs that
  they had officially described as impossible.  They demanded that we
  stop work on animation immediately, arguing that our work could not
  be allowed to contradict their official position.  At that point,
  the telecom carrier's representative intervened and bluntly told the
  set-top box representative to just shut up.  If the set-top box guy
  was furious, the telecom guy was even more so, 'You guys told us
  animation was not possible and these people are showing that it is!
  You manufacture the set-top box.  How can you not know what it is
  capable of?'
</p>
<p>
  Meanwhile, I continued working on the proof of concept.  It worked
  very well in the emulator, but I did not yet have access to the
  actual hardware.  The device was still in the process of being
  shipped to us, so all my early proof-of-concepts ran on the
  emulator.  The following week, the architect planned to travel to
  the set-top box company's office to test my widgets on the real
  hardware.
</p>
<p>
  At the time, I was quite proud of demonstrating results that even
  the hardware maker believed were impossible.  When the architect
  eventually travelled to test the widgets on the actual device, a
  problem emerged.  What looked like buttery smooth animation on the
  emulator appeared noticeably choppy on a real television.  Over the
  next few weeks, I experimented with frame rates, buffering
  strategies and optimising the computation done in the the rendering
  loop.  Each week, the architect travelled for testing and returned
  with the same report: the animation had improved somewhat, but it
  still remained choppy.  The modest embedded hardware simply could
  not keep up with the required computation and rendering.  In the
  end, the telecom carrier decided that no animation was better than
  poor animation and dropped the idea altogether.  So in the end, the
  set-top box developers turned out to be correct after all.
</p>
<h2 id="good-blessings">Good Blessings<a href="#good-blessings"></a></h2>
<p>
  Back in 2009, after completing about a year at RSA Security, I began
  looking for work that felt more intellectually stimulating,
  especially projects involving mathematics and algorithms.  I spoke
  with a few senior leaders about this, but nothing materialised for
  some time.  Then one day, Dr Burt Kaliski, Chief Scientist at RSA
  Laboratories, asked to meet me to discuss my career aspirations.  I
  have written about this in more detail in another post here:
  <a href="good-blessings.html">Good Blessings</a>.  I will summarise
  what followed.
</p>
<p>
  Dr Kaliski met me and offered a few suggestions about the kinds of
  teams I might approach to find more interesting work.  I followed
  his advice and eventually joined a team that turned out to be an
  excellent fit.  I remained with that team for the next six years.
  During that time, I worked on parser generators, formal language
  specification and implementation, as well as indexing and querying
  engines of a petabyte-scale database.  I learnt something new almost
  every day during those six years.  It remains one of the most
  enjoyable periods of my career.  I have especially fond memories of
  working on parser generators alongside remarkably skilled engineers
  from whom I learnt a lot.
</p>
<p>
  Years later, I reflected on how that brief meeting with Dr Kaliski
  had altered the trajectory of my career.  I realised I was not sure
  whether I had properly expressed my gratitude to him for the role he
  had played in shaping my path.  So I wrote to thank him and explain
  how much that single conversation had influenced my life.  A few
  days later, Dr Kaliski replied, saying he was glad to know that the
  steps I took afterwards had worked out well.  Before ending his
  message, he wrote this heart-warming note:
</p>
<blockquote>
  &lsquo;One of my goals is to be able to provide encouragement to
  others who are developing their careers, just as others have
  invested in mine, passing good blessings from one generation to
  another.&rsquo;
</blockquote>
<h2 id="the-ctf-scoreboard">The CTF Scoreboard<a href="#the-ctf-scoreboard"></a></h2>
<p>
  This story comes from 2019.  By then, I was no longer a
  twenty-something engineer just starting out.  I was now a
  middle-aged staff engineer with years of experience building both
  low-level networking systems and database systems.  Most of my work
  up to that point had been in C and C++.  I was now entering a new
  phase of my career where I would be leading the development of
  microservices written in Go and Python.  Like many people in this
  profession, computing has long been one of my favourite hobbies.  So
  although my professional work for the previous decade had focused on
  C and C++, I had plenty of hobby projects in other languages,
  including Python and Go.  As a result, switching gears from systems
  programming to application development was a smooth transition for
  me.  I cannot even say that I missed working in C and C++.  After
  all, who wants to spend their days occasionally chasing memory bugs
  in core dumps when you could be building features and delivering
  real value to customers?
</p>
<p>
  In October 2019, during Cybersecurity Awareness Month, a Capture the
  Flag (CTF) event was organised at our office.  The contest featured
  all kinds of technical puzzles, ranging from SQL injection
  challenges to insecure cryptography problems.  Some challenges also
  involved reversing binaries and exploiting stack overflow issues.
</p>
<p>
  I am usually rather intimidated by such contests.  The whole idea of
  competitive problem-solving under time pressure tends to make me
  nervous.  But one of my colleagues persuaded me to participate in
  the CTF.  And, somewhat to my surprise, I turned out to be rather
  good at it.  Within about eight hours, I had solved roughly 90% of
  the puzzles.  I finished at the top of the scoreboard.
</p>
<figure>
  <img src="files/blog/ctf-2019.png" alt="Scoreboard of a Capture the Flag (CTF) event">
  <figcaption>
    CTF Scoreboard
  </figcaption>
</figure>
<p>
  In my younger days, I was generally known to be a good problem
  solver.  I was often consulted when thorny problems needed solving
  and I usually managed to deliver results.  I also enjoyed solving
  puzzles.  I had a knack for them and happily spent hours, sometimes
  days, working through obscure mathematical or technical puzzles and
  sharing detailed write-ups with friends of the nerd variety.  Seen
  in that light, my performance at the CTF probably should not have
  surprised me.  Still, I was very pleased.  It was reassuring to know
  that I could still rely on my systems programming experience to
  solve obscure challenges.
</p>
<p>
  During the course of the contest, my performance became something of
  a talking point in the office.  Colleagues occasionally stopped by
  my desk to appreciate my progress in the CTF.  Two much younger
  colleagues, both engineers I admired for their skill and
  professionalism, were discussing the results nearby.  They were
  speaking softly, but I could still overhear parts of their
  conversation.  Curious, I leaned slightly and listened a bit more
  carefully.  I wanted to know what these two people, whom I admired a
  lot, thought about my performance.
</p>
<p>
  One of them remarked on how well I was doing in the contest.  The
  other replied, 'Of course he is doing well.  He has more than ten
  years of experience in C.'  At that moment, I realised that no
  matter how well I solved those puzzles, the result would naturally
  be credited to experience.  In my younger days, when I solved tricky
  problems like these, people would sometimes call me smart.  Now
  people simply saw it as a consequence of my experience.  Not that I
  particularly care for labels such as 'smart' anyway, but it did make
  me realise how things had changed.  I was now simply the person with
  many years of experience.  Solving technical puzzles that involved
  disassembling binaries, tracing execution paths and reconstructing
  program logic was expected rather than remarkable.
</p>
<p>
  I continue to sharpen my technical skills to this day.  While my
  technical results may now simply be attributed to experience, I hope
  I can continue to make a good impression through my professionalism,
  ethics and kindness towards the people I work with.  If those leave
  a lasting impression, that is good enough for me.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/twenty-five-years-of-computing.html">Read on website</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a> |
  <a href="https://susam.net/tag/programming.html">#programming</a>
</p>
]]>
</description>
</item>
<item>
<title>Jan '26 Notes</title>
<link>https://susam.net/26a.html</link>
<guid isPermaLink="false">ntjts</guid>
<pubDate>Thu, 29 Jan 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  In these monthly notes, I jot down ideas and references I
  encountered during the month that I did not have time to expand into
  their own posts.  A few of these may later develop into independent
  posts but most of them will likely not.  In any case, this format
  ensures that I record them here.  I spent a significant part of this
  month studying the book <em>Algebraic Graph Theory</em> by Godsil
  and Royle, so many of the notes here are about it.  There are a few
  non-mathematical, technical notes towards the end.
</p>
<h2 id="contents">Contents<a href="#contents"></a></h2>
<ol>
  <li><a href="#cayley-graphs">Cayley Graphs</a></li>
  <li><a href="#vertex-transitive-graphs">Vertex-Transitive Graphs</a></li>
  <li><a href="#arc-transitive-graphs">Arc-Transitive Graphs</a></li>
  <li><a href="#bipartite-graphs-and-cycle-parity">Bipartite Graphs and Cycle Parity</a></li>
  <li><a href="#tutte-theorem">Tutte's Theorem</a></li>
  <li><a href="#tutte-8-cage">Tutte's 8-Cage</a></li>
  <li><a href="#lcg">Linear Congruential Generator</a></li>
  <li><a href="#cat-n">Numbering Lines</a></li>
</ol>
<h2 id="cayley-graphs">Cayley Graphs<a href="#cayley-graphs"></a></h2>
<p>
  Let \( G \) be a group and let \( C \subseteq G \) such that \( C \)
  is closed under taking inverses and does not contain the identity,
  i.e.

  \[
    \forall x \in C, \; x^{-1} \in C, \qquad e \notin C.
  \]

  Then the Cayley graph \( X(G, C) \) is the graph with the vertex set
  \( V(X(G, C)) \) and edge set \( E(X(G, C)) \) defined by

  \begin{align*}
    V(X(G, C)) &amp;= G, \\
    E(X(G, C)) &amp;= \{ gh : hg^{-1} \in C \}.
  \end{align*}

  The set \( C \) is known as the connection set.
</p>
<h2 id="vertex-transitive-graphs">Vertex-Transitive Graphs<a href="#vertex-transitive-graphs"></a></h2>
<p>
  A graph \( X \) is <em>vertex-transitive</em> if its automorphism
  group acts transitively on its set of vertices \( V(X).  \)
  Intuitively, this means that no vertex has a special role.  We can
  'move' the graph around so that any chosen vertex becomes any other
  vertex.  In other words, all vertices are indistinguishable.  The
  graph looks the same from each vertex.
</p>
<p>
  The \( k \)-cube \( Q_k \) is vertex-transitive.  So are the Cayley
  graphs \( X(G, C).  \)  However the path graph \( P_3 \) is not
  vertex-transitive since no automorphism can send the middle vertex
  of valency \( 2 \) to an end vertex of valency \( 1.  \)
</p>
<h2 id="arc-transitive-graphs">Arc-Transitive Graphs<a href="#arc-transitive-graphs"></a></h2>
<p>
  The cube \( Q_3 \) is \( 2 \)-arc-transitive but not \( 3
  \)-arc-transitive.  In \( Q_3, \) a \( 3 \)-arc belonging to a \( 4
  \)-cycle cannot be sent to a \( 3 \)-arc that does not belong to a
  \( 4 \)-cycle.  This is easy to explain.  The end vertices of a \( 3
  \)-arc belonging to a \( 4 \)-cycle are adjacent but the end
  vertices of a \( 3 \)-arc not belonging to a \( 4 \)-cycle are not
  adjacent.  Therefore, no automorphism can map the end vertices of
  the first \( 3 \)-arc to those of the second \( 3 \)-arc.
</p>
<p>
  For intuition, imagine that a traveller stands on a vertex and
  chooses an edge to move along.  They do this \( s \) times thereby
  walking along an arc of length \( s, \) also known as an \( s
  \)-arc.  By the definition of \( s \)-arcs, the traveller is not
  allowed to backtrack from one vertex to the previous one
  immediately.  In an \( s \)-arc-transitive graph, these arcs look
  the same no matter which vertex they start from or which edges they
  choose.  In the cube, this is indeed true for \( s = 2.  \)  All arcs
  of length \( 2 \) are indistinguishable.  No matter which arc of
  length \( 2 \) the traveller has walked along, the graph would look
  the same from their perspective at each vertex along the arc.
  However, this no longer holds good for arcs of length \( 3 \) since
  there are two distinct kinds of arcs of length \( 3.  \)  The first
  kind ends at a distance of \( 1 \) from the starting vertex of the
  arc (when the arc belongs to a \( 4 \)-cycle).  The second kind ends
  at a distance \( 3 \) from the starting vertex of the arc (when the
  arc does not belong to a \( 4 \)-cycle).  Therefore the cube is not
  \( 3 \)-arc-transitive.
</p>
<h2 id="bipartite-graphs-and-cycle-parity">Bipartite Graphs and Cycle Parity<a href="#bipartite-graphs-and-cycle-parity"></a></h2>
<p>
  A graph is bipartite if and only if it contains no cycles of odd
  length.  Equivalently, every cycle in a bipartite graph has even
  length.  Conversely, if every cycle in a graph has even length, then
  the graph is bipartite.
</p>
<h2 id="tutte-theorem">Tutte's Theorem<a href="#tutte-theorem"></a></h2>
<p>
  For any \( s \)-arc-transitive cubic graph, \( s \le 5.  \)  This was
  demonstrated by W. T. Tutte in 1947.  A proof can be found in
  Chapter 18 of <em>Algebraic Graph Theory</em> by Norman Biggs.
</p>
<p>
  In 1973, Richward Weiss established a more general theorem that
  proves that for any \( s \)-arc-transitive graph, \( s \le 7.  \)
  The bound is weaker but it applies to all graphs rather than only to
  cubic ones.
</p>
<h2 id="tutte-8-cage">Tutte's 8-Cage<a href="#tutte-8-cage"></a></h2>
<p>
  The book <em>Algebraic Graph Theory</em> by Godsil and Royle offers
  the following two descriptions of Tutte's 8-cage on 30 vertices:
</p>
<blockquote>
  Take the cube and an additional vertex \( \infty.  \)  In each set of
  four parallel edges, join the midpoint of each pair of opposite
  edges by an edge, then join the midpoint of the two new edges by an
  edge, and finally join the midpoint of this edge to \( \infty.  \)
</blockquote>
<blockquote>
  Construct a bipartite graph \( T \) with the fifteen edges as one
  colour class and the fifteen \( 1 \)-factors as the other, where
  each edge is adjacent to the three \( 1 \)-factors that contain it.
</blockquote>
<p>
  It can be shown that both descriptions construct a cubic bipartite
  graph on \( 30 \) vertices of girth \( 8.  \)  It can be further
  shown that there is a unique cubic bipartite graph on \( 30 \)
  vertices with girth \( 8.  \)  As a result both descriptions above
  construct the same graph.
</p>
<h2 id="lcg">Linear Congruential Generator<a href="#lcg"></a></h2>
<p>
  Here is a simple linear congruential generator (LCG) implementation
  in JavaScript:
</p>
<pre><code>function srand (seed) {
  let x = seed
  return function () {
    x = (1664525 * x + 1013904223) % 4294967296
    return x
  }
}</code></pre>
<p>
  Here is an example usage:
</p>
<pre><samp>&gt; <kbd>const rand = srand(0)</kbd>
undefined
&gt; <kbd>rand()</kbd>
1013904223
&gt; <kbd>rand()</kbd>
1196435762
&gt; <kbd>rand()</kbd>
3519870697</samp></pre>
<h2 id="cat-n">Numbering Lines<a href="#cat-n"></a></h2>
<p>
  Both BSD and GNU <code>cat</code> can number output lines with
  the <code>-n</code> option.  For example:
</p>
<pre><samp>$ <kbd>printf 'foo\nbar\nbaz\n' | cat -n</kbd>
     1  foo
     2  bar
     3  baz</samp></pre>
<p>
  However I have always used <code>nl</code> for this.  For example:
</p>
<pre><samp>$ <kbd>printf 'foo\nbar\nbaz\n' | nl</kbd>
     1  foo
     2  bar
     3  baz</samp></pre>
<p>
  While <code>nl</code> is
  <a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/nl.html">specified
  in POSIX</a>, the <code>cat -n</code> option
  <a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/cat.html">is
  not</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/26a.html">Read on website</a> |
  <a href="https://susam.net/tag/monthly.html">#monthly</a> |
  <a href="https://susam.net/tag/mathematics.html">#mathematics</a> |
  <a href="https://susam.net/tag/programming.html">#programming</a> |
  <a href="https://susam.net/tag/javascript.html">#javascript</a> |
  <a href="https://susam.net/tag/shell.html">#shell</a>
</p>
]]>
</description>
</item>
<item>
<title>QuickQWERTY 1.2.1</title>
<link>https://susam.net/code/news/quickqwerty/1.2.1.html</link>
<guid isPermaLink="false">qqoto</guid>
<pubDate>Tue, 27 Jan 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  QuickQWERTY 1.2.1 is now available.  QuickQWERTY is a web-based
  touch typing tutor for QWERTY keyboards that runs directly in the
  web browser.
</p>
<p>
  This release contains a minor bug fix in Unit 4.3.  Unit 4.3 is a
  'Control' unit that lets you practise typing partial words as well
  as full words.  In one place in this unit, the following sequence of
  partial and full words occurs:
</p>
<pre><code>l li lime lime</code></pre>
<p>
  The full word <code>lime</code> was incorrectly repeated twice.
  This has been fixed to:
</p>
<pre><code>l li lim lime</code></pre>
<p>
  To try out QuickQWERTY, go to
  <a href="../../../quickqwerty.html">quickqwerty.html</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/quickqwerty/1.2.1.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/programming.html">#programming</a>
</p>
]]>
</description>
</item>
<item>
<title>Attention Media &#x2260; Social Networks</title>
<link>https://susam.net/attention-media-vs-social-networks.html</link>
<guid isPermaLink="false">amnsm</guid>
<pubDate>Tue, 20 Jan 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  When web-based social networks started flourishing nearly two
  decades ago, they were genuinely social networks.  You would sign up
  for a popular service, follow people you knew or liked and read
  updates from them.  When you posted something, your followers would
  receive your updates as well.  Notifications were genuine.  The
  little icons in the top bar would light up because someone had sent
  you a direct message or engaged with something you had posted.
  There was also, at the beginning of this millennium, a general sense
  of hope and optimism around technology, computers and the Internet.
  Social networking platforms were one of the services that were part
  of what was called Web 2.0, a term used for websites built around
  user participation and interaction.  It felt as though the
  information superhighway was finally reaching its potential.  But
  sometime between 2012 and 2016, things took a turn for the worse.
</p>
<p>
  First came the infamous infinite scroll.  I remember feeling uneasy
  the first time a web page no longer had a bottom.  Logically, I knew
  very well that everything a browser displays is a virtual construct.
  There is no physical page.  It is just pixels pretending to be one.
  Still, my brain had learned to treat web pages as objects with a
  beginning and an end.  The sudden disappearance of that end
  disturbed my sense of ease.
</p>
<p>
  Then came the bogus notifications.  What had once been meaningful
  signals turned into arbitrary prompts.  Someone you followed had
  posted something unremarkable and the platform would surface it as a
  notification anyway.  It didn't matter whether the notification was
  relevant to me.  The notification system stopped serving me and
  started serving itself.  It felt like a violation of an unspoken
  agreement between users and services.  Despite all that, these
  platforms still remained social in some diluted sense.  Yes, the
  notifications were manipulative, but they were at least about people
  I actually knew or had chosen to follow.  That, too, would change.
</p>
<p>
  Over time, my timeline contained fewer and fewer posts from friends
  and more and more content from random strangers.  Using these
  services began to feel like standing in front of a blaring
  loudspeaker, broadcasting fragments of conversations from all over
  the world directly in my face.  That was when I gave up on these
  services.  There was nothing social about them anymore.  They had
  become <em>attention media</em>.  My attention is precious to me.  I
  cannot spend it mindlessly scrolling through videos that have
  neither relevance nor substance.
</p>
<p>
  But where one avenue disappeared, another emerged.  A few years ago,
  I stumbled upon Mastodon and it reminded me of the early days of
  Twitter.  Back in 2006, I followed a small number of folks of the
  nerd variety on Twitter and received genuinely interesting updates
  from them.  But when I log into the ruins of those older platforms
  now, all I see are random videos presented to me for reasons I can
  neither infer nor care about.  Mastodon, by contrast, still feels
  like social networking in the original sense.  I follow a small
  number of people I genuinely find interesting and I receive their
  updates and only their updates.  What I see is the result of my own
  choices rather than a system trying to capture and monetise my
  attention.  There are no bogus notifications.  The timeline feels
  calm and predictable.  If there are no new updates from people I
  follow, there is nothing to see.  It feels closer to how social
  networks used to work originally.  I hope it stays that way.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/attention-media-vs-social-networks.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Nested Code Fences in Markdown</title>
<link>https://susam.net/nested-code-fences.html</link>
<guid isPermaLink="false">ncfim</guid>
<pubDate>Mon, 19 Jan 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  Today, we will meet a spiky-haired nerd named Corey Dumm, who
  normally lives within Markdown code fences.  We will get to know him
  a bit, smile with him when his fences hold and weep quietly when
  misfortune strikes.
</p>
<p>
  One of the caveats of the Markdown universe is the wide variety of
  Markdown implementations available.  In these parallel universes,
  the rules of Markdown rendering differ subtly.  In this post, we
  will focus only on the CommonMark specification.  Since GitHub
  Flavoured Markdown (GFM) is a strict superset of CommonMark,
  whatever we discuss here applies equally well to both CommonMark and
  GFM.
</p>
<h2 id="contents">Contents<a href="#contents"></a></h2>
<ul>
  <li><a href="#basic-code-fences">Basic Code Fences</a></li>
  <li><a href="#fancy-code-fences">Fancy Code Fences</a></li>
  <li><a href="#basic-code-spans">Basic Code Spans</a></li>
  <li><a href="#fancy-code-spans">Fancy Code Spans</a></li>
  <li><a href="#specification">Specification</a></li>
</ul>
<h2 id="basic-code-fences">Basic Code Fences<a href="#basic-code-fences"></a></h2>
<p>
  Corey had a knack for working with computers ever since he was a
  kid.
</p>
<!-- Markdown #1 -->
<pre><code>Corey at his computer:

```
(o_o)--.|[_]|
```</code></pre>
<p>
  Everything was perfect in Corey's world.  The CommonMark renderer
  would convert the Markdown above to the following HTML:
</p>
<!-- Rendered HTML #1 -->
<div class="box">
<p>Corey at his computer:</p>
<pre><code>(o_o)--.|[_]|
</code></pre>
</div>
<!-- Raw HTML #1 -->
<details>
<summary>View HTML</summary>
<pre><code>&lt;p&gt;Corey at his computer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(o_o)--.|[_]|
&lt;/code&gt;&lt;/pre&gt;</code></pre>
</details>
<p>
  At this point, all was well.  Corey grew quickly.  Before long, he
  had a head full of spiky hair.  Then the fences began to matter.
</p>
<!-- Markdown #2 -->
<pre><code>Corey, all grown up:

```
 ```
(o_o)--.|[_]|
```</code></pre>
<p>
  Let us see how this renders.  I must warn you that during the
  Markdown-to-HTML translation, Corey loses his hair.  Some viewers
  may find the following scene disturbing.  Viewer discretion is
  advised.  Here is the rendered HTML:
</p>
<!-- Rendered HTML #2 -->
<div class="box">
<p>Corey, all grown up:</p>
<pre><code></code></pre>
<p>(o_o)--.|[_]|</p>
<pre><code></code></pre>
</div>
<!-- Raw HTML #2 -->
<details>
<summary>View HTML</summary>
<pre><code>&lt;p&gt;Corey, all grown up:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(o_o)--.|[_]|&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;</code></pre>
</details>
<p>
  Corey's hair is gone!  What a catastrophic accident!  Corey is
  alright, though.  He is still quite afraid of Markdown fences, but
  otherwise well and bouncing back.  Why did this happen?  The second
  set of triple backticks immediately ends the fenced code block
  started by the first set of triple backticks.  As a result, Corey's
  smiley face ends up outside the fenced code block.  The triple
  backticks that were once Corey's hair are now woven into the fabric
  of the surrounding HTML.  Fortunately, CommonMark offers a few ways
  to avoid such accidents.
</p>
<h2 id="fancy-code-fences">Fancy Code Fences<a href="#fancy-code-fences"></a></h2>
<p>
  In CommonMark, fenced code blocks are most commonly written using
  triple backticks.  However, the specification also allows tildes as
  an alternative fence.  This can be useful when the code itself
  contains backticks.  Let us see an example:
</p>
<!-- Markdown #3a -->
<pre><code>Corey, all grown up:

~~~
 ```
(o_o)--.|[_]|
~~~</code></pre>
<p>
  In fact, a code fence need not consist of exactly three backticks or
  tildes.  Any number of backticks or tildes is allowed, as long as
  that number is at least three.  The following is therefore
  equivalent:
</p>
<!-- Markdown #3b -->
<pre><code>Corey, all grown up:

~~~~~
 ```
(o_o)--.|[_]|
~~~~~</code></pre>
<p>
  And so is this:
</p>
<!-- Markdown #3c -->
<pre><code>Corey, all grown up:

`````
 ```
(o_o)--.|[_]|
`````</code></pre>
<p>
  All three examples render like this:
</p>
<!-- Rendered HTML #3 -->
<div class="box">
<p>Corey, all grown up:</p>
<pre><code> ```
(o_o)--.|[_]|
</code></pre>
</div>
<!-- Raw HTML #3 -->
<details>
<summary>View HTML</summary>
<pre><code>&lt;p&gt;Corey, all grown up:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; ```
(o_o)--.|[_]|
&lt;/code&gt;&lt;/pre&gt;</code></pre>
</details>
<p>
  No hair is lost in translation.
</p>
<h2 id="basic-code-spans">Basic Code Spans<a href="#basic-code-spans"></a></h2>
<p>
  A similar problem arises with inline code spans.  Most Markdown
  users know to use backticks to delimit inline code spans.  For
  example:
</p>
<!-- Markdown 4 -->
<pre><code>An old picture of Corey at his computer: `(o_o)--.|[_]|`</code></pre>
<p>
  This produces the following output:
</p>
<!-- Rendered HTML 4 -->
<div class="box">
<p>An old picture of Corey at his computer: <code>(o_o)--.|[_]|</code></p>
</div>
<!-- Raw HTML 4 -->
<details>
<summary>View HTML</summary>
<pre><code>&lt;p&gt;An old picture of Corey at his computer: &lt;code&gt;(o_o)--.|[_]|&lt;/code&gt;&lt;/p&gt;</code></pre>
</details>
<p>
  However, what do we do when we need to put Corey's dear friend Becky
  Trace within an inline code span?  Becky has short, straight hair
  tucked neatly on either side of her face.  Here's a picture of her:
</p>
<p class="textcenter">
  <code>`(o_o)`</code>
</p>
<p>
  I believe you can already see the difficulty here.  Inline code
  spans use backticks as delimiters.  So when we put Becky within a
  code span, the first backtick in Becky's face would terminate the
  code span immediately and then the rest of Becky would lie outside
  it.  CommonMark offers solutions for this kind of situation as well.
</p>
<h2 id="fancy-code-spans">Fancy Code Spans<a href="#fancy-code-spans"></a></h2>
<p>
  An inline code span delimiter need not consist of exactly one
  backtick.  It can consist of any number of backticks.  So
  <code>`foo`</code> and <code>``foo``</code> produce identical HTML.
  There is another important but less well-known detail.  When the
  text inside an inline code span begins and ends with spaces, one
  space is removed from each end before rendering.  So
  <code>`foo`</code> and <code>`&nbsp;foo&nbsp;`</code> are
  equivalent.  Therefore, when we need to put backticks within an
  inline code span, we can start the code span using multiple
  backticks and a space.  For example:
</p>
<!-- Markdown #5 -->
<pre><code>Meet Corey's friend Becky Trace: `` `(o_o)` ``</code></pre>
<p>
  Here is the rendered output:
</p>
<!-- Rendered HTML #5 -->
<div class="box">
<p>Meet Corey's friend Becky Trace: <code>`(o_o)`</code></p>
</div>
<!-- Raw HTML #5 -->
<details>
<summary>View HTML</summary>
<pre><code>&lt;p&gt;Meet Corey's friend Becky Trace: &lt;code&gt;`(o_o)`&lt;/code&gt;&lt;/p&gt;</code></pre>
</details>
<p>
  Becky has her hair intact too.  We have avoided the mishap that once
  caused great distress to Corey.  That, my friends, is how backticks
  survive nesting in Markdown.
</p>
<h2 id="specification">Specification<a href="#specification"></a></h2>
<p>
  Before I finish this post, let us take a look at the CommonMark
  specification to see where these details are defined.  The excerpts
  quoted below are taken from
  <a href="https://spec.commonmark.org/0.30/">CommonMark Spec Version
  0.30</a>, which is by now over four years old.
</p>
<p>
  From section
  <a href="https://spec.commonmark.org/0.30/#fenced-code-blocks">4.5
  Fenced Code Blocks</a>:
</p>
<blockquote>
  <p>
    A <a href="https://spec.commonmark.org/0.30/#code-fence">code
    fence</a> is a sequence of at least three consecutive backtick
    characters (<code>`</code>) or tildes (<code>~</code>).  (Tildes
    and backticks cannot be mixed.)
  </p>
</blockquote>
<blockquote>
  <p>
    The content of the code block consists of all subsequent lines,
    until a closing
    <a href="https://spec.commonmark.org/0.30/#code-fence">code fence</a>
    of the same type as the code block began with (backticks or tildes),
    and with at least as many backticks or tildes as the opening code
    fence.
  </p>
</blockquote>
<p>
  From section
  <a href="https://spec.commonmark.org/0.30/#code-spans">6.1 Code
  Spans</a>:
</p>
<blockquote>
  <p>
    A <a href="https://spec.commonmark.org/0.30/#backtick-string">backtick
    string</a> is a string of one or more backtick characters
    (<code>`</code>) that is neither preceded nor followed by a
    backtick.
  </p>
  <p>
    A <a href="https://spec.commonmark.org/0.30/#code-span">code
    span</a> begins with a backtick string and ends with a backtick
    string of equal length.  The contents of the code span are the
    characters between these two backtick strings, normalized in the
    following ways:
  </p>
  <ul>
    <li>
      First, <a href="https://spec.commonmark.org/0.30/#line-ending">line endings</a>
      are converted to <a href="https://spec.commonmark.org/0.30/#space">spaces</a>.
    </li>
    <li>
      If the resulting string both begins <em>and</em> ends with a
      <a href="https://spec.commonmark.org/0.30/#space">space</a>
      character, but does not consist entirely of
      <a href="https://spec.commonmark.org/0.30/#space">space</a>
      characters, a single
      <a href="https://spec.commonmark.org/0.30/#space">space</a>
      character is removed from the front and back.  This allows you
      to include code that begins or ends with backtick characters,
      which must be separated by whitespace from the opening or
      closing backtick strings.
    </li>
  </ul>
</blockquote>
<p>
  I hope these little nuggets of Markdown trivialities will one day
  prove useful in your own Markdown misfortunes.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/nested-code-fences.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Minimal GitHub Workflow</title>
<link>https://susam.net/minimal-github-workflow.html</link>
<guid isPermaLink="false">mghwf</guid>
<pubDate>Thu, 15 Jan 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  This is a note where I capture the various errors we receive when we
  create GitHub workflows that are smaller than the smallest possible
  workflow.  I do not know why anyone would ever need this information
  and I doubt it will serve any purpose for me either but sometimes
  you just want to know things, no matter how useless they might be.
  This is one of the useless things I wanted to know today.
</p>
<h2 id="contents">Contents<a href="#contents"></a></h2>
<ul>
  <li><a href="#empty-workflow">Empty Workflow</a></li>
  <li><a href="#on">On</a></li>
  <li><a href="#on-push">On Push</a></li>
  <li><a href="#jobs">Jobs</a></li>
  <li><a href="#job-id">Job ID</a></li>
  <li><a href="#steps">Steps</a></li>
  <li><a href="#runs-on">Runs On</a></li>
  <li><a href="#runs-on-ubuntu-latest">Runs On Ubuntu Latest</a></li>
  <li><a href="#empty-steps">Empty Steps</a></li>
  <li><a href="#run">Run</a></li>
  <li><a href="#run-echo">Run Echo</a></li>
  <li><a href="#hello-world">Hello, World</a></li>
</ul>
<h2 id="empty-workflow">Empty Workflow<a href="#empty-workflow"></a></h2>
<p>
  For the first experiment we just create a zero byte file and push it
  to GitHub as follows, say, like this:
</p>
<pre><code>mkdir -p .github/workflows/
touch .github/workflows/hello.yml
git add .github/
git commit -m 'Empty workflow'
git push -u origin main</code></pre>
<p>
  Under the GitHub repo's <strong>Actions</strong> tab, we find this
  error:
</p>
<pre><samp>Error
No event triggers defined in `on`</samp></pre>
<h2 id="on">On<a href="#on"></a></h2>
<p>
  Then we update the workflow as follows:
</p>
<pre><code>on:</code></pre>
<p>
  Now we get this error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
(Line: 1, Col: 4): Unexpected value '', (Line: 1, Col: 1): Required property is missing: jobs</samp></pre>
<h2 id="on-push">On Push<a href="#on-push"></a></h2>
<p>
  Next update:
</p>
<pre><code>on: push</code></pre>
<p>
  Corresponding error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
(Line: 1, Col: 1): Required property is missing: jobs</samp></pre>
<h2 id="jobs">Jobs<a href="#jobs"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:</code></pre>
<p>
  Error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
(Line: 2, Col: 6): Unexpected value ''</samp></pre>
<h2 id="job-id">Job ID<a href="#job-id"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:</code></pre>
<p>
  Error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
(Line: 3, Col: 9): Unexpected value ''</samp></pre>
<h2 id="steps">Steps<a href="#steps"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:
    steps:</code></pre>
<p>
  Error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
(Line: 4, Col: 11): Unexpected value '', (Line: 4, Col: 5): Required property is missing: runs-on</samp></pre>
<h2 id="runs-on">Runs On<a href="#runs-on"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:
    runs-on:
    steps:</code></pre>
<p>
  Error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
(Line: 4, Col: 13): Unexpected value '', (Line: 5, Col: 11): Unexpected value ''</samp></pre>
<h2 id="runs-on-ubuntu-latest">Runs On Ubuntu Latest<a href="#runs-on-ubuntu-latest"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:
    runs-on: ubuntu-latest
    steps:</code></pre>
<p>
  Error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
(Line: 5, Col: 11): Unexpected value ''</samp></pre>
<h2 id="empty-steps">Empty Steps<a href="#empty-steps"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:
    runs-on: ubuntu-latest
    steps: []</code></pre>
<p>
  Error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
No steps defined in `steps` and no workflow called in `uses` for the following jobs: world</samp></pre>
<h2 id="run">Run<a href="#run"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:
    runs-on: ubuntu-latest
    steps:
      - run:</code></pre>
<p>
  Success:
</p>
<pre><samp>&#x25bc; Run

  shell: /usr/bin/bash -e {0}</samp></pre>
<h2 id="run-echo">Run Echo<a href="#run-echo"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:
    runs-on: ubuntu-latest
    steps:
      - run: echo</code></pre>
<p>
  Success:
</p>
<pre><samp>&#x25bc; Run
  echo
  shell: /usr/bin/bash -e {0}
</samp></pre>
<h2 id="hello-world">Hello, World<a href="#hello-world"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:
    runs-on: ubuntu-latest
    steps:
      - run: echo hello, world</code></pre>
<p>
  Success:
</p>
<pre><samp>&#x25bc; Run echo hello, world
  echo hello, world
  shell: /usr/bin/bash -e {0}
hello, world</samp></pre>
<p>
  The experiments are preserved in the commit history of
  <a href="https://github.com/spxy/minighwf">github.com/spxy/minighwf</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/minimal-github-workflow.html">Read on website</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>


</channel>
</rss>
