← Blog
Work notesEN

From external feedback to release: two DFD features in a production product

An external UX review led to two production features built with DFD and an agent. Honest gates, evidence on one machine, late requirements and a specification that found a bug before coding.

Two windows showing the same checklist: green on the left, empty dashed entries on the right, with a log file failing to travel between the machines.

In the first article in this series I argued that with agents, development cost does not disappear: it moves upstream to specification and downstream to verification. In the third and fourth, the strongest signal came from the same place: a gate saying “done” when it was not.

This fifth test has one important difference: I did not write the input. It was an external UX feedback document about Taita, the parenting app I am building, from someone who examined the product without having built it. It led to two features taken all the way to production with DFD and Claude Code.

Again, an experiment rather than a demonstration. One product, two features, one developer. The numbers are real and the approvals are mine: feedback came from outside; I signed off the gates.

The feedback and what it became

The document was short and practical: simpler registration, fewer fields and less text at login, a step-by-step first-use guide, and a school lunch menu workflow broken into simpler steps. It also covered broader product identity questions.

DFD immediately did something useful: it separated features from non-features. Product identity remained explicitly out of scope in both specifications. The rest became two features:

Feature 001: registration, login and first use Feature 003: school lunches step by step
Risk High: touches login and onboarding Low, with only one medium dimension
Path Full Light
Opening to merge About 20 hours About 4 hours
Tests Red and green recorded for all 10 tasks, frontend and backend; full CI green 42 new tests across 10 tasks
Accessibility axe in a real browser: no local violations; one production violation found and fixed 24 screens, two themes, no violations

Risk was not a feeling: the CLI calculates it from five dimensions, security, reliability, cost, user impact and architecture. Touching login raises impact to “high”, which alone requires the full path. School lunches changed only the interface over existing endpoints, so it followed the light path. This put the first article’s risk-proportionate reviews into practice on a real product.

What worked

The specification found a bug before coding. To write the “When does this menu start?” step, I asked the agent to check how the backend actually uses dates rather than copy the existing interface. The backend counts the “cycle week” from the start date, while the interface asked which week “we are in today”. Those coincide only if the menu starts this week. Without an end date, it also disappeared everywhere after three cycles, without warning. Tests would have confirmed the wrong behaviour. The requirement to write what every field means before designing it found the problem.

A real browser saw what tests could not. Feature 001’s first production accessibility audit found a violation absent locally: the “Log in with Telegram” button’s iframe had no title, because the widget only loads on the production domain. It became a DFD quick record, 002, explaining why the features are 001 and 003: a limited fix tracked in one file, completed that day. For feature 003, axe in Chromium found three contrast problems invisible to jsdom, the Node.js DOM implementation that does not calculate real rendered colours. One concerned the red “Delete” buttons used across the app. Fixing it improved every destructive button, rather than just the school lunch interface. This is shift-right on a small scale: the real signal is where the product runs.

Red also exposed bad tests. In TDD, tests are written and observed failing before implementation makes them pass. DFD requires the initial failure to come from the requirement rather than the environment. This caught three things:

  • A malformed shell loop made Jest fail because it could not find a file. A false red, which I discarded.
  • A security test failed before reaching its console assertion. A targeted red against the old code revealed the actual issue: the wizard logged the API response, which Sentry collected as a breadcrumb, an event trail attached to errors.
  • One assertion contradicted the specification, so I corrected the test.

Hashes raised the alarm. DFD records a SHA-256 fingerprint of every verified file. In feature 001, one file was “changed since evidence” even though nobody had touched it on main. Evidence had been recorded against a working copy containing an uncommitted change, a state that did not exist in the repository. Here the gate did its job.

What did not work

Evidence lived on a single machine. This was the biggest problem, resembling the fourth article. The project’s .gitignore excluded all *.log files, including DFD test logs. Local gates were green because the files existed. Every other checkout saw blocked features with missing evidence. For 001, original logs were still on disk and matched their hashes, so I committed them. Quick record 002’s logs were lost: I genuinely reran the checks, using the pre-fix component for red and rerunning CI after green. I did not invent the dates.

In the fourth article, the gate said “done” for stale state. Here it was honest, but honest on one machine only. The fix is one line, !.dfd/**/*.log, but the lesson is wider: evidence that does not travel with code is a memory rather than evidence.

Requirements arrived after work began. For 003, I currently make product decisions, and added two things after approving the design: “overload is visual too” during specification, and “users must choose how long to repeat the menu, because menus are seasonal” when implementation was almost finished. DFD handled the latter well: design moved to stale-review, requiring renewed approval and an updated plan, and the change had its own red. But this costs time. The framework makes cost visible rather than eliminating it. The first article’s “requirements emerge through use” acquired an explicit price: another approval, an updated plan and a substantial part of the feature’s four hours.

That requirement exposed missing production data. Offering “until the last school day” required regional school calendars. Production had only national holidays for 2026/27: no term dates, holidays or bridge days. This year’s school closure notifications were therefore empty too, unnoticed. The loading script would not run either. It was outside feature scope and became separate work. Without that data requirement, the gap would still be there.

Task-level TDD widens when one component covers everything. The plan had ten tasks, but six lived in the same component being rewritten: the old wizard was 1,583 lines. I wrote all tests first, then implemented. Each red still preceded its green, but “one task, one cycle” became mostly bookkeeping in a rewrite like this.

Approvals are a weak point. As a solo developer, I had the agent record every decision with --human-confirmed, based on chat answers, sometimes multiple choice. The CLI openly states it records a declared decision rather than authenticating the decision-maker. It works, but the “responsible human” and “developer” were the same person, with an agent transcribing.

Framework friction. A text check blocked release review because a note mentioned a criterion unrelated to that release. Feature and quick-record numbering overlapped: 002 was already taken. And the otherwise sensible rule that CI must follow green forced a new CI run on an already deployed commit to repair record 002’s chronology.

The missing role: a PM inside the loop

The most interesting insight came from looking at where decisions originated. Feedback entered DFD as the feature’s epic, the source document cited by the specification. But every significant choice was mine:

  • Whether to overwrite an existing menu.
  • Removing manual calendar ID entry.
  • Asking users to choose a winter menu’s start date instead of suggesting one.

These are product decisions, taken by the developer.

DFD already has a place for a Product Manager, the person balancing users, priorities and product goals: design review. Before coding, someone approves the goal, criteria and unresolved choices; before release, someone approves the plan, thresholds and rollback. With a PM involved, open specification questions would go to that person rather than to me in chat. Approval would stop being something I declare to myself.

What I take away

The series argues that cost moves upstream and downstream. Here both sides met: the most useful part of specification was reading existing code to understand what the data meant. It found a bug tests would have cemented, before a new line was written.

The series gains another thread. Third article: CI that could not fail. Fourth: a gate stuck on old state. Fifth: an honest gate, but only on the test machine. A green gate means something only if it also says where and against which state it is green.

Declared limitations

  • Two features on one product, one developer, no replication.
  • The feedback author did not approve the gates; recorded decisions are mine.
  • The second feature’s rollout is still ongoing as I write; the 24- and 48-hour checks remain open.
  • Timings describe agent work sessions, rather than a comparison with the same work without DFD.
  • Some school calendar dates come from two agreeing aggregators rather than official documents. This is declared in the data file, but remains secondary sourcing.

Sources in the private repository

  • PR MartoEporedia/taita#697, merged: feature 001, .dfd/features/001-ux-primo-accesso/.
  • PR MartoEporedia/taita#706, merged: quick record 002, Telegram iframe title.
  • PR MartoEporedia/taita#710, merged: !.dfd/**/*.log in .gitignore, committed evidence logs, regenerated record 002.
  • PR MartoEporedia/taita#711, merged: feature 003, .dfd/features/003-mensa-a-passi/.
  • PR MartoEporedia/taita#712, merged: regional school calendars for 2026/27.
#AI#agents#spec#DFD#Claude Code#UX#accessibility