update blog post, and run credo with 'pi'

This commit is contained in:
2026-03-18 15:03:24 +00:00
parent ceeeb994fb
commit 80046094b8
15 changed files with 165 additions and 19 deletions
+32
View File
@@ -0,0 +1,32 @@
# Makefile for Firehose app
MISE_BIN ?= /home/vscode/.local/bin/mise
MISE_EXEC = $(MISE_BIN) exec --
.PHONY: check precommit deps compile test format credo
# Run all static analysis checks
check: credo format
# Precommit target for CI/pre-commit hooks
precommit: check compile
# Sync dependencies
deps:
$(MISE_EXEC) mix deps.get
# Compile the project
compile:
$(MISE_EXEC) mix compile --warnings-as-errors
# Run tests
test: deps
$(MISE_EXEC) mix test
# Format code
format:
$(MISE_EXEC) mix format
# Run Credo static analysis
credo:
$(MISE_EXEC) mix credo --strict
@@ -1,4 +1,7 @@
defmodule Firehose.EngineeringBlog do
@moduledoc """
Engineering blog configuration.
"""
use Blogex.Blog,
blog_id: :engineering,
app: :firehose,
+3
View File
@@ -1,4 +1,7 @@
defmodule Firehose.ReleaseNotes do
@moduledoc """
Release notes blog configuration.
"""
use Blogex.Blog,
blog_id: :release_notes,
app: :firehose,
+1 -1
View File
@@ -88,8 +88,8 @@ defmodule FirehoseWeb do
import FirehoseWeb.CoreComponents
# Common modules used in templates
alias Phoenix.LiveView.JS
alias FirehoseWeb.Layouts
alias Phoenix.LiveView.JS
# Routes generation with the ~p sigil
unquote(verified_routes())
@@ -29,6 +29,7 @@ defmodule FirehoseWeb.CoreComponents do
use Phoenix.Component
use Gettext, backend: FirehoseWeb.Gettext
alias Phoenix.HTML.Form
alias Phoenix.LiveView.JS
@doc """
@@ -181,7 +182,7 @@ defmodule FirehoseWeb.CoreComponents do
def input(%{type: "checkbox"} = assigns) do
assigns =
assign_new(assigns, :checked, fn ->
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
Form.normalize_value("checkbox", assigns[:value])
end)
~H"""
@@ -1,6 +1,9 @@
<header class="navbar px-4 sm:px-6 lg:px-8 border-b border-base-200">
<div class="flex-1">
<a href="/" class="font-display text-xl font-semibold tracking-tight text-primary hover:opacity-80 transition">
<a
href="/"
class="font-display text-xl font-semibold tracking-tight text-primary hover:opacity-80 transition"
>
firehose
</a>
</div>
@@ -58,6 +58,7 @@ defmodule FirehoseWeb.BlogController do
end
defp parse_page(nil), do: 1
defp parse_page(str) do
case Integer.parse(str) do
{page, ""} when page > 0 -> page
@@ -6,7 +6,12 @@
<div class="space-y-4 text-lg leading-relaxed text-base-content/80">
<p>
I'm <strong class="text-base-content">Willem van den Ende</strong>,
partner at <a href="https://qwan.eu" class="text-primary hover:underline" target="_blank" rel="noopener">QWAN</a>.
partner at <a
href="https://qwan.eu"
class="text-primary hover:underline"
target="_blank"
rel="noopener"
>QWAN</a>.
This is where I write about AI-native consulting, shitty evals,
and whatever prototype I'm building this week.
</p>
@@ -21,7 +26,9 @@
class="rounded-box border border-base-200 p-5 space-y-2 hover:border-primary/30 transition"
>
<a href={"#{post_base_path(post)}/#{post.id}"} class="block space-y-2">
<h3 class="font-semibold text-base-content hover:text-primary transition">{post.title}</h3>
<h3 class="font-semibold text-base-content hover:text-primary transition">
{post.title}
</h3>
<p class="text-sm text-base-content/60">{post.description}</p>
<div class="flex items-center gap-2 text-xs text-base-content/50">
<time datetime={Date.to_iso8601(post.date)}>
+2 -1
View File
@@ -66,7 +66,8 @@ defmodule Firehose.MixProject do
{:jason, "~> 1.2"},
{:dns_cluster, "~> 0.2.0"},
{:bandit, "~> 1.5"},
{:blogex, path: "../blogex"}
{:blogex, path: "../blogex"},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
]
end
+2
View File
@@ -1,6 +1,8 @@
%{
"bandit": {:hex, :bandit, "1.10.3", "1e5d168fa79ec8de2860d1b4d878d97d4fbbe2fdbe7b0a7d9315a4359d1d4bb9", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "99a52d909c48db65ca598e1962797659e3c0f1d06e825a50c3d75b74a5e2db18"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"credo": {:hex, :credo, "1.7.17", "f92b6aa5b26301eaa5a35e4d48ebf5aa1e7094ac00ae38f87086c562caf8a22f", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1eb5645c835f0b6c9b5410f94b5a185057bcf6d62a9c2b476da971cde8749645"},
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
@@ -7,20 +7,24 @@
}
---
I wrote about [publishing short posts](https://www.qwan.eu/2025/05/20/publish-short-posts.html) on the QWAN blog last year. Giving myself license to write shorter, rougher pieces. That worked for a while. But some things don't belong on a consultancy blog.
I wrote about [publishing short posts](https://www.qwan.eu/2025/05/20/publish-short-posts.html) on the QWAN blog last year. Giving myself license to write shorter, rougher pieces. That worked for a while. But some things don't feel like a good fit for the QWAN blog just yet.
When I prototype with a coding agent at 11pm, the thing I learn is not a polished QWAN insight. It's a half-formed observation about evals, or a trick for keeping the human in the loop, or just "I built this and here's what surprised me." The QWAN blog has a certain standard. This stuff needs somewhere scruffier to land.
This post was partially written with claude code, see the commit history [on our gitea](https://gitea.apps.sustainabledelivery.com/mostalive/firehose) if you want to check the differences.
Hence **Firehose** — named after what it feels like to work with AI coding agents. You're drinking from a firehose of generated code, suggestions, and decisions. The interesting question is not "how do I generate more" but "how do I stay in control of what's coming out."
When I prototype with a coding agent at 11pm (I should go to bed and write a post about sustainable pace the next day ;-) ), the thing I learn is not a polished QWAN insight. It's a half-formed observation about something that just happened, or a trick for keeping the human in the loop, or just "I built this and here's what surprised me."
That's also what this site is built with, by the way. The homepage, the blog engine, the layout — all built in conversation with Claude Code. I wanted to experience what our clients experience: shipping something real with an AI agent, and noticing where the friction is.
Hence **Firehose** — named after what it feels like to work with AI coding agents. You're drinking from a firehose of generated code, suggestions, and decisions. The interesting question is not "how do I generate more" but "how do I stay in control of what's coming out.". And also, currently, how do I generate just enough and focus on interesting feedback loops instead of code?
A few things I noticed, building this:
I wrote this last may as well [shallow research tool](https://www.qwan.eu/2025/05/01/agentic-search.html):
- **Layout inheritance is a design decision.** The blog engine rendered pages outside Phoenix's layout pipeline. Getting navbar and CSS onto blog pages meant rethinking how the pieces fit together — not just adding a wrapper div.
- **Warm aesthetics take intention.** The default Phoenix boilerplate is fine, but it says nothing about who you are. Choosing fonts and colours forced me to think about what "personal but professional" looks like.
- **It's fast when it works, and confusing when it doesn't.** When the agent understands your stack, you move at extraordinary speed. When it doesn't (say, the difference between `@inner_block` and `@inner_content` in Phoenix layouts), you can burn time on a misunderstanding that a human would catch in seconds.
> I want to both get better at using LLMs for programming, and also understand how they work. Marc suggested earlier this year that I write a series of blog posts about my use of them, but I have been drinking from a firehose, and it is quite difficult to figure out a good place to start writing.
I have made good progress in learning, and at the same time, practices are still evolving. I see people write patterns. I think it is useful, but too early for that. I am at heuristics (rules of thumb).
That is also why I open sourced the code for this blog [firehose repository on our gitea](https://gitea.apps.sustainabledelivery.com/mostalive/firehose). I think Jekyll, the static site generator we have for QWAN is passable, but I want the option to have a more interactive blog, and since this is going to be a firehose of ideas, give readers the option to subscribe to only what they are interested in, filter posts, like etc. I helped a friend with 'Ghost', but it felt clunky. I like writing in plain text and publishing with `git push` - that works with Jekyll and other static site generators.
I am exploring working in small slices. That does require some initial investment in modularity. If you look at the code, you will notice that some of the blogging functionality is separate from the main site. I want an 'engineering blog' and 'release notes' as a plugin for Software as a Service applications.
This is the space I want to write in. Shorter than a conference talk, longer than a LinkedIn post. Honest about what works and what doesn't.
If you're a CTO or engineering lead wondering what "AI-native development" actually looks like day to day — not the vendor pitch, the lived experience — that's what I'll be writing about here.
If you're wondering what "AI-native development" actually looks like day to day — not the vendor pitch, the lived experience — that's what I'll be writing about here.
@@ -74,7 +74,7 @@ defmodule FirehoseWeb.BlogTest do
conn = get(conn, "/api/blog/engineering")
assert %{"blog" => "engineering", "posts" => posts} = json_response(conn, 200)
assert is_list(posts)
assert length(posts) > 0
refute Enum.empty?(posts)
end
test "GET /api/blog/engineering/:slug returns a post", %{conn: conn} do
@@ -99,7 +99,7 @@ defmodule FirehoseWeb.BlogTest do
conn = get(conn, "/api/blog/releases")
assert %{"blog" => "release_notes", "posts" => posts} = json_response(conn, 200)
assert is_list(posts)
assert length(posts) > 0
refute Enum.empty?(posts)
end
test "GET /api/blog/releases/:slug returns a post", %{conn: conn} do
+3 -2
View File
@@ -18,6 +18,7 @@ defmodule Firehose.DataCase do
using do
quote do
alias Ecto.Adapters.SQL.Sandbox
alias Firehose.Repo
import Ecto
@@ -36,8 +37,8 @@ defmodule Firehose.DataCase do
Sets up the sandbox based on the test tags.
"""
def setup_sandbox(tags) do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Firehose.Repo, shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
pid = Sandbox.start_owner!(Firehose.Repo, shared: not tags[:async])
on_exit(fn -> Sandbox.stop_owner(pid) end)
end
@doc """