update blog post, and run credo with 'pi'

This commit is contained in:
Willem van den Ende 2026-03-18 15:03:24 +00:00
parent ceeeb994fb
commit 80046094b8
15 changed files with 165 additions and 19 deletions

27
Makefile Normal file
View File

@ -0,0 +1,27 @@
# Makefile for Firehose monorepo
.PHONY: check precommit deps compile test format
# Common check target that runs all static analysis
check:
@echo "Running static analysis..."
@make -C app MISE_BIN=/home/vscode/.local/bin/mise check
# Precommit target for CI/pre-commit hooks
precommit: check
# Sync dependencies
deps:
@make -C app deps
# Compile the project
compile:
@make -C app compile
# Run tests
test:
@make -C app test
# Format code
format:
@make -C app format

32
app/Makefile Normal file
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

View File

@ -1,4 +1,7 @@
defmodule Firehose.EngineeringBlog do
@moduledoc """
Engineering blog configuration.
"""
use Blogex.Blog,
blog_id: :engineering,
app: :firehose,

View File

@ -1,4 +1,7 @@
defmodule Firehose.ReleaseNotes do
@moduledoc """
Release notes blog configuration.
"""
use Blogex.Blog,
blog_id: :release_notes,
app: :firehose,

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())

View File

@ -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"""

View File

@ -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>

View File

@ -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

View File

@ -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)}>

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

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"},

View File

@ -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.

View File

@ -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

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 """

61
planner_request.md Normal file
View File

@ -0,0 +1,61 @@
# Refactoring Plan for Firehose Blog Controller Tests
## Context
Based on context.md, we have a Phoenix blog controller with repetitive validation tests that need refactoring.
## Goals
1. Extract test helpers to reduce code duplication
2. Standardize test naming conventions
3. Reorganize tests to follow defensive programming flow
4. Add missing negative test coverage
5. Create separate contexts for different refactorings
## Recommended Planner Agents
### 1. TestHelperExtractor Agent
**Purpose**: Handle Smell 1 (Repetitive Validation Tests) and Smell 4 (Redundant Layout Assertions)
**Tasks**:
- Extract page validation test logic into `test_page_fallback/2` helper
- Create `assert_has_app_layout/1` helper for layout assertions
- Move helpers to support module or test case
**Context Isolation**: This can run in a separate test context without affecting controller logic.
### 2. TestOrganizer Agent
**Purpose**: Handle Smell 5 (Test Order) and Smell 3 (Inconsistent Naming)
**Tasks**:
- Reorder test blocks: validation first, then success cases, then edge cases
- Standardize all test descriptions to follow pattern: "GET /blog/:type/:slug returns [result]"
- Rename describe blocks to follow semantic order
**Context Isolation**: Pure test organization, no production code changes.
### 3. CoverageExpander Agent
**Purpose**: Handle Smell 2 (Missing Negative Tests) and Smell 8 (Edge Cases)
**Tasks**:
- Add test for unknown blog_id (`/blog/invalid`)
- Add test for empty page parameter (`?page=`)
- Add test for very large page numbers
- Add test for invalid blog halt behavior (Smell 6)
**Context Isolation**: Adds new tests without modifying existing logic.
### 4. ResponseHelperCreator Agent
**Purpose**: Handle Smell 7 (Mixed Response Types)
**Tasks**:
- Create `assert_html/2` and `assert_json/2` helpers
- Ensure proper content-type verification
- Update existing tests to use new helpers
## Execution Strategy
Run each agent in isolated contexts:
1. TestHelperExtractor → creates helper functions
2. ResponseHelperCreator → builds response assertions
3. TestOrganizer → reorganizes existing structure
4. CoverageExpander → adds new test cases
This keeps the main thread clean and allows focused changes per agent.