update blog post, and run credo with 'pi'
This commit is contained in:
parent
ceeeb994fb
commit
80046094b8
27
Makefile
Normal file
27
Makefile
Normal 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
32
app/Makefile
Normal 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,
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
defmodule Firehose.ReleaseNotes do
|
||||
@moduledoc """
|
||||
Release notes blog configuration.
|
||||
"""
|
||||
use Blogex.Blog,
|
||||
blog_id: :release_notes,
|
||||
app: :firehose,
|
||||
|
||||
@ -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)}>
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
61
planner_request.md
Normal 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.
|
||||
Loading…
x
Reference in New Issue
Block a user