---
title: Worked example
route: /resources/worked-example
lang: en
part_of: Onirion
version: "4.0"
date: 2026-09-22
author: Alexis Boyer
license: CC BY 4.0
one_line: "The practice case run end to end, with every artifact filled in and marked as simulated."
kind: example
layers:
  summary: /resources/worked-example
  complete_text: /complete.md
  markdown: /resources/worked-example.md
---

<!-- source: public-kit/example/completed-case.md -->

# Completed case — the exercise run, gate by gate

**A method by Alexis Boyer · version 4.0 · Day 1**

**This is a simulated run.** Nothing here is a client project, a real product or a real organisation. The case is the exercise case in [Run your first case](/resources/first-case#worksheet), carried out against the [supplied local application](/resources/sandbox) that ships with this kit. There is no sector, no company and no person in it: the two identities are the exercise application's own switcher links, and the people who tested are referred to by what they did. The artifacts below are what a run of the method (Onirion) produces at each gate — an index, discovery notes, a PRD, a design prompt, a port, a backend connection, a test round, a release note, and the two files that record why the product moved the way it moved. Read them for their shape. Do not read the dates, the messages or the test output as a record of real work.

The case, in one line: someone wrote an update, selected **Save draft**, left the page, and the text was gone on return. The outcome the run owns is that **the author gets their confirmed text back, and no one else can read it** — including through the route that does not go through the page.

The run below took eight working days, from the folder of sources to the release note. The long part of a real build — connecting a real backend until the flows hold in depth — is a day here, because the whole product is one page and one stored record. Do not read the timing as a claim about anything larger.

## Gate 0 — set up the project folder

### What went into the folder

One folder, made for this case. Everything the run would draw on went into it: the exercise worksheet, the supplied application, the report that opened the case, an exported chat thread where the loss was first mentioned, an older note on what "saved" was supposed to mean, a screen recording, a spreadsheet of requests collected earlier, and a capture of the confirmation message as it stands today.

The coding agent's session was opened on that folder. At that point there was nothing to build and nothing to run in it — a folder of sources is enough to start.

The memory and the journal were created first, before any prompt ran: `PROJECT_MEMORY.md` and `PROJECT_JOURNAL.md`, both empty. The project channel was created the same day, with the two people who would test later in it from the start.

### `converted/INDEX.md`, as the first prompt returned it

| Source | What it is | Date |
| --- | --- | --- |
| `report.txt` | The report that opens the case: the author selected Save draft, left the page, and the text was gone on return. | Day 2 |
| `thread-export.md` | An exported chat thread where the loss was first mentioned, and two replies saying it had happened to them as well. | Day 1 to Day 2 |
| `saved-means-what.md` | An older note on what the word "saved" should mean in the interface. | undated — no date in the file, none in its name |
| `requests.csv` | Converted from a spreadsheet of requests collected earlier. Two columns carry dates; the rest do not. | a three-month window |
| `first-case.md` | The exercise worksheet this run follows. | Day 13 |
| `sandbox/README.md` | The supplied application's own notes: how to run it, its two switcher identities, the record it starts with, and the three deliberate defects it carries. | undated |
| `sandbox/app.py` | The supplied application. One module, standard library only, bound to loopback. | undated |
| `sandbox/test_acceptance.py` | Three acceptance tests written against the intended behaviour, not against what the application currently does. | undated |
| `capture-confirmation.png` | A capture of the confirmation message as the application renders it today. Listed, not converted. | file date Day 3 |

### What could not be read

- `notes-an earlier year.pdf` — password protected. Not opened, not converted.
- `walkthrough.mov` — a screen recording. Listed from its file name only; nothing was inferred from it.
- `capture-confirmation.png` — an image. Listed and kept as a source, but no text was extracted from it and nothing was concluded from it.
- `requests.csv` — converted, but eleven rows carry no date, so nothing in them can be placed in time.

The originals were never touched. The conversion wrote into `converted/` and nothing was moved, renamed or overwritten. The pass sent nothing anywhere.

### The obvious holes, filled the same day

The password for the PDF was asked for and supplied; the prompt was re-run on that one file and it converted. The recording was watched by the person running the exercise, who wrote a one-paragraph note beside it saying what it shows — a walkthrough of the save flow, from before the failure switch existed, which decides nothing. The undated rows in the spreadsheet stayed undated and were marked as such in the index rather than guessed at.

The session was told to state where it is before going further, and it did, at every gate after this one.

**In hand at the end of Gate 0:** everything in one folder, converted, inventoried.

## Gate 1 — gather the discovery

### `DISCOVERY.md`, as it was written

**Sources read**

- The report and the chat thread: three separate accounts of text disappearing after a save that appeared to succeed.
- The older note on what "saved" means: it says the word must follow the write, never precede it.
- The supplied application's notes: they name three deliberate defects — a save that confirms without writing, a direct read route that answers anyone, and a write failure that is swallowed and reported as success.
- The application itself, read as a source rather than as code to change.
- The three supplied acceptance tests, read as the statement of intended behaviour.
- One acceptance run of those tests, to see what the starting point actually is rather than what the notes say it is.

The acceptance run, as it came back:

```text
test_author_recovers_only_confirmed_saved_text_after_return ... FAIL
test_failed_save_keeps_edit_visible_and_does_not_replace_confirmed_draft ... FAIL
test_second_identity_cannot_read_author_draft_in_ui_or_direct_endpoint ... FAIL

AssertionError: 'The imaginary library opens next Tuesday.' != 'The imaginary library now opens on Friday.'
 : A confirmed save must survive reload.

AssertionError: 200 != 503 : The deterministic failure must not claim success.

AssertionError: 200 not found in (403, 404) : A second user must be denied at the endpoint.
```

Three failures, which is the documented starting point. Three *errors* instead would have meant the environment stopped the tests from starting their local server on loopback.

**What is established, and by which source**

- The confirmation message is not evidence of a write. The save response renders the confirmation with an empty field while the stored record is unchanged. *(Observed in the run above; consistent with the application's own notes.)*
- The page hides the author's text from the second identity, but only because the read it performs selects by owner and returns nothing for an identity that holds no record. Nothing in the page is enforcing a rule.
- The direct read route returns the record's content to any identity that has been chosen. *(Observed: the route looks the record up by its id and answers with the content; there is no ownership check on that branch.)*
- A write failure is raised inside the storage layer and caught by the handler, which then answers as if the write had succeeded and drops the submitted text from the page.

**What contradicts what**

1. **One record for everyone, or one record per author.** The worksheet's rules describe the author retrieving *their* latest confirmed text, and the read path agrees: it selects by owner. The write path does not: it returns the same constant record id whatever identity performed the save. One source describes a record per author; the other implements a single shared record. Both cannot hold, and which one is intended decides whether a save by one identity can overwrite another's text. *Not resolved here. Sent to the alignment questions at Gate 2.*
2. **What the confirmation is for.** The older note says the word "saved" must follow the write. The running interface shows it before any write has been attempted, and the capture in the folder shows the same message. The note is older than the interface, so the disagreement is real rather than a misreading. *Resolved by the order written into the PRD: the tests and the notes decide, the current behaviour decides nothing.*

**What is absent**

1. **What a denied direct read should answer.** Nothing in the material says. The supplied test accepts either of two answers — one that refuses and thereby confirms the record exists, and one that behaves as though it does not exist. Those are two different product decisions about whether the existence of someone's record is itself private, and the material does not settle it.
2. **Whether the second identity may hold a record of their own.** The read path would return nothing for them; the write path would send their text to the author's record id. No source says whether the editor should be there for them at all.
3. **What happens to unsaved text when the tab is closed rather than reloaded.** The rules cover reload only.

**Hypothesis on the product boundary**

One page, one stored record per author, two identities, and one failure that can be provoked on purpose. Deletion, expiry, simultaneous editing, offline saving and real authentication are outside it.

**Privacy risks**

The stored content is text a person wrote. The switcher is not authentication — it is a local cookie with two values — so nothing here demonstrates a permission model; it demonstrates a read model. The application binds to loopback and is never to be deployed or given real data.

**Repository, starter and template constraints**

No approved starter and no written conventions exist for this exercise. The supplied application's own constraints stand in: standard library only, no new dependency, loopback only, and the supplied tests are not to be weakened to make the run look finished.

### The control session, at Gate 1

The main session concluded: *"the second identity does not see the author's text, so the privacy rule already holds."*

The control session was given the same material and challenged it.

**Finding.** The conclusion is not supported by what the main session read. The page shows nothing to the second identity because the read it performs selects by owner and that identity owns no record — not because any rule is applied. The route that does not go through the page looks the record up by its id and answers with its content to whichever identity has been chosen. **Evidence:** the read branch for that route in the application module performs no comparison between the record's owner and the current identity; the acceptance run above fails on exactly that assertion, with the route answering 200 where the test requires a denial. **What I could not check:** whether a real product would place the rule in the query or in the handler — that is a decision, not a defect, and it is yours.

**The decision that followed.** Privacy for this product is a read-model rule, not an interface behaviour: what an identity must not see never reaches their browser, and the check lives in the query rather than in whatever renders the page. Written into the PRD as section 6, and the acceptance check that matters is the one that exercises the route, not the one that looks at the page. Recorded in the memory the same day.

**In hand at the end of Gate 1:** what is known, what is missing, what contradicts.

## Gate 2 — write the PRD

### The alignment questions, and what came back

The PRD prompt was run on the converted folder, its index, `DISCOVERY.md`, and the memory and journal. It stated what was established, what contradicted what and what was absent, then asked two alignment questions before writing anything.

**Question 1 — what a denied direct read answers.**

*Source A, the supplied test, accepts either a refusal or a not-found answer. Source B, the discovery notes, records that the record's id is printed in the application's own notes and is therefore guessable. The product risk: a refusal tells anyone who asks that a record with that id exists and belongs to someone else, which is a fact about a person's content. Not-found hides that, at the cost of being less honest to a legitimate client that has simply lost its own record. I would default to the refusal because it is the more conventional answer. Confirm or correct.*

Answered: **not-found**. The existence of someone's record is part of what is private here, and the id is guessable, so the more conventional answer leaks the one thing worth protecting. The default the prompt proposed was corrected, which is why the question was asked rather than decided silently.

**Question 2 — whether the second identity may save.**

*Source A, the read path, is written per owner. Source B, the write path, targets a single constant record. The product risk: if the second identity may save and the write stays as it is, their text replaces the author's confirmed text, and the interface will report that as a success to both of them. Default: both identities may save, and every write is keyed by its owner. Confirm or correct.*

Answered: **confirmed**. Both identities hold their own record; a save under one identity must never touch another's.

### The PRD

Fourteen sections, in the order the template sets, and no others. It lives beside the memory and the journal and moves into `docs/` with them at Gate 4.

#### 1. Strategic framing

- **What this product is going to be:** one page where a signed-in author writes an update, saves it explicitly, and gets exactly what was confirmed back when they return.
- **What is wanted from it:** that the word on the screen matches what happened in storage — every time, including when storage refuses.
- **What is explicitly not wanted:** saving on the author's behalf, saving in the background, guessing what they meant, or any message that lets someone believe their text is safe when it is not.
- **The business loop it serves:** people write updates for other people to read. A tool that loses them once stops being used, and the loss is silent, so the trust is spent before anyone reports it.
- **The constraints that shape it:** one module, the standard library only, loopback only, no new dependency, and three supplied acceptance tests that state the intended behaviour and are not to be weakened.
- **What the sections below detail:** the boundary, the two identities and what each may see, the three flows, the states the stored record moves through, the privacy rule and where it is enforced, and what has to be true before any of it counts as done.

#### 2. Product boundary

- **Who the product is for:** a signed-in author writing an update for later.
- **Who it is not for:** anyone reading someone else's updates, and anyone administering the store.
- **Workflows that belong elsewhere:** authentication, account management, and anything that moves an update on to a reader.
- **What the product must never expose:** another identity's text, and whether another identity holds a record at all.
- **Shared objects assumed:** one stored record, carrying an id, an owner and its content.
- **Non-goals:** deletion, expiry, version history, simultaneous editing, offline saving, saving without an explicit action.
- **How an out-of-scope workflow is represented instead:** a read that is not the owner's ends as a not-found answer carrying no content — not as an error the reader can act on, and not as a request path.

#### 3. Roles and permissions

| Role | What they do | What they can see | What they must not see | What they can do |
| --- | --- | --- | --- | --- |
| Author identity | Writes an update and saves it explicitly. | Their own record, its confirmed text, and when it was confirmed. | Any other identity's text; whether any other identity holds a record. | Write, save, retry a failed save. |
| Second identity | The same, on their own record. Starts with no record. | Their own record; the empty state until they save. | The author's text, through the page or through the direct route. | Write, save, retry a failed save — on their own record only. |

- **Where role differences change navigation, actions, disabled states or empty states:** nowhere in the navigation, and nowhere in the actions. The two roles differ by exactly one thing — what their record read returns — and that difference is only visible as the empty state one of them starts in. They are written out separately anyway, because "the same page with different data" is precisely the case where a rule gets left in the interface instead of the query.
- **How a role is granted, changed and removed:** in this exercise, by choosing a switcher link, which is not authentication and demonstrates nothing about permissions. A real product names its approved authentication here, with its owner. Recorded as a gap of the exercise, not as a design.

#### 4. Core workflows

- **Save and confirm.**
- Who runs it: either identity.
- Entry point: the page, with text in the field.
- Steps: the author edits; selects Save draft; the write is attempted; the write is confirmed; the page reports it.
- Branches: the write is refused — see the third workflow.
- Failures and how they end: never silently; a refused write ends in the failed state and never in the confirmed one.
- Confirmations: the confirmation appears only after the store has confirmed, and names when.
- Final state: the record holds the submitted text.
- **Return and recover.**
- Who runs it: the owner of a record.
- Entry point: reopening the page, after a reload or after switching away and back.
- Steps: the record is read by owner; its confirmed content fills the field.
- Branches: no record yet — the empty state.
- Failures: a read that fails leaves the field empty and says so; it never shows stale text as though it were confirmed.
- Final state: the field holds the last confirmed text, and nothing else.
- **Failed save and retry.**
- Who runs it: either identity, when the write is refused.
- Entry point: a save whose write does not complete.
- Steps: the answer reports the refusal; the submitted text stays in the field; an error is shown; Retry is offered.
- Branches: retry succeeds and the flow ends as a confirmed save; the page is reloaded before a successful retry, and the last confirmed text comes back — the unsaved text is gone, which is the honest outcome and must not be disguised.
- Confirmations: none until a write is confirmed.
- Final state: either confirmed, or unchanged with the refusal still visible.
- **A read that is not the owner's.** Entry point: the direct route, with a record id. It ends as a not-found answer carrying no content. It has no success path.

#### 5. Product states and data visibility

- **Object: the stored record.**
- Its states, in order: absent → confirmed. A confirmed record stays confirmed; a later confirmed save replaces its content.
- What moves it: a write the store has confirmed, and nothing else. Not a submission, not a rendered message.
- Terminal states, and what reopens them: none. Deleting the generated store file beside the application resets the exercise.
- What each role sees in each state: its owner sees absent as the empty state and confirmed as their text. Every other identity sees absent in both, because the record they do not own is not read for them at all.
- What is hidden, and from whom: the content and the existence of a record, from everyone but its owner.
- **Saving, failed and editing are states of the page, not of the record.** Writing this down is what stops the interface from inventing a fourth record state and reporting it as storage.

#### 6. Privacy and ethics rules

- **Sensitive data held here:** text a person wrote and has not published.
- **How it must be handled:** read by owner, written by owner, and never loaded into an answer before the owner has been compared.
- **What must never be shown, and to whom:** the content, and the existence of a record, to any identity that does not own it.
- **Rules for search, filters and exports:** there are none of these in scope, and none may be added without returning to this section.
- **What may reach the browser at all:** only the requesting identity's own record. The rule lives in the query, not in the component that renders.
- **Controlled exceptions:** none.
- **The intake path for a request to see someone else's content:** none exists, and none is to be improvised.
- **Retention and deletion:** the record lives in the local store file beside the application. It holds nothing about a person beyond the text they typed, and no real content is to be put into it.

#### 7. Which source wins when sources disagree

1. An explicit decision by the person who set the exercise, recorded in the memory with its date.
2. The three supplied acceptance tests — they state the intended behaviour, and they are not weakened to make a run look finished.
3. The supplied application's own notes, which name the deliberate defects.
4. The strategic framing section of this PRD.
5. The rest of this PRD.
6. The worksheet and the older notes in the folder.

The current behaviour of the running application decides nothing: it is known to be defective, and it appears in the discovery as evidence, never as an instruction.

- **What must be checked before an older note is used:** whether the supplied notes contradict it, and whether it predates a decision recorded in the memory.
- **Who records a change to this order, and where:** the Product Builder, in the memory, dated.

#### 8. Design baseline and acceptable drift

Assumed now:

- **The reference this product follows:** there is no design system for this exercise. The reference is the supplied application's own markup and the platform's default form controls, named as a constraint rather than as inspiration.
- **Its version or location:** the supplied application as it stands in the kit.
- **Non-negotiable in it:** the confirmation region and the error region stay separate and stay announced; the submitted text stays in the field when a write is refused; status is never carried by colour alone.
- **Acceptable drift:** spacing, density, the order of the two actions, and the wording of every message.
- **Where the prototype and the front end draw on the same thing:** both follow the reference above, so that what the design tool produced and what the code produces remain the same product.

To request from Design Ops — none of which exists for this exercise, so each line records what stood in instead:

- The components and their states — stood in by the supplied markup.
- The tokens and the rules for using them — none; the defaults were used and nothing was invented.
- What may be extended and what may not — agreed ad hoc with the person who set the exercise.
- Who maintains the system — nobody; recorded as a gap.
- An agent-readable form of the above — the constraints paragraph in the supplied notes.
- The named owner of each request, and the date an answer is needed — the exercise has no owner to name, and that is written in the journal, dated, rather than papered over.

#### 9. Functional depth

- **Flows that must work end to end:** all three. There is no screen here deep enough to be worth faking.
- **Flows that may stay placeholders:** none.
- **What each clickable surface does:** Save draft attempts a write; Simulate failed save attempts a write that the store will refuse, which is a deliberate, documented facility of the exercise; Retry resubmits the text in the field; the switcher links change identity; switching away clears the identity.
- **Simulated states deliberately allowed:** exactly one — the refused write, through the supplied switch.
- **States required everywhere:** empty, saving, confirmed, refused, permission-denied, and the answer that carries no content.

#### 10. Backlog taxonomy

| Category | What belongs in it here | Where it is tracked | Who owns it |
| --- | --- | --- | --- |
| Product decisions | What a denied read answers; whether the second identity may save; what happens to unsaved text on tab close. | The memory, dated. | Product Builder, decided with the person who set the exercise. |
| Implementation tasks | The write keyed by owner; the refusal answer; the separated regions; the Retry action. | The journal. | Product Builder. |
| Design parity issues | The message placement that had to change from what the design showed. | The journal, as deliberate drift. | Product Builder. |
| Privacy and legal guardrails | The read-model rule, and the answer that carries no content. | This PRD, section 6. | Product Builder. |
| QA findings | What came back from the test round. | The journal, and the test file where a check was added. | Product Builder. |
| Backend integration work | The store contract and the refusal path. | The journal. | Product Builder. |
| Future nice-to-haves | Version history; recovering unsaved text after a tab close. | The memory's open questions. | Not owned; not started. |

#### 11. Backend and API assumptions

Assumed now:

- **Objects and contracts assumed:** one record with an id, an owner and content; a read by owner; a read by id; a write that either confirms or raises.
- **Authentication and permissions assumed:** the identity arrives as a local cookie holding one of two values, and it is not authentication. Every rule in this PRD is a read-model rule that does not depend on that being real.
- **The business meaning of key values:** *confirmed* means the store completed the write and said so. Nothing else means confirmed.
- **Routes these flows use:** the page; the save submission; the direct read of one record by id; the two identity links; the link that clears the identity.
- **The shape the mock services follow until the real store is connected:** the store's own three operations — read one record for an owner, read one record by id, write content for an owner — so that swapping them changes the wiring and not the flows.

To request from Backend and API Ops — which does not exist for this exercise:

- A catalog with real examples, request and response shapes, errors and versioning, reference patterns for authentication and migrations, a test environment with safe data, read-only access to logs, and a written route for an API that does not exist yet. None of these exists here. The supplied module is the contract, the exercise's own notes are the documentation, and that gap is recorded in the journal, dated, rather than filled by invention.

To request from Frontend Ops — which does not exist for this exercise either:

- An approved starter, its version and its owner; the API client pattern; the checks required on a deployable branch. None exists. The supplied application's constraints stood in, with the agreement of the person who set the exercise, recorded in the journal.

#### 12. Acceptance criteria

- **Save and confirm.** Works against the mock services and then against the real store. The confirmation appears only after the store confirms. The confirmed text survives a reload and an identity switch away and back. Checks run before finishing: the supplied acceptance run, plus walking the flow in a browser.
- **Return and recover.** The field holds the last confirmed text and nothing else. The empty state is reached by the identity that owns no record. No privacy regression: the read carries the owner.
- **Failed save and retry.** The answer does not report success. The submitted text is still in the field. The error is announced in its own region and the confirmation region is empty. Retry is offered and works. A reload before a successful retry brings back the last confirmed text.
- **A read that is not the owner's.** Answers not-found, and the answer carries no part of the content. Exercised through the route, not through the page.
- **Across all of them.** Tests added or updated; memory and journal updated where product behaviour, architecture, a decision or future reasoning changed.

#### 13. The QA contract

- **Personas to test:** the owner of the seeded record, and the identity that owns nothing.
- **Role-based privacy checks:** the direct route requested as the non-owner; the page requested as the non-owner; a save performed as the non-owner followed by a read as the owner.
- **Requirements derived from what testers said:** the failure message must say where the text is, not only that something went wrong.
- **Regression flows:** all three workflows, plus the non-owner read, after every change to the store or the handler.
- **Validation commands:** the supplied acceptance run, from the application's directory. On the Codex path, the default workspace sandbox blocks the tests' own local server and they end in permission errors before reaching any real result; allow network access for the session, or run the command outside the sandbox. Checked with Python 3.9.6 and Codex CLI 0.155.0-alpha.9.2.
- **Expected report format:** what was run, what it returned, what was walked in a browser, and what was not checked.
- **Severities, and when they start:** P0 blocking, P1 important, P2 minor — and they start only once there are more defects than the session can hold. In this run that never happened, so none were used.

#### 14. Open decisions

| Decision | Alignment question behind it | Default proposed | Who decides | By when |
| --- | --- | --- | --- | --- |
| What a denied direct read answers | The test accepts a refusal or a not-found answer; a refusal confirms that someone's record exists, and the id is guessable. | Refusal | The person who set the exercise | Before the store is connected |
| Whether the second identity may save | The read path is per owner, the write path targets one constant record; if both are true, one identity's text replaces another's and both are told it worked. | Both may save, every write keyed by owner | The person who set the exercise | Before the store is connected |
| Unsaved text when the tab is closed | The rules cover reload only; nothing says whether closing the tab should behave differently. | Out of scope; the text is lost and nothing implies otherwise | The person who set the exercise | Not needed for this run |

Both of the first two were answered at this gate. The third stayed open and is recorded as open in the memory rather than decided in passing.

**In hand at the end of Gate 2:** what is in and what is out, agreed with the person who set the exercise.

## Gate 3 — prototype the end-to-end flows

The coding session took the PM role, wrote the design prompt from the PRD and sent it to the design tool. The prompt was not hand-written and screens were not pasted back and forth by hand.

### The design prompt that was sent

```text
Design a full high-fidelity prototype for the update editor, a single-page
workspace where a signed-in author writes an update, saves it explicitly, and
recovers exactly what was confirmed.

This is a real logged-in working surface, not a landing page. Start directly
inside the product experience.

Treat the attached PRD — whose first section is the strategic framing —
together with the discovery notes and the supplied application's own notes, as
what decides. Where a source is missing, do not invent a product decision.

Design end-to-end flows, not only the major screens. Every flow below must be
walkable from its entry point to its final state, including the intermediate
steps, the branches, the failures and the confirmations.

## Strategic context
People write updates for other people to read. A tool that loses one, silently,
is abandoned before anyone reports the loss. This product exists so that the
word on the screen matches what happened in storage, including when storage
refuses.

This product helps an author answer:
1. Is what I wrote actually kept?
2. If it was not, is my text still here?
3. Can anyone else read it?

The product should feel like a working surface, not a document editor with
chrome. Calm, plain, quick to re-enter.

## Product boundary
This product is for a signed-in author writing an update for later.
This product is not for a reader, an administrator, or anyone reading someone
else's content.

Do not design: deletion, expiry, version history, simultaneous editing,
offline saving, sharing, account management, or saving without an explicit
action by the author.

A read that is not the owner's has no success state to design. It ends as an
answer carrying nothing.

## Users and roles
- Author identity: writes and saves; sees their own confirmed text and when it
  was confirmed.
- Second identity: the same actions on their own record; starts owning nothing;
  must never see the other's text or learn that it exists.

The two roles use the same single surface. The only difference is what their
record read returns. Show the empty state that difference produces.

## Core product principles
1. The confirmation follows the write. Never precedes it.
2. A refused write never removes the author's text from the page.
3. Status is announced, and never carried by colour alone.
4. Every clickable surface implies a real behaviour.
5. Nothing on this surface suggests a capability the product does not have.

## Navigation and shell
One surface. An identity line at the top with a way to switch away, the editor,
the actions, and one region for confirmation and one for error. No brand row,
no navigation the product does not have.

## Visual direction
There is no design system for this product. Follow the supplied application's
own markup and the platform's default form controls. Treat that as a
constraint, not as inspiration. Plain type, near-black text, one readable
column, compact actions. Avoid heroes, decorative gradients, oversized cards
and any analytics surface.

## Core data concepts
- The update record: its owner, its confirmed content, and when it was
  confirmed.
- The page states: empty, editing, saving, confirmed, refused.

## Required screens and flows
1. Entry: choosing an identity, and the surface that follows.
2. Empty state: the identity owns no record yet.
3. Editing: text in the field, nothing saved yet, and it must be obvious that
   nothing is saved yet.
4. Saving: the moment between the action and the answer.
5. Confirmed: the message, what it names, and where it sits.
6. Refused write: the text still in the field, the error in its own region, the
   Retry action, and the absence of any confirmation.
7. Retry, succeeding: what changes on the surface.
8. Reload after a refused write: the last confirmed text returns and the
   unsaved text does not. Design how that is said, honestly.
9. Return: leaving and coming back to the last confirmed text.

## Interaction states to include
Empty; saving; confirmed; refused with the text preserved; retry; announced
status; announced error; disabled action with a stated reason; narrow screen.

## Privacy and visibility rules
Respect: an identity sees only its own record; nothing indicates whether
another record exists. Do not expose another identity's content anywhere,
including in a message, a count or a placeholder.

## Content and mock data
Language: English. Use realistic update text. Avoid lorem ipsum. Write real
product copy, especially for the refused-write message, which is the hardest
sentence in this product.

## Output expectations
A complete surface with all of its states, not one screen. Someone reading the
result should be able to walk each flow from end to end and should never be in
doubt about whether their text is kept.
```

### What came back, and what the showings changed

The first output was a clean single surface with the states listed, and one thing wrong with it: the refused-write message said *Save failed.* It was shown to two people who write updates.

- Both read *Save failed* as *your text is gone*. One of them selected the text and copied it before doing anything else — which is exactly the behaviour the product exists to make unnecessary. The message was rewritten: **"Not saved. Your text is still here. Try again."** The message names where the text is, because that is the only question the person is actually asking.
- The confirmation said only *Saved*, and neither of them could tell whether it referred to what was in the field now or to something earlier. The confirmation gained the time it was confirmed.
- One of them asked what happens if the tab is closed with unsaved text. The honest answer is that it is lost. That was not designed around; it was recorded as an open decision and left open.

They were shown the flow they do every day — write something, save it, come back — not a tour of the surface. The second showing ended with both saying it was the tool they needed, which is what closed the gate.

**In hand at the end of Gate 3:** a prototype the people concerned recognise as their tool.

## Gate 4 — port the prototype into code

**There was no starter to clone.** No approved starter repository and no written conventions exist for this exercise, so none were invented. The supplied application is the repository. The gap was written into the journal, dated, with what stood in for it and with whose agreement — and that record is the request that gets made next time, not a decision that the gap was acceptable in general.

The three working documents moved together into `docs/` inside that repository — the PRD, `PROJECT_MEMORY.md` and `PROJECT_JOURNAL.md` — and the folder of sources stayed where it was.

### What the port produced

The page layer was rewritten so that every state the design accepted exists in the code, fed by a service shaped like the store's contract but returning canned answers. Nothing touched storage at this gate.

| State | What it renders | Reached by |
| --- | --- | --- |
| Empty | Field empty, no message in either region. | An identity that owns no record. |
| Editing | The field holds text; the confirmation region is empty. | Typing after any state. |
| Confirmed | Confirmation region names the time; the field holds the confirmed text. | A write the service says it completed. |
| Refused | Error region filled; field holds the submitted text; confirmation region empty; Retry offered and Save hidden. | A write the service says it refused. |
| Not the owner's | Nothing. No field, no message, no content in the answer. | The direct route with an id the identity does not own. |

What else the port did, and what it deliberately did not do:

- The confirmation region and the error region became two separate regions, each announced in its own way, so that a refusal can never be written into the region that means success.
- Every message moved to one place at the top of the module instead of being written into the markup, so the copy the showings produced has a single home.
- The handler chooses a state and passes values; the rendering holds no rules. Routes stayed thin.
- The mock service exposed the store's three operations by name, so Gate 5 would be a swap and not a rewrite.
- Nothing was redesigned. Two things came out different from the design, on purpose, and both were recorded: the message sits in the page's own region rather than in the floating element the design showed, because the surface re-renders on submission and a floating element is not announced when it does; and the actions row wraps below a narrow width, which the design had not covered.

The back-and-forths were the work of this gate: the running surface beside the design, naming what was off — the error sitting below the field where nobody looked, the two actions indistinguishable at a glance, the button reading *Save again* where the design said *Retry*, the density of the actions row. Fix, rerun, look again. No screen was rebuilt on paper first, and no map was written from the design to the code; the port was close enough that neither would have paid for itself.

The three acceptance tests still failed at the end of this gate, on the same three assertions as at Gate 1. That was expected: nothing was connected yet, and a green build was never the evidence being sought.

**In hand at the end of Gate 4:** a surface that runs, that you can click.

## Gate 5 — connect the backend

The mock service was replaced with the store, one flow at a time, in the same session. No second building session was opened.

### What the connection changed

- **The write stopped being a no-op.** The store now writes the submitted content and reports what it wrote; the handler's confirmation is issued from that report and not from having reached the end of the function.
- **The write is keyed by its owner, not by a constant record id.** This is the change the control session forced — see below.
- **A refused write answers as a refusal.** The exception raised inside the store is no longer swallowed: the answer carries the refusal, the submitted text, the error in its own region, and a Retry action, and it carries no confirmation. Reloading before a successful retry returns the last confirmed content, which is the honest outcome.
- **The direct read compares the record's owner with the current identity before any content is loaded into the answer.** A read that is not the owner's ends as a not-found answer whose body carries no part of the content. The comparison lives in the query, so the content of a record someone does not own never reaches the process that would render it, let alone their browser.
- **The page read stayed keyed by owner,** so the second identity meets the empty state rather than someone else's text.
- Deep on each flow before moving to the next: the empty state, the refusal, a second refusal in a row, a retry that succeeds, a reload between the two, and a read attempted as the identity that owns nothing.

### The control session, at Gate 5

Called in on a doubt rather than on a bug: the tests were passing and the flows looked right.

**Finding.** The write still targets a single constant record id. The read is keyed by owner and the write is not, so a save performed under the second identity replaces the author's confirmed content, and both identities are told it worked.
>
**Evidence.** The store's write operation returns the same constant record id whatever owner it is given — the same contradiction recorded in `DISCOVERY.md`, still present in the connected code. PRD section 3 says a save under one identity must never touch another's record, and PRD section 6 makes that a read-model rule. **Reproduction:** choose the second identity, type any text, select Save draft; switch to the author identity and reload the page. The author's confirmed text is the second identity's text.
>
**Why the tests did not catch it.** None of the three supplied acceptance tests performs a save under the second identity. They check that the second identity cannot *read*; nothing checks that the second identity cannot *overwrite*.
>
**What I could not check:** whether the same hole exists on any other path — there is only one write path, so this is the whole of it.
>
*I read, checked and challenged. I changed nothing and I decided nothing.*

**The decision that followed.** The write is keyed by its owner, and the record id stops being a constant in the write path. A regression test was added beside the three supplied ones — save as the second identity, then read as the author, and assert the author's confirmed text is unchanged — because a defect that no check catches will come back. The three supplied tests were not touched. Recorded in the memory, dated, and in the journal with what was verified.

The finding went from the control session to the main session directly. How the two sessions reach each other is set up in the tool; nothing in this kit documents it for a particular assistant, and if they cannot reach each other the finding is relayed by hand, unchanged, with its origin stated.

**In hand at the end of Gate 5:** the flows hold in depth.

## Gate 6 — test, then ship

### The test round, with demo accounts

Authentication is not connected in this exercise and is outside it, so demo accounts are what exists: the two switcher links. They were sent to the two people who had walked the design at Gate 3, with the record the application starts with and the failure button explained, and with the reminder that the switcher is not authentication and that no real content goes in.

What came back, and what was done with it:

- *"I pressed the failure button twice and after the second one I couldn't tell whether the first attempt had gone through."* The error region now names when the last confirmed save happened, so a refusal always says what is still safe.
- *"Retry sits next to Save draft and I can't tell them apart."* Retry only appears in the refused state, and Save is hidden while it shows. One action at a time.
- *"On my phone the two buttons were on one line and I pressed the wrong one."* The actions row wraps below a narrow width — the drift already recorded at Gate 4, now the thing a tester actually hit.
- *"When it says Saved, saved where?"* Left as it is. The confirmation names the time; naming a place would be a claim about storage this product does not make.

Each one was fixed as it came back. Severities were never used in this run, because there was never more coming back than the session could hold — the moment that changes is the moment P0, P1 and P2 start earning their keep, and it did not arrive.

The acceptance run after the fixes, from the application's directory:

```text
python3 -m unittest -v test_acceptance.py

test_author_recovers_only_confirmed_saved_text_after_return ... ok
test_failed_save_keeps_edit_visible_and_does_not_replace_confirmed_draft ... ok
test_second_identity_cannot_read_author_draft_in_ui_or_direct_endpoint ... ok
test_second_identity_save_does_not_touch_author_record ... ok

Ran 4 tests
OK
```

Walked in a browser as well, as both identities: the empty state, a save, a reload, a switch away and back, two refusals in a row, a retry, a reload between a refusal and its retry, and the direct route requested as the identity that owns nothing.

### The release note

Prepared in the main session and sent by hand. The prompt that produced it sends nothing anywhere.

```text
Release note — the update editor
Date: Day 12
To: the leads of the trades concerned

What is in this release, flow by flow
- Save and confirm: the confirmation follows the write and names when it
  happened. A confirmed save survives a reload and an identity switch.
- Return and recover: the field holds the last confirmed content and nothing
  else. An identity that owns no record meets the empty state.
- Refused write: the answer reports the refusal, keeps the submitted text in
  the field, shows the error in its own announced region, offers Retry, and
  shows no confirmation. A reload before a successful retry returns the last
  confirmed content.
- A read that is not the owner's: answers not-found, with no part of the
  content in the answer. The ownership comparison is in the query.

What has been tested, by whom, with which accounts
- Two people who had not seen the build, using the two demo identities the
  exercise supplies. These are switcher links, not authentication.
- The Product Builder, walking all four flows in a browser as both identities.

What was verified and how
- The acceptance run: the three supplied checks and one added regression
  check, all passing, from the application's directory.
- Walked end to end in a browser: empty state, save, reload, switch away and
  back, two refusals in a row, retry, reload between a refusal and its retry,
  and the direct route requested as the identity that owns nothing.
- The control session's finding at Gate 5 — a save under one identity
  overwriting another's record — reproduced, fixed, and covered by the added
  regression check.

What is known to be unfinished
- Unsaved content is lost when the tab is closed. This is an open product
  decision, recorded in the memory, not a defect and not designed around.
- The switcher is not authentication and demonstrates nothing about
  permissions. Every rule in this release is a read-model rule that does not
  depend on it.

What the leads are asked to give the go-ahead on
- The read-model rule and the not-found answer.
- The refusal path, which is where this product either keeps its word or
  does not.

Which review did not take place, and who was not there to give it
- No frontend, backend or data lead read this change: this exercise has no
  trades to convene, and there is no staging and no production to ship to.
  The application is loopback-only and is not deployed. The go-ahead was
  given by the person who set the exercise, on their own.
- That gap is the request that gets made next time, and it is written in the
  journal, dated, rather than left to be reconstructed later.
```

Nothing was deployed, because the exercise has nowhere to deploy to and the supplied application is never to leave the machine. What this gate produced is the note, the go-ahead that was actually available, and the written record of the review that did not happen. The delivery path itself — local, then the repository, then staging, then production, with the checks along the way and a way to see which revision is live — is on the list of what was asked for and not supplied.

The one case where things are deliberately frozen did not apply here: nothing was going to production.

**In hand at the end of Gate 6:** demo accounts in real hands, and a note that names what was verified and what was not reviewed.

## The memory and the journal at the end of the run

### `docs/PROJECT_MEMORY.md`

**Current product position.** One surface where an author writes an update and saves it explicitly. The confirmation follows the write and names when it happened. A refused write keeps the submitted text in the field and never claims success. An identity reads only its own record, and a read that is not the owner's answers not-found with no content — the comparison is in the query, not in the interface. Both identities may save; every write is keyed by its owner. Unsaved content is lost when the tab is closed, and that is open rather than decided.

**Product principles.** The confirmation follows the write. A refusal never removes the author's text. What an identity must not see never reaches their browser. Nothing on the surface suggests a capability the product does not have.

```text
## Day 5 - Privacy here is a read-model rule
Context: the main session concluded the privacy rule already held because the
  page shows nothing to the second identity. The control session checked that
  against the material and it does not hold: the page shows nothing because the
  read selects by owner and that identity owns no record, and the direct route
  answers with the content to any chosen identity.
Decision: what an identity must not see never reaches their browser. The check
  belongs in the query, not in whatever renders the page.
Rationale: an interface that hides content is hiding it from the one person who
  is looking at the interface. The route that does not go through the page is
  where the rule is either real or absent.
Trade-offs: slightly more work in the read path; no way to build a legitimate
  administration view later without returning to this decision.
Impact on product / architecture / QA: PRD section 6; the acceptance check that
  matters exercises the route, not the page.
Open questions: none.
Links: DISCOVERY.md, the control session's Gate 1 finding.

## Day 6 - A denied direct read answers with a refusal [REPLACED Day 7]
Context: the supplied test accepts either a refusal or a not-found answer.
Decision: answer with a refusal, as the more conventional choice.
Rationale: it is honest to a legitimate client that has lost its own record.
Trade-offs: it confirms that a record with that id exists and belongs to
  someone else.
Impact on product / architecture / QA: none — replaced before implementation.
Open questions: whether the existence of a record is itself private here.
Links: the alignment question asked at Gate 2.
REPLACED BY the entry of Day 7. Kept because the reasoning that was
  corrected is worth being able to see.

## Day 7 - A denied direct read answers not-found
Context: the alignment question above was put to the person who set the
  exercise, with the risk stated: the record id is printed in the supplied
  notes and is therefore guessable, so a refusal tells anyone who asks that
  someone else's record exists.
Decision: answer not-found, with no part of the content in the answer.
Rationale: the existence of a person's unpublished content is part of what is
  private. The default the prompt proposed was corrected, which is why it was
  asked rather than chosen silently.
Trade-offs: less informative to a legitimate client. Accepted.
Impact on product / architecture / QA: PRD sections 2, 6 and 12; the direct
  read path; the privacy check.
Open questions: none.
Links: PRD section 14.

## Day 7 - Both identities may save, every write keyed by its owner
Context: the read path was written per owner and the write path targeted one
  constant record. If both were true, one identity's text would replace
  another's and both would be told it had worked.
Decision: each identity holds its own record; a save under one identity never
  touches another's.
Rationale: the contradiction was in the material from the first day and it
  decides whether the privacy rule means anything at all.
Trade-offs: none worth recording at this size.
Impact on product / architecture / QA: PRD sections 3 and 5; the write path;
  the regression check added on Day 11.
Open questions: none.
Links: DISCOVERY.md, contradiction 1.

## Day 9 - The refusal message names where the text is
Context: two people were shown the prototype. Both read "Save failed" as "your
  text is gone", and one copied the text out of the field before doing anything
  else.
Decision: the message says the text is still here and invites a retry.
Rationale: the only question a person has at that moment is where their text
  is. Answering anything else is answering a question nobody asked.
Trade-offs: a longer message in a small region.
Impact on product / architecture / QA: the copy; PRD section 13, as a
  requirement derived from what testers said.
Open questions: what happens to unsaved content when the tab is closed —
  raised by one of them, left open on purpose.
Links: the Gate 3 showings.

## Day 11 - The write is keyed by its owner in the connected code
Context: the control session found that the connected write still targeted the
  constant record id, so a save under the second identity replaced the author's
  confirmed content, and none of the three supplied checks caught it because
  none of them saves as the second identity.
Decision: key the write by its owner, and add a regression check that saves as
  the second identity and then reads as the author.
Rationale: the decision of Day 7 had been recorded and not implemented.
  A defect that no check catches comes back.
Trade-offs: none. The three supplied checks were not touched.
Impact on product / architecture / QA: the write path; the test file.
Open questions: none.
Links: the control session's Gate 5 finding, with its reproduction.

## Day 12 - Shipped without a review by the trades
Context: the release note was prepared for the leads of the trades concerned.
  This exercise has no trades to convene, no staging and no production.
Decision: the person who set the exercise gave the go-ahead alone, and the gap
  was written down rather than dressed up.
Rationale: nobody should have to reconstruct later what was read before it went
  out. A gap named is the request made next time.
Trade-offs: no independent technical reading of the change.
Impact on product / architecture / QA: none on the product; it is the standing
  request to whoever would own the delivery path.
Open questions: none.
Links: the release note; the journal entry of the same date.
```

**Open questions.** What happens to unsaved content when the tab is closed. Whether a version history is ever wanted. Neither is being worked on.

### `docs/PROJECT_JOURNAL.md`

```text
## Day 4 - Gate 0, project folder
Context: the case opened with a report of text lost after a save that appeared
  to succeed.
Decision or change: one folder created; every source put in it; memory and
  journal created before anything else ran; project channel created with the
  people who would test later already in it.
Implementation summary: the inventory and conversion prompt ran; converted
  files written to converted/; index written to converted/INDEX.md.
Verification performed: read the index and the list of what could not be read.
  Originals untouched. Nothing sent anywhere.
Trade-offs: eleven rows of the converted spreadsheet carry no date and were
  marked as undated rather than guessed at.
Follow-ups: password obtained for one file and the prompt re-run on it; the
  screen recording watched and a one-paragraph note written beside it.
Links: converted/INDEX.md.

## Day 5 - Gate 1, discovery and the first control pass
Context: the material disagreed with itself about whether there is one record
  or one per author, and said nothing about what a denied read should answer.
Decision or change: DISCOVERY.md written with the source list, the two
  contradictions, the three gaps, the hypothesis, the privacy risks and the
  constraints. Privacy established as a read-model rule.
Implementation summary: none — nothing was changed in the application at this
  gate.
Verification performed: ran the supplied acceptance checks to see the actual
  starting point: three failures, on the reload, on the refusal answer and on
  the direct read. Three errors instead would have meant the environment
  blocked the tests' own local server.
Trade-offs: the contradictions were left unresolved here on purpose and carried
  to the alignment questions.
Follow-ups: two alignment questions for Gate 2.
Links: DISCOVERY.md; the control session's finding; the memory entry of the
  same date.

## Day 7 - Gate 2, the PRD
Context: the alignment questions were answered, one of them against the default
  the prompt had proposed.
Decision or change: one document written, its first section the strategic
  framing, fourteen sections, every assumption marked as an assumption.
Implementation summary: none.
Verification performed: read back section by section against DISCOVERY.md; each
  assumption traced to the source it rests on, or to what is missing and who
  decides it.
Trade-offs: the tab-close question was left open rather than answered in
  passing.
Follow-ups: the PRD is what every later prompt reads.
Links: the PRD; the memory entries of Day 6 and Day 7.

## Day 8 - Gate 3, the absence of a design system recorded
Context: there is no design system and no reference product for this exercise.
Decision or change: the supplied application's own markup and the platform's
  default form controls stand in, agreed with the person who set the exercise.
Implementation summary: named as a constraint in the design prompt.
Verification performed: none to perform; this is a record, not a change.
Trade-offs: nothing here demonstrates a design system, and the substitute is
  what the prototype and the code both follow so they stay the same product.
Follow-ups: the components and their states, the tokens, what may be extended
  and who maintains it are on the standing request list. Nobody was invented
  to own them.
Links: PRD section 8.

## Day 9 - Gate 3, what the showings changed
Context: the prototype was shown twice to two people who write updates.
Decision or change: the refusal message rewritten to name where the text is;
  the confirmation now names when it was confirmed.
Implementation summary: in the prototype only; no code existed yet.
Verification performed: second showing; both said it was the tool they needed,
  which is what closed the gate.
Trade-offs: a longer message in a small region.
Follow-ups: the tab-close question, left open.
Links: the memory entry of the same date.

## Day 10 - Gate 4, the port, and one road that did not work
Context: no approved starter and no written conventions exist for this
  exercise. The supplied application is the repository.
Decision or change: the gap recorded with whose agreement the substitute was
  used; the PRD, memory and journal moved into docs/ inside the repository;
  every state ported against a service shaped like the store's contract.
Implementation summary: confirmation and error split into two announced
  regions; copy pulled into one place; routes left thin; the mock service
  exposing the store's three operations by name.
Verification performed: walked every state in a browser beside the design. The
  acceptance checks still failed on the same three assertions, which is what
  was expected with nothing connected.
Trade-offs: the first port kept the message in a floating element, as the design
  showed. It is not announced when the surface re-renders on submission, so it
  was reverted to the page's own region. Recorded here because the reason is
  worth keeping, not because the attempt was wise.
Follow-ups: the actions row wraps below a narrow width — drift from the design,
  deliberate.
Links: the PRD; the design output.

## Day 11 - Gate 5, the store connected and the overwrite caught
Context: the mock service was replaced with the store, flow by flow, in the same
  session. The checks were passing and the flows looked right.
Decision or change: the write keyed by its owner; the refusal answered as a
  refusal; the ownership comparison moved into the query so that content an
  identity does not own never reaches the answer.
Implementation summary: one write path, one read by owner, one read by id with
  the owner in the query; the refusal path carrying the submitted text, the
  error region, Retry, and no confirmation.
Verification performed: the control session's reproduction walked by hand —
  save as the second identity, then read as the author — before and after the
  fix. The acceptance checks and the added regression check all passing. All
  four flows walked in a browser as both identities, including two refusals in
  a row and a reload between a refusal and its retry.
Trade-offs: none worth recording at this size.
Follow-ups: none.
Links: the control session's finding; the memory entry of the same date.

## Day 12 - Gate 6, the test round and the release note
Context: demo identities sent to two people who had not seen the build.
Decision or change: three things fixed from what came back — the error region
  now names when the last confirmed save happened; Retry replaces Save while
  the refusal is showing; the actions row wraps on a narrow screen. One request
  declined, with the reason: the confirmation names the time and will not name
  a place, because that would be a claim about storage this product does not
  make.
Implementation summary: copy and layout only.
Verification performed: the acceptance checks and the regression check, all
  passing, run from the application's directory. Walked again in a browser
  after the fixes.
Trade-offs: severities were never used, because there was never more coming
  back than the session could hold.
Follow-ups: no frontend, backend or data lead read this change; there is no
  staging and no production; nothing was deployed, and the application stays
  on loopback. That gap is the request made next time.
Links: the release note; the memory entry of the same date.
```

## What this run did not have

Worth naming plainly, because a worked example that hides its gaps teaches the wrong thing:

- **No design system and no reference product.** A substitute was named, agreed and dated. The system itself is still a request with nobody to address it.
- **No approved starter and no written conventions.** The supplied application's own constraints stood in. Nothing was invented that a reviewer would have had to undo.
- **No backend service to ask.** The supplied module was the contract and the exercise's own notes were the documentation.
- **No staging, no delivery path, no leads to give a go-ahead.** What could be verified was verified and written down; what was not reviewed was named, with who was not there to review it.

Every one of those lines is a request with a name missing from it. That is what they are for: turning "this was harder than it should have been" into something specific enough to ask for.

Text licensed CC BY 4.0. Code MIT.
