Doing requirements review on a complex booking flow, I hit an edge case the acceptance criteria hadn't addressed: what happens when a user retrieves a saved trip quote and amends it?

The naive answer and why it's wrong

The obvious implementation is to replace the original quote with the updated one. It's the simplest path: save creates a record, amend updates it. One record, current state.

But if the original quote was already shared with a traveller (sent as a link, or handed off to a booking agent), updating it in place changes what they see when they open the link. The flight options, the price, possibly the itinerary: all different from what was quoted. That's not an amendment, it's a mistake that looks like an amendment.

The right structure is parent/child lineage: the original persists unchanged, the amended quote references it. The sharing link always resolves to the specific version it was created for. Lineage lets you answer "what was this quote when it was shared?" without archaeology.

This is a data model decision. A small one, in the grand scheme, but once the schema is built around simple replacement, retrofitting lineage means a migration. Caught in requirements review before implementation starts, it's a question: "should amended quotes keep the original?" Caught during a sprint, it's a refactor.

The other places ambiguity lives

The same set of acceptance criteria had a few other unresolved questions.

What does "stale" mean for a trip quote? Time-based (saved more than N days ago), availability-based (one of the flights is no longer bookable), or price-based (the fare has changed)? Each definition implies different logic and a different user experience. The acceptance criteria used "stale" without specifying. A developer implementing staleness detection would have to pick one, and they'd probably pick the easiest one, which may not be the intended one.

What does "traveller-ready" mean? This turned out to be a status concept: a quote that's been validated for handoff to the traveller, with complete information and confirmed options. But the criteria used it more loosely, sometimes to mean "ready to book" and sometimes to mean "ready to share." These are different states with different requirements.

"Refresh" was similarly underspecified, used in some places to mean re-fetch live pricing and in others to mean rebuild the search from current parameters. The distinction matters when a traveller returns to a quote that was priced under a sale fare that's since expired.

None of these are traps. They're honest gaps in a set of requirements being written for a complex system. The job of requirements review is to find them.

As I write, both the lineage question and the definition of "stale" are still with Product, undecided. That's not a failure of the process: it's the process working. A question sitting with the people entitled to answer it is in a considerably better place than an answer a developer improvised at implementation time and nobody else ever saw.

The one that got through

All of those were caught. Here's one that wasn't, which is the more instructive case.

Everyone working on the assistant (including the developers, including me) carried an assumption that its flow was linear: search, then quote, then book. One path, three steps, in order. Nothing in the acceptance criteria said that. Nothing said otherwise either. It was simply the shape everybody pictured when they read them, and because everybody pictured the same shape, it never came up.

The real flow branches. A user can go from search straight to booking without a quote in between. They can retrieve a saved quote and book from there, skipping search entirely. The linear version wasn't a simplification of the truth, it was a different structure, and code written for a pipeline doesn't become a graph by adjusting a parameter. Correcting it meant a significant refactor.

That's the ambiguity that actually costs you. The ones I found in review were visible because the words were doing obvious double duty: "stale" and "refresh" announce themselves as underspecified the moment you look at them properly. This one was invisible precisely because there was no disagreement to detect. A gap everyone fills the same way doesn't read as a gap at all. It reads as shared understanding, right up until the point where reality supplies a third path nobody's implementation accounts for.

I don't have a clean technique for catching those. The closest thing I've got is that when a flow is described in a sequence, it's worth asking explicitly whether it's the only sequence, not because the answer is usually no, but because the question is one nobody thinks to ask about a thing they already believe they understand.

Criteria that can't be written as Given/When/Then

There's a broader outcome from this project that I didn't expect, and it's changed how I read acceptance criteria generally.

Product writes the criteria. That hasn't changed. What has changed is that we now treat them as guidance rather than as criteria in the strict sense, because you can't hold an LLM-mediated flow to literal Given/When/Then scenarios.

Gherkin assumes a deterministic mapping: a given precondition and a given action produce a specified outcome, and the test is whether that outcome appeared. Put a language model in the middle and that assumption breaks. The same Given and the same When can produce a range of outcomes that are all correct: differently worded, differently ordered, differently complete. The useful question stops being "did it produce this output" and becomes "is this within the range of right answers."

Criteria written as strict scenarios then fail in one of two directions. Either acceptable behaviour quietly falsifies them, and you spend your time reconciling a passing feature with a failing scenario. Or you specify so narrowly that the only way to satisfy the letter of the criteria is to constrain the model until you've designed it out of the feature you were building.

Treating criteria as guidance sounds like a loosening, and it would be if nothing replaced the rigour. What replaces it is being much more explicit about the things that are deterministic (the data contract, the boundaries, what must never happen) and accepting that the conversational surface needs judgement rather than assertion. The precision moves. It doesn't disappear.

Writing architecture up the stack

Alongside the acceptance criteria work, I wrote an architecture and current-direction document for director-level engineering review. Not a system diagram, but a narrative: what the AI-assisted trip quoting system is doing, why the structure is what it is, and where it's heading.

In the interests of not overselling it: nothing came back that changed the direction. No question from that audience reframed the problem for me. The value was almost entirely in the writing, which I've come to think is the more reliable benefit anyway. You can't explain a system's direction clearly without having a clear model of it, and the places where the narrative wouldn't hold together were places where my thinking hadn't, either. Writing at that altitude forces different questions than implementation does, not "how does session state get passed between services?" but "is the AI layer appropriately separated from the booking services, and does that hold as the feature grows?" Getting no pushback isn't proof the answers were right, but the exercise had already paid for itself before anyone read it.

The product/architecture intersection

One thing that came out of this review: an AI assistant that builds and refines trip quotes occupies similar space to a basket that lets users save and compare quotes. If both features are built in parallel without deciding how they relate, you end up with two surfaces that duplicate most of their behaviour, neither as good as one coherent thing.

Getting alignment on this before implementation is cheap. Getting it after both features are built is expensive: by then both have users, both have engineering investment, and neither team wants to deprecate their work. The direction of travel is that the basket gets reduced in scope as the AI assistant matures. That's a consequence worth knowing before the basket is built as a full-featured alternative.

The invisible contribution

None of this showed up in a commit log. No pull requests, no features shipped. The output was documents, surfaced questions, and a clearer set of requirements for the team implementing the work.

The contribution is real, though. The lineage question would have become a schema decision made without full context. The staleness definition would have been interpreted one way when the product team intended another. The position of the basket relative to the AI assistant would have been left implicit until it became a conflict.

And the flow assumption is what the other side of the ledger looks like. One shared, unexamined belief about the shape of a user journey, held by everyone and written down by no one, cost more than every ambiguity I caught in review put together.

Designing the API for an AI consumer is one part of building an AI feature well. Designing the requirements so the thing that gets built is the right thing is another. The second part is less glamorous, but it's where a lot of the risk lives.