# The method

How a human and an agent built a browser extension against an undocumented, changing target
over five weeks, and what made it work.

This is the part that transfers. The extension is in guide 02, the parsing in 03, the storage
in 04 — but if you only read one of these, read this one, because the method is what produced
the rest.

---

## 1. The shape of the collaboration

The division of labour was never negotiated. It fell out of what each side can actually do:

| | The human | The agent |
|---|---|---|
| has | a real browser, a real account, real data | the ability to read 100 MB of JSON and not get bored |
| does | drives the target, captures a run, reloads the build | writes the code, forms hypotheses, tests them |
| decides | what matters, what to try next | what the evidence supports |
| cannot | read a 3 MB payload by hand | see the screen, click anything, hold an account |

The agent never touches the target. Not as a safety rule — as an architecture. The extension
**records responses the browser already received** and originates no requests of its own. That
single constraint decided most of what follows: it means every fact in the dataset is
something the target volunteered to a real user in a real session, and it means the agent's
job is reading, never fetching.

**The loop is one run long.**

```
human browses with the recorder on  →  exports a run  →  hands the agent the file
                                                              ↓
    agent parses it, finds what is there and what is missing, writes it up
                                                              ↓
    agent ships a new version of the extension addressing what the run exposed
                                                              ↓
    human pulls, RELOADS, browses again  →  the next run tests the last fix
```

Each turn of the loop takes a day or two and produces two artefacts: a new version of the
tool, and a permanent note about what was learned. Neither is optional. A run that produced
code but no note leaves the next session to rediscover it; a run that produced a note but no
code leaves the tool unable to capture the thing next time.

### Why "phase 0" was the best decision in the project

The first build was deliberately stupid. It had no parsers, no model of the domain, no
opinion about what the data meant. It recorded response bodies to disk and did nothing else.
Its own README opened with **"This is not the product."**

That is the decision to copy. The instinct is to write the real extension — the one with the
features — and discover the payload shapes as you go. Doing it the other way round means:

- Every parser you eventually write is written against **captured evidence**, not a guess.
- When a hypothesis is wrong, you re-derive from stored bodies for free instead of asking the
  human to reproduce a browsing session.
- The recorder keeps working when the target changes; only the parsers break.

Concretely, this paid off three times in five weeks. Twice a conclusion was overturned and the
fix was a re-run of the deriver over data already in hand. Once a feature added in week five
found events sitting in a capture from week three that the week-three code had walked straight
past. None of that is possible if you only keep what you understood at the time.

**Keep the evidence, derive the meaning.** Two directories, two formats, one direction of
travel: verbatim captures in, derived files out, and the derived files are always
regenerable.

---

## 2. Evidence discipline

The project's stated rules, in the order they earn their keep:

### Fail closed

Anything unrecognised is counted and skipped. Never guess an identity, never invent a
timestamp. A record we could not attribute is reported as unattributed — not merged into
whoever was nearest.

This sounds obvious and is violated constantly by well-meaning code. `foo || 0` turns "we
never saw this" into "this is zero". `res || {}` turns "no answer came back" into "an answer
with nothing in it" — that one cost two capture runs, see guide 02. The rule is: **missing is
`null`, and `null` never becomes `0`, `''`, or `{}`.**

### Verify against an artefact the system itself produces

This is the single highest-value habit in the project. Every claim that survived was checked
against something the target emitted independently; every claim that collapsed was plausible
and unchecked.

Worked example. Object ids turned out to embed a creation time in their high bits — shift
right by a fixed number and you get epoch milliseconds. That is a *derivation*, not a field,
so it needs proof. Three independent checks, all against the target's own output:

1. Compared against a pagination cursor the target generated — **27 ms** apart.
2. Compared against the target's own rendered relative-age strings ("6 h", "2 d") — six for six.
3. Later, an id minted by the server during a write, compared against the recorder's own clock
   at the moment of capture — **0.269 s**, which is a network round trip.

Two independent clocks agreeing to within a round trip is about as good as this kind of
evidence gets. One clock agreeing with itself is not evidence at all.

### Read the markup, not the English

Where a payload renders a sentence with a name in it, the name's character range is usually
marked in the payload — an attribute span, a component key, a region name. Read *that*.
Parsing the sentence works until the string is localised, pluralised, or A/B tested.

The generalisation: **prefer the structural marker the system uses to lay out the page over
the human-readable text it lays out.** When two things are indistinguishable by component and
typography, look for the system's own name for the region containing them. It is nearly always
there, because the system needs it too.

### One sample gives you an example, not a grammar

A pattern written against a single captured example matched two of the next five cases. The
identifier it parsed had three spellings in the wild and one of them was not a spelling
variant at all — it was a different type of object arriving through the same endpoint.

Two distinct mistakes wearing the same coat, and they need opposite fixes: a spelling variant
means loosen the pattern; a different type means branch, because loosening would have produced
a parser that builds a valid-looking URL pointing at nothing.

When the sample is a structured identifier with a type in it, **read the type as data rather
than baking it into the pattern.**

### A completeness statistic is only evidence if completeness is independent of what you measure

The worst error in the project. A conclusion rested on a sample of 70 payloads "59 of which
were whole". The 11 that were truncated were the only 11 large enough to contain the thing
being looked for — the truncation limit selected against the evidence, perfectly.

"59 of 70 were whole" and "the 11 that were not are the only ones that could have held the
answer" are the same sentence about the same run, and only one of them is worth saying.

Whenever a cap, a limit, a page size or a sample boundary is in play, ask whether it selects
for the thing being tested. And when a fix for such a limit has shipped, **check the build
stamp of the run you are about to call the recapture** — see guide 02, because this is exactly
why builds stamp themselves.

### Fail loudly at the boundaries, quietly in the middle

Skipping an unrecognised record is correct: it is counted in an `unparsed` tally and the run
continues. But a *delivery* that fails must be loud, because there is no tally that will catch
it. The distinction is whether the failure is about data you did not understand (quiet, count
it) or about a mechanism that did not work (loud, stop).

---

## 3. Corrections stay where the mistake was

Every doc in this project carries its own errata. A falsified claim is not edited away — it is
struck through or annotated in place, at the point it was made, with the correction next to it.

```
**CORRECTION (date).** The paragraph that used to stand here said X, on the stated guess
that Y. The first real capture answered it: they are not. Across 17 payloads, ...
```

Three reasons this is worth the ugliness:

1. **A confident wrong claim in a doc is a trap for the next reader**, including the next
   session of the same agent. Deleting it removes the trap but also removes the warning.
2. **The reasoning that produced the error is reusable.** "Statistic that selected against its
   own evidence" is a failure mode you will meet again; "we were wrong about payload shapes"
   is not.
3. **It calibrates the rest of the document.** A doc with visible corrections is one you can
   trust the uncorrected parts of.

The project keeps a running "what we have been wrong about" section in its handover doc, and
numbered lessons in a `NOTES.md` that is append-only. Thirty-three entries in five weeks. The
numbers get cited from code comments — a parser that does something surprising says
`see NOTES #31` and the reasoning is one click away, permanently, instead of living in a chat
log nobody will search.

**Write the note when the lesson is fresh, not when the feature is finished.** Half of these
entries would not exist if they had been deferred to the end of the session.

---

## 4. Version every change, and make the build say what it is

Every behaviour change gets a version bump and a changelog entry that explains **why**, with
the measured evidence in it, not just what changed. The changelog is the project's narrative
spine; a reader can go from "why does this do that" to the run that caused it in one hop.

This became non-negotiable after a silent failure that cost ten days. Files on disk were
updated twice, but a browser extension keeps running the build it loaded until it is explicitly
reloaded — so the captures kept coming back as if the new code did not exist, looking exactly
like a quiet week on the target.

The structural fix, and the transferable one: **every artefact the tool produces states which
build produced it.** Not "when was it exported" — which build, loaded when. Diagnosing the
original incident required inferring a version from the wording of an internal reason string,
which worked and should never have been necessary. Provenance about the tool belongs beside
provenance about the data.

Corollary: the UI shows the running build, and turns red when the build has been loaded for
more than a few days. Both surfaces show it, because the one the human opens first is the one
that has to tell them.

---

## 5. Tests, and what they are for here

The test suite is zero-dependency and grew to 176 tests. Its job is not coverage. Its job is
to **pin the lessons** so they cannot be un-learned by a later edit.

Read the test names and you get the project's history:

```
extractor: the person is named by the page URL, not by the payload
extractor: a section heading is structure, not something the person wrote
extractor: with no skills control, nothing is guessed at
db: an export with no blob reports null contents, never an empty file
db: archiving a 113 MB run never evicts the run being archived
parser: a card with no id yields nothing, never the previous id
```

Each of those is a bug that happened, or a rule that was hard-won. A test named
`test('handles null input')` pins a behaviour; a test named
`the label goes with the value` pins a *decision*, and tells the next reader why the code looks
the way it does.

Two practices worth stealing:

- **Fixtures are hand-authored and synthetic**, mirroring the structure of real payloads with
  obviously fake identities. Real captures stay in the private data store and never become
  test data. This is a privacy rule that turned out to be a design rule too: writing a
  synthetic fixture forces you to state what the structure actually is.
- **The runner auto-discovers test files.** It used to have a hand-maintained import list, and
  a new test file sat there for two versions passing vacuously because nothing imported it.
  The fix includes a guard that exits non-zero if it discovers zero files, because "no tests
  found" and "all tests passed" must never look the same.

---

## 6. Reproduce before you theorise

The most recent bug looked exactly like a storage-eviction problem: the caps were tight, a run
had just added a very large file, and the newest file was missing. The theory fit every
observed fact.

It was wrong. A fifteen-line script against the real storage function with realistic sizes
disproved it in one run — eviction walks oldest-first and could not reach the newest entry.
The reproduction cost less than the theorising had already cost, and without it the "fix"
would have been a rewrite of eviction with the actual bug untouched.

**When you have a theory that fits, the cheapest next move is usually to try to falsify it
with fifteen lines, not to act on it.** Especially when the theory is comfortable.

---

## 7. What to do when the session dies

Agent sessions are ephemeral; the work must not be. This project survived a container reset
that destroyed every local file mid-task, and lost nothing, because of three habits:

1. **Commit and push at the end of every meaningful unit**, not at the end of the session.
   The session does not get to choose when it ends.
2. **The handover doc is written for a stranger**, and updated as part of the work rather than
   as a farewell. It states the data contract, what is settled, what is open, and what the
   project has been wrong about.
3. **Nothing important lives only in the conversation.** If a decision matters, it is in a
   numbered note, a changelog entry, a test name, or a code comment — four places that travel.

The one thing that did not survive was access: the credentials to reach the stores were in the
destroyed container. Expect to ask the human for those again, and design so that is the *only*
thing you need to ask for.

---

## 8. The failure modes to expect

Ranked by how much time they cost here.

| Failure | Looks like | Guard |
|---|---|---|
| Running an old build | A quiet week on the target | Stamp the build into every artefact; show it in the UI |
| A limit that selects against your evidence | A clean, confident, wrong conclusion | Ask what the cap excludes before trusting a sample |
| A boundary that fails only when data gets big | Worked for months, then a corrupt file | Never pass bulk data through a channel with a size limit |
| A pattern written against one example | Silent under-collection; no error anywhere | Read types as data; count what you skip |
| A test file nothing imports | Green suite, zero assertions | Auto-discover; fail loudly on zero found |
| Deriving a fact from the thing it points at | Plausible timestamps that are all wrong | Verify against an artefact the system produced |

Every one of these is silent by nature. That is the pattern: in this kind of work, the
expensive bugs do not throw. They return something reasonable-looking, and you find out weeks
later when a number does not match a screenshot.

Which is the last rule, and maybe the first: **the human looking at the real screen is a test
oracle you cannot replicate.** When they say "that doesn't look right", it is usually cheaper
to believe them first and check second.
