← Working with agents
Choice Architecture as intent

Spec before code

Green means agreement, until something outside can disagree.

An agent writes a function and twelve tests, and every test passes. That tells you the code agrees with the agent’s own reading of the ticket. It does not tell you the reading was right. Let’s find out what it takes to get a test that could have said no.

The judgment to keep

Decide where the truth your tests check comes from, what that source can and cannot catch, and when writing the behavior down first is worth its cost.

TypeScript
01 / The decision

Its tests will go green. What will green mean?

If you have asked an agent to “add tests too”, you have already made this choice without naming it. The agent that wrote the code also chose what the code should do, then wrote tests that check it does that. The loop closes on itself. Nothing in it can notice that the choice was wrong.

Everything on this page is real. The agents below were run on 12 September 2026, and every file they wrote is kept in this repository byte for byte with its hash. The run notes here record the model for the two agents that wrote code, sonnet; the transcripts themselves are kept outside the repository, and only their hashes are here. Where an agent did something nobody asked for, the page says so.

Our shared scenarioSummarize how each page was read

summarizeReading(events) turns this site’s reading events into one summary per page for an analytics dashboard: views, dwell time, scroll depth, milestones, and whether the page was really read.

What the dashboard needs
Numbers a product decision can rest on: a sleeping laptop must not read as three hours, and a bounce must not read as engagement.
What the agent writes
The function and a vitest suite, run until green.
What must be true of green
A test that passes should mean the code does what the product intends, not only what the agent guessed.

Before any code existed, the intended behavior was written down as a contract: eleven numbered rules, R1 to R11. It was hashed at 18:33 UTC and kept away from the first agent. That order is the whole point of the lesson’s title.

Read the contractEleven rules, written before the code
contract.md
# summarizeReading — contract

Written 12 September 2026, before any implementation of this function existed.

The analytics dashboard shows how each page on the site was read. `summarizeReading` turns the browser tracker's raw events for one reader into one summary per page. It is a pure function.

## Types

```ts
export type ReadingEventKind = 'page_view' | 'heartbeat' | 'scroll' | 'page_leave';

export interface ReadingEvent {
	kind: ReadingEventKind;
	/** The page's path, as the tracker saw it. */
	path: string;
	/** When the event happened, in milliseconds since the epoch. */
	at: number;
	/** heartbeat and page_leave: visible time since the previous slice, in milliseconds. */
	durationMs?: number;
	/** scroll and page_leave: how far down the page the reader has been, 0 to 100. */
	depth?: number;
}

export type Milestone = 25 | 50 | 75 | 100;

export interface PageSummary {
	path: string;
	views: number;
	dwellMs: number;
	depth: number;
	milestones: Milestone[];
	engaged: boolean;
}

export function summarizeReading(events: readonly ReadingEvent[]): PageSummary[];
```

## Rules

**R1 · Which pages.** There is one summary for each page that has at least one `page_view`. Events for a page that was never viewed are ignored: they produce no summary and affect nothing.

**R2 · Same page.** Two paths name the same page when they are equal after removing a single trailing `/`. The root path `/` is left as it is. A summary's `path` is that normalized form. Nothing else is normalized: case matters, and `//` is not collapsed.

**R3 · Order.** Summaries are ordered by each page's earliest `page_view` time, earliest first. Pages whose earliest `page_view` has the same time are ordered by `path`, comparing strings with `<`.

**R4 · Views.** `views` is the number of `page_view` events for the page.

**R5 · Dwell.** `dwellMs` is the sum of `durationMs` over the page's `heartbeat` and `page_leave` events. On any other kind, `durationMs` is ignored. A `durationMs` that is absent, negative, or not a finite number contributes 0. A single event contributes at most 30 minutes, 1,800,000 ms; a longer value counts as exactly 1,800,000. Timestamps never contribute to dwell: two events an hour apart with no durations have a dwell of 0.

**R6 · Depth.** `depth` is the greatest `depth` among the page's `scroll` and `page_leave` events, after clamping each value to the range 0 to 100 and rounding it down to a whole number. On any other kind, `depth` is ignored. An absent or non-finite `depth` is ignored. A page with no usable depth has `depth` 0.

**R7 · Milestones.** `milestones` lists, in ascending order, each of 25, 50, 75 and 100 that is less than or equal to `depth`.

**R8 · Engaged.** `engaged` is true when `dwellMs` is at least 10,000 and `depth` is at least 50. Both must hold.

**R9 · Arrival order.** Events can arrive in any order. The result depends only on which events are present, never on their position in the array.

**R10 · Purity.** The input array and the events in it are never modified. The same input always produces an equal result.

**R11 · Nothing to read.** An empty array produces an empty array.
02 / Three options

Three places a test’s truth can come from.

These are not three levels of virtue. Each is the right call somewhere. What separates them is the question a green run can answer.

A

Let it test its own work

Does the code agree with the agent that wrote it?

The same reader writes the implementation and the suite, and runs it until green.

Cheapest by far. Enough when the result is visible, reversible, and used once. Green shows agreement with one reading.
B

Tests from reading the code

Does the code keep doing what it does now?

A second person or agent reads the implementation and writes a suite for it.

Catches what a careful reader notices. Inherits every decision the code already made, and cannot see a promise with no line behind it.
C

Spec first, blind tests

Does the code do what we said it should?

The behavior is written down before the code, and a writer that cannot see the code derives the suite.

Costs a written contract. The suite can disagree with the code; it can never know more than the document.
What can each source of truth catch?
OptionThe suite checks againstIt can catchIt cannot catch
A · Test its own workThe author’s reading of the request.Slips between what the agent meant and what it typed.A wrong reading. The tests share it.
B · From the codeWhat the code visibly does, and its comments.Regressions, and gaps a careful reader spots.Decisions nobody wrote down, and promises with no line behind them.
C · Spec, then blindA document written before the code.Code that disagrees with the document, including bugs that already exist.Anything the document does not say.
03 / Break the circle

Twelve green tests, and a suite that says no.

The first agent got an ordinary ticket: the types, and one sentence of intent per field. No contract. It wrote a clean implementation and twelve tests, and ran them green.

Then two more agents got the contract and a single tool, Write. They could not open a file or run a command, so they never saw this code and could not tune their tests until they passed. The first was dispatched directly. The second was spawned later, unasked, by the agent in the next section, and it wrote the second blind suite. Neither saw the other’s suite. Here is what happened when all three suites ran against the first agent’s code.

Its own tests

12 of 12 pass

Written by the agent that wrote the code.

Full output7 lines, as recorded
its own tests · 12 Sep 2026
$ npx vitest run --project server src/lib/features/reading-summary/   # the instruction run, its own tests, as written
 RUN  v4.1.11 /Users/sj/Desktop/dev/builds/heyrian
 Test Files  1 passed (1)
      Tests  12 passed (12)
$ echo $?
0
Blind suite

22 of 64 fail

Written from the contract by an agent that could only write.

  • R12/2
  • R24/6
  • R32/3
  • R54/11
  • R63/8
  • R82/5
  • R92/2
  • property2/2
  • other1/8

other: failing tests whose names cite no contract rule.

  • a page with only heartbeat/scroll/page_leave events, never a page_view, produces no summary
  • an unviewed page's events do not leak into a different, viewed page's summary
  • a trailing slash does not create a second page
  • // normalizes to / by removing its single trailing slash, colliding with the root page
  • a path with two trailing slashes only has one removed, so it differs from the single-trailing-slash form
  • and 17 more
Full output617 lines, as recorded
the blind suite against its code · 12 Sep 2026
$ vitest run   # the blind suite, written from the contract, against the instruction run’s code

 RUN  v4.1.11 <run>

 ✓ barrier.spec.ts > R11 - nothing to read > an empty array of events produces an empty array of summaries 1ms
 × barrier.spec.ts > R1 - a page only exists in the output if it was viewed > a page with only heartbeat/scroll/page_leave events, never a page_view, produces no summary 5ms
   → events for an unviewed page must not fabricate a summary for it: expected [ { path: '/never-viewed', …(5) } ] to deeply equal []
 × barrier.spec.ts > R1 - a page only exists in the output if it was viewed > an unviewed page's events do not leak into a different, viewed page's summary 1ms
   → only the viewed page should appear in the output: expected [ '/viewed', '/never-viewed' ] to deeply equal [ '/viewed' ]
 × barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > a trailing slash does not create a second page 1ms
   → trailing slash is the same page, so both views land on one summary: expected [ …(2) ] to have a length of 1 but got 2
 ✓ barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > the root path / is left exactly as it is 0ms
 × barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > // normalizes to / by removing its single trailing slash, colliding with the root page 0ms
   → root and // must merge into a single page named /: expected [ { path: '/', views: 1, …(4) }, …(1) ] to have a length of 1 but got 2
 × barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > a path with two trailing slashes only has one removed, so it differs from the single-trailing-slash form 1ms
   → trailing-slash removal is a single pass, not repeated, so /blog/ and /blog// are different pages: expected [ '/blog/', '/blog//' ] to deeply equal [ '/blog', '/blog/' ]
 ✓ barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > case is never normalized: /Blog and /blog are different pages 0ms
 × barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > an internal double slash is never collapsed, only a trailing slash is touched 1ms
   → these normalize to the same page: only the trailing slash is removed, the internal // stays: expected [ …(2) ] to have a length of 1 but got 2
 × barrier.spec.ts > R3 - summaries are ordered by earliest page_view time, then by path > the page viewed first comes first 1ms
   → expected [ '/second', '/first' ] to deeply equal [ '/first', '/second' ]
 ✓ barrier.spec.ts > R3 - summaries are ordered by earliest page_view time, then by path > a page's earliest page_view is its minimum across all its page_views, regardless of array position 0ms
 × barrier.spec.ts > R3 - summaries are ordered by earliest page_view time, then by path > ties on earliest page_view time break on path using plain <, not a locale-aware compare 0ms
   → ties must be broken with the plain < operator on paths, not a locale-aware compare: expected [ '/banana', '/Cherry' ] to deeply equal [ '/Cherry', '/banana' ]
 ✓ barrier.spec.ts > R4 - views counts page_view events for the page > duplicate page_view events are each counted, not deduplicated 0ms
 ✓ barrier.spec.ts > R4 - views counts page_view events for the page > non-page_view events never add to views 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > heartbeat and page_leave durations are added together 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > durationMs on any other kind of event is ignored 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a page_view carrying durationMs contributes nothing to dwell 0ms
 × barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a negative durationMs contributes 0, it does not subtract from other events' dwell 0ms
   → a negative duration must floor at 0 contribution, it must never reduce the running total: expected 500 to be 1000 // Object.is equality
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > an absent durationMs contributes 0 0ms
 × barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a non-finite durationMs (NaN) contributes 0 0ms
   → expected NaN to be +0 // Object.is equality
 × barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > an infinite durationMs contributes 0, distinct from the cap that applies to large finite values 0ms
   → Infinity is not finite and must contribute 0; it must not be silently clamped to the 1,800,000 cap: expected Infinity to be +0 // Object.is equality
 × barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a single event contributes at most 1,800,000ms; a longer finite value is capped exactly there 0ms
   → a single event's dwell contribution is capped at 30 minutes: expected 5000000 to be 1800000 // Object.is equality
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a duration exactly at the cap is not reduced further 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > the cap applies per event, not to the running total 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > timestamps never contribute to dwell: a gap with no durations dwells for 0ms 0ms
 ✓ barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > the maximum among scroll and page_leave depths wins, not the latest one 0ms
 ✓ barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > depth on any other kind of event is ignored 1ms
 × barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a depth above 100 is clamped to 100, not ignored 0ms
   → an out-of-range depth is clamped into range, it is not dropped: expected 150 to be 100 // Object.is equality
 ✓ barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a negative depth clamps to 0 0ms
 × barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a fractional depth is rounded down, not to the nearest whole number 0ms
   → depth rounds down (floor); 87.9 must not become 88: expected 87.9 to be 87 // Object.is equality
 ✓ barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > an absent depth is ignored 0ms
 × barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a non-finite depth (NaN) is ignored, it does not win the max over a real value 0ms
   → NaN must not be treated as larger than every real depth: expected NaN to be 40 // Object.is equality
 ✓ barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a page with no usable depth data defaults to 0 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 0, milestones are [] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 10, milestones are [] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 24, milestones are [] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 25, milestones are [25] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 49, milestones are [25] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 50, milestones are [25,50] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 74, milestones are [25,50] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 75, milestones are [25,50,75] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 99, milestones are [25,50,75] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 100, milestones are [25,50,75,100] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > a depth that clamps to 100 still yields every milestone 0ms
 ✓ barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=10000, depth=50 -> engaged=true (meets both minimums exactly) 0ms
 × barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=9999, depth=100 -> engaged=false (one millisecond short of the dwell minimum, however deep) 0ms
   → engaged requires both conditions to hold; neither one alone is sufficient: expected true to be false // Object.is equality
 × barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=100000, depth=49 -> engaged=false (one point short of the depth minimum, however long) 0ms
   → engaged requires both conditions to hold; neither one alone is sufficient: expected true to be false // Object.is equality
 ✓ barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=0, depth=0 -> engaged=false (no engagement signal at all) 0ms
 ✓ barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=50000, depth=100 -> engaged=true (comfortably exceeds both minimums) 0ms
 ✓ barrier.spec.ts > every event kind affects only the fields the contract assigns to it > page_view affects only views and ordering, never dwell or depth 0ms
 ✓ barrier.spec.ts > every event kind affects only the fields the contract assigns to it > heartbeat affects only dwell, never views or depth 0ms
 ✓ barrier.spec.ts > every event kind affects only the fields the contract assigns to it > scroll affects only depth, never views or dwell 0ms
 ✓ barrier.spec.ts > every event kind affects only the fields the contract assigns to it > page_leave affects both dwell and depth, never views 0ms
 × barrier.spec.ts > fail-closed on data that does not match the compile-time shape (inference, see comment above) > a string where durationMs should be contributes 0; it is not parsed or coerced 0ms
   → a non-number durationMs must not be parsed or coerced: expected '05000' to be +0 // Object.is equality
 ✓ barrier.spec.ts > fail-closed on data that does not match the compile-time shape (inference, see comment above) > null where depth should be is ignored; it does not throw and does not win a comparison it should lose 0ms
 × barrier.spec.ts > R9 - the result depends only on which events are present, never their order > a small hand-picked reordering produces an identical result 0ms
   → array position must never change the outcome, only which events exist: expected [ Array(2) ] to deeply equal [ Array(2) ]
 × barrier.spec.ts > R9 - the result depends only on which events are present, never their order > property: random shuffles of the same events match a reference model built directly from the contract's prose 1ms
   → trial 0: the implementation must match the contract's own rules for aggregation, ordering and filtering: expected [ …(8) ] to deeply equal [ …(3) ]
 ✓ barrier.spec.ts > R10 - purity: the input is never modified, and the same input always yields an equal result > the events array and its elements are the same references after the call 0ms
 ✓ barrier.spec.ts > R10 - purity: the input is never modified, and the same input always yields an equal result > a deep-frozen input does not throw, proving no mutation is attempted 0ms
 ✓ barrier.spec.ts > R10 - purity: the input is never modified, and the same input always yields an equal result > calling summarizeReading twice on the same input yields an equal result both times 0ms
 × barrier.spec.ts > property: adding a dwell-bearing event never decreases a page's dwellMs > dwellMs is monotone under any additional heartbeat/page_leave event, valid or not 0ms
   → trial 0: a minimum contribution of 0 means dwell can only stay the same or grow, never shrink: expected NaN to be greater than or equal to 5000500
 × barrier.spec.ts > property: adding a depth-bearing event never decreases a page's depth > depth is monotone under any additional scroll/page_leave event, valid or not 0ms
   → trial 2: depth is a running maximum; it can only stay the same or grow, never shrink: expected NaN to be greater than or equal to 50
 ✓ barrier.spec.ts > worked example: a full pipeline from raw events to one summary > views, dwell, depth, milestones and engaged are all derived consistently from the same events 0ms
 ✓ barrier.spec.ts > a lone page_view with no other signal starts from a clean baseline > views=1, dwellMs=0, depth=0, milestones=[], engaged=false 0ms

 Test Files  1 failed (1)
      Tests  22 failed | 42 passed (64)


⎯⎯⎯⎯⎯⎯ Failed Tests 22 ⎯⎯⎯⎯⎯⎯⎯

 FAIL  barrier.spec.ts > R1 - a page only exists in the output if it was viewed > a page with only heartbeat/scroll/page_leave events, never a page_view, produces no summary
AssertionError: events for an unviewed page must not fabricate a summary for it: expected [ { path: '/never-viewed', …(5) } ] to deeply equal []

- Expected
+ Received

- []
+ [
+   {
+     "depth": 90,
+     "dwellMs": 6000,
+     "engaged": true,
+     "milestones": [
+       25,
+       50,
+       75,
+     ],
+     "path": "/never-viewed",
+     "views": 0,
+   },
+ ]

 ❯ barrier.spec.ts:255:5
    253|    summarizeReading(events),
    254|    'events for an unviewed page must not fabricate a summary for it'
    255|   ).toEqual([]);
       |     ^
    256|  });
    257|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/22]⎯

 FAIL  barrier.spec.ts > R1 - a page only exists in the output if it was viewed > an unviewed page's events do not leak into a different, viewed page's summary
AssertionError: only the viewed page should appear in the output: expected [ '/viewed', '/never-viewed' ] to deeply equal [ '/viewed' ]

- Expected
+ Received

  [
    "/viewed",
+   "/never-viewed",
  ]

 ❯ barrier.spec.ts:264:5
    262|    result.map((s) => s.path),
    263|    'only the viewed page should appear in the output'
    264|   ).toEqual(['/viewed']);
       |     ^
    265|   expect(result[0].dwellMs, 'dwell from an unrelated unviewed page mus…
    266|   expect(result[0].depth, 'depth from an unrelated unviewed page must …

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/22]⎯

 FAIL  barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > a trailing slash does not create a second page
AssertionError: trailing slash is the same page, so both views land on one summary: expected [ …(2) ] to have a length of 1 but got 2

- Expected
+ Received

- 1
+ 2

 ❯ barrier.spec.ts:278:88
    276|   const events = [pv('/blog', 100), pv('/blog/', 200)];
    277|   const result = summarizeReading(events);
    278|   expect(result, 'trailing slash is the same page, so both views land …
       |                                                                                        ^
    279|   expect(result[0].views, '/blog and /blog/ are the same page and both…
    280|  });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/22]⎯

 FAIL  barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > // normalizes to / by removing its single trailing slash, colliding with the root page
AssertionError: root and // must merge into a single page named /: expected [ { path: '/', views: 1, …(4) }, …(1) ] to have a length of 1 but got 2

- Expected
+ Received

- 1
+ 2

 ❯ barrier.spec.ts:293:71
    291|   const events = [pv('/', 1), pv('//', 2)];
    292|   const result = summarizeReading(events);
    293|   expect(result, 'root and // must merge into a single page named /').…
       |                                                                       ^
    294|   expect(result[0].path).toBe('/');
    295|   expect(result[0].views, 'both page_views count toward the merged pag…

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/22]⎯

 FAIL  barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > a path with two trailing slashes only has one removed, so it differs from the single-trailing-slash form
AssertionError: trailing-slash removal is a single pass, not repeated, so /blog/ and /blog// are different pages: expected [ '/blog/', '/blog//' ] to deeply equal [ '/blog', '/blog/' ]

- Expected
+ Received

  [
-   "/blog",
    "/blog/",
+   "/blog//",
  ]

 ❯ barrier.spec.ts:308:5
    306|    result.map((s) => s.path).sort(),
    307|    'trailing-slash removal is a single pass, not repeated, so /blog/ a…
    308|   ).toEqual(['/blog', '/blog/']);
       |     ^
    309|  });
    310|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/22]⎯

 FAIL  barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > an internal double slash is never collapsed, only a trailing slash is touched
AssertionError: these normalize to the same page: only the trailing slash is removed, the internal // stays: expected [ …(2) ] to have a length of 1 but got 2

- Expected
+ Received

- 1
+ 2

 ❯ barrier.spec.ts:327:5
    325|    result,
    326|    'these normalize to the same page: only the trailing slash is remov…
    327|   ).toHaveLength(1);
       |     ^
    328|   expect(result[0].path).toBe('/a//b');
    329|  });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/22]⎯

 FAIL  barrier.spec.ts > R3 - summaries are ordered by earliest page_view time, then by path > the page viewed first comes first
AssertionError: expected [ '/second', '/first' ] to deeply equal [ '/first', '/second' ]

- Expected
+ Received

  [
-   "/first",
    "/second",
+   "/first",
  ]

 ❯ barrier.spec.ts:339:55
    337|  it('the page viewed first comes first', () => {
    338|   const events = [pv('/second', 200), pv('/first', 100)];
    339|   expect(summarizeReading(events).map((s) => s.path)).toEqual(['/first…
       |                                                       ^
    340|  });
    341|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[7/22]⎯

 FAIL  barrier.spec.ts > R3 - summaries are ordered by earliest page_view time, then by path > ties on earliest page_view time break on path using plain <, not a locale-aware compare
AssertionError: ties must be broken with the plain < operator on paths, not a locale-aware compare: expected [ '/banana', '/Cherry' ] to deeply equal [ '/Cherry', '/banana' ]

- Expected
+ Received

  [
-   "/Cherry",
    "/banana",
+   "/Cherry",
  ]

 ❯ barrier.spec.ts:358:5
    356|    summarizeReading(events).map((s) => s.path),
    357|    'ties must be broken with the plain < operator on paths, not a loca…
    358|   ).toEqual(['/Cherry', '/banana']);
       |     ^
    359|  });
    360| });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[8/22]⎯

 FAIL  barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a negative durationMs contributes 0, it does not subtract from other events' dwell
AssertionError: a negative duration must floor at 0 contribution, it must never reduce the running total: expected 500 to be 1000 // Object.is equality

- Expected
+ Received

- 1000
+ 500

 ❯ barrier.spec.ts:406:5
    404|    summarizeReading(events)[0].dwellMs,
    405|    'a negative duration must floor at 0 contribution, it must never re…
    406|   ).toBe(1000);
       |     ^
    407|  });
    408|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[9/22]⎯

 FAIL  barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a non-finite durationMs (NaN) contributes 0
AssertionError: expected NaN to be +0 // Object.is equality

- Expected
+ Received

- 0
+ NaN

 ❯ barrier.spec.ts:416:47
    414|  it('a non-finite durationMs (NaN) contributes 0', () => {
    415|   const events = [pv('/p', 0), hb('/p', 1, NaN)];
    416|   expect(summarizeReading(events)[0].dwellMs).toBe(0);
       |                                               ^
    417|  });
    418|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[10/22]⎯

 FAIL  barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > an infinite durationMs contributes 0, distinct from the cap that applies to large finite values
AssertionError: Infinity is not finite and must contribute 0; it must not be silently clamped to the 1,800,000 cap: expected Infinity to be +0 // Object.is equality

- Expected
+ Received

- 0
+ Infinity

 ❯ barrier.spec.ts:424:5
    422|    summarizeReading(events)[0].dwellMs,
    423|    'Infinity is not finite and must contribute 0; it must not be silen…
    424|   ).toBe(0);
       |     ^
    425|  });
    426|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[11/22]⎯

 FAIL  barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a single event contributes at most 1,800,000ms; a longer finite value is capped exactly there
AssertionError: a single event's dwell contribution is capped at 30 minutes: expected 5000000 to be 1800000 // Object.is equality

- Expected
+ Received

- 1800000
+ 5000000

 ❯ barrier.spec.ts:429:110
    427|  it('a single event contributes at most 1,800,000ms; a longer finite v…
    428|   const events = [pv('/p', 0), hb('/p', 1, 5_000_000)];
    429|   expect(summarizeReading(events)[0].dwellMs, "a single event's dwell …
       |                                                                                                              ^
    430|    1_800_000
    431|   );

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[12/22]⎯

 FAIL  barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a depth above 100 is clamped to 100, not ignored
AssertionError: an out-of-range depth is clamped into range, it is not dropped: expected 150 to be 100 // Object.is equality

- Expected
+ Received

- 100
+ 150

 ❯ barrier.spec.ts:475:111
    473|   // absent/non-finite ones, the result would be the default 0, not 10…
    474|   const events = [pv('/p', 0), sc('/p', 1, 150)];
    475|   expect(summarizeReading(events)[0].depth, 'an out-of-range depth is …
       |                                                                                                               ^
    476|    100
    477|   );

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[13/22]⎯

 FAIL  barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a fractional depth is rounded down, not to the nearest whole number
AssertionError: depth rounds down (floor); 87.9 must not become 88: expected 87.9 to be 87 // Object.is equality

- Expected
+ Received

- 87
+ 87.9

 ❯ barrier.spec.ts:487:99
    485|  it('a fractional depth is rounded down, not to the nearest whole numb…
    486|   const events = [pv('/p', 0), sc('/p', 1, 87.9)];
    487|   expect(summarizeReading(events)[0].depth, 'depth rounds down (floor)…
       |                                                                                                   ^
    488|  });
    489|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[14/22]⎯

 FAIL  barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a non-finite depth (NaN) is ignored, it does not win the max over a real value
AssertionError: NaN must not be treated as larger than every real depth: expected NaN to be 40 // Object.is equality

- Expected
+ Received

- 40
+ NaN

 ❯ barrier.spec.ts:497:104
    495|  it('a non-finite depth (NaN) is ignored, it does not win the max over…
    496|   const events = [pv('/p', 0), sc('/p', 1, 40), sc('/p', 2, NaN)];
    497|   expect(summarizeReading(events)[0].depth, 'NaN must not be treated a…
       |                                                                                                        ^
    498|  });
    499|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[15/22]⎯

 FAIL  barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=9999, depth=100 -> engaged=false (one millisecond short of the dwell minimum, however deep)
 FAIL  barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=100000, depth=49 -> engaged=false (one point short of the depth minimum, however long)
AssertionError: engaged requires both conditions to hold; neither one alone is sufficient: expected true to be false // Object.is equality

- Expected
+ Received

- false
+ true

 ❯ barrier.spec.ts:554:5
    552|    summarizeReading(events)[0].engaged,
    553|    'engaged requires both conditions to hold; neither one alone is suf…
    554|   ).toBe(expected);
       |     ^
    555|  });
    556| });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[16/22]⎯

 FAIL  barrier.spec.ts > fail-closed on data that does not match the compile-time shape (inference, see comment above) > a string where durationMs should be contributes 0; it is not parsed or coerced
AssertionError: a non-number durationMs must not be parsed or coerced: expected '05000' to be +0 // Object.is equality

- Expected:
0

+ Received:
"05000"

 ❯ barrier.spec.ts:624:104
    622|   const events = [pv('/p', 0), mkEvent({ kind: 'heartbeat', path: '/p'…
    623|   expect(() => summarizeReading(events)).not.toThrow();
    624|   expect(summarizeReading(events)[0].dwellMs, 'a non-number durationMs…
       |                                                                                                        ^
    625|  });
    626|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[17/22]⎯

 FAIL  barrier.spec.ts > R9 - the result depends only on which events are present, never their order > a small hand-picked reordering produces an identical result
AssertionError: array position must never change the outcome, only which events exist: expected [ Array(2) ] to deeply equal [ Array(2) ]

- Expected
+ Received

  [
    {
-     "depth": 0,
-     "dwellMs": 2000,
-     "engaged": false,
-     "milestones": [],
-     "path": "/a",
-     "views": 1,
-   },
-   {
      "depth": 70,
      "dwellMs": 0,
      "engaged": true,
      "milestones": [
        25,
        50,
      ],
      "path": "/b",
+     "views": 1,
+   },
+   {
+     "depth": 0,
+     "dwellMs": 2000,
+     "engaged": false,
+     "milestones": [],
+     "path": "/a",
      "views": 1,
    },
  ]

 ❯ barrier.spec.ts:645:5
    643|    summarizeReading(reordered),
    644|    'array position must never change the outcome, only which events ex…
    645|   ).toEqual(summarizeReading(inOrder));
       |     ^
    646|  });
    647|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[18/22]⎯

 FAIL  barrier.spec.ts > R9 - the result depends only on which events are present, never their order > property: random shuffles of the same events match a reference model built directly from the contract's prose
AssertionError: trial 0: the implementation must match the contract's own rules for aggregation, ordering and filtering: expected [ …(8) ] to deeply equal [ …(3) ]

- Expected
+ Received

  [
    {
-     "depth": 100,
-     "dwellMs": 1800000,
+     "depth": 0,
+     "dwellMs": 0,
+     "engaged": false,
+     "milestones": [],
+     "path": "/a/b",
+     "views": 1,
+   },
+   {
+     "depth": 0,
+     "dwellMs": 1800001,
+     "engaged": true,
+     "milestones": [],
+     "path": "/blog//",
+     "views": 0,
+   },
+   {
+     "depth": 99.99,
+     "dwellMs": 0,
      "engaged": true,
      "milestones": [
        25,
        50,
        75,
-       100,
      ],
+     "path": "/products/42",
+     "views": 0,
+   },
+   {
+     "depth": 0,
+     "dwellMs": 5000000,
+     "engaged": true,
+     "milestones": [],
      "path": "/blog",
-     "views": 3,
+     "views": 2,
    },
    {
      "depth": 0,
      "dwellMs": 0,
      "engaged": false,
      "milestones": [],
-     "path": "/a/b",
+     "path": "//",
      "views": 1,
    },
    {
      "depth": 0,
+     "dwellMs": 1800001,
+     "engaged": true,
+     "milestones": [],
+     "path": "/a//b",
+     "views": 0,
+   },
+   {
+     "depth": 100,
      "dwellMs": 0,
-     "engaged": false,
-     "milestones": [],
-     "path": "/",
+     "engaged": true,
+     "milestones": [
+       25,
+       50,
+       75,
+       100,
+     ],
+     "path": "/blog/",
      "views": 1,
+   },
+   {
+     "depth": 0,
+     "dwellMs": Infinity,
+     "engaged": true,
+     "milestones": [],
+     "path": "/Blog",
+     "views": 0,
    },
  ]

 ❯ barrier.spec.ts:658:6
    656|     inOrderActual,
    657|     `trial ${trial}: the implementation must match the contract's own …
    658|    ).toEqual(expected);
       |      ^
    659|    expect(shuffledActual, `trial ${trial}: shuffling the same events m…
    660|     expected

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[19/22]⎯

 FAIL  barrier.spec.ts > property: adding a dwell-bearing event never decreases a page's dwellMs > dwellMs is monotone under any additional heartbeat/page_leave event, valid or not
AssertionError: trial 0: a minimum contribution of 0 means dwell can only stay the same or grow, never shrink: expected NaN to be greater than or equal to 5000500
 ❯ barrier.spec.ts:719:6
    717|     after,
    718|     `trial ${trial}: a minimum contribution of 0 means dwell can only …
    719|    ).toBeGreaterThanOrEqual(before);
       |      ^
    720|   }
    721|  });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[20/22]⎯

 FAIL  barrier.spec.ts > property: adding a depth-bearing event never decreases a page's depth > depth is monotone under any additional scroll/page_leave event, valid or not
AssertionError: trial 2: depth is a running maximum; it can only stay the same or grow, never shrink: expected NaN to be greater than or equal to 50
 ❯ barrier.spec.ts:742:6
    740|     after,
    741|     `trial ${trial}: depth is a running maximum; it can only stay the …
    742|    ).toBeGreaterThanOrEqual(before);
       |      ^
    743|   }
    744|  });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[21/22]⎯
$ echo $?
1
Second blind suite

24 of 57 fail

Another write-only agent, same contract, never saw the first suite.

  • R12/3
  • R22/4
  • R32/3
  • R41/3
  • R56/11
  • R65/11
  • R84/8
  • R92/2
  • a page with no page_view produces no summary, no matter what other events it has
  • an unviewed page is invisible to the result even when it sits among viewed pages
  • merges "/about" and "/about/" into one summary named "/about"
  • does not collapse a run of slashes: only a single trailing slash is stripped
  • orders pages by their earliest page_view time, earliest first
  • and 19 more
Full output594 lines, as recorded
a second blind suite against its code · 12 Sep 2026
$ vitest run   # a second blind suite, written from the contract by the contract run’s sub-writer, against the instruction run’s code

 RUN  v4.1.11 <run>

 × implementer.spec.ts > R1 · a summary exists only for pages with at least one page_view > a page with no page_view produces no summary, no matter what other events it has 5ms
   → expected [ { path: '/never-viewed', …(5) } ] to deeply equal []
 × implementer.spec.ts > R1 · a summary exists only for pages with at least one page_view > an unviewed page is invisible to the result even when it sits among viewed pages 1ms
   → expected [ '/viewed', '/never-viewed' ] to deeply equal [ '/viewed' ]
 ✓ implementer.spec.ts > R1 · a summary exists only for pages with at least one page_view > a page_view alone is enough to produce a summary, with zero dwell and depth 0ms
 × implementer.spec.ts > R2 · two paths equal after removing one trailing slash name the same page > merges "/about" and "/about/" into one summary named "/about" 1ms
   → expected [ { path: '/about/', …(5) }, …(1) ] to have a length of 1 but got 2
 ✓ implementer.spec.ts > R2 · two paths equal after removing one trailing slash name the same page > leaves the root path "/" as "/", not as an empty string 0ms
 ✓ implementer.spec.ts > R2 · two paths equal after removing one trailing slash name the same page > treats differently-cased paths as different pages: case is not normalized 0ms
 × implementer.spec.ts > R2 · two paths equal after removing one trailing slash name the same page > does not collapse a run of slashes: only a single trailing slash is stripped 0ms
   → expected [ '/foo//', '/foo/' ] to deeply equal [ '/foo/', '/foo' ]
 × implementer.spec.ts > R3 · summaries are ordered by earliest page_view, then by path > orders pages by their earliest page_view time, earliest first 0ms
   → expected [ '/z', '/a' ] to deeply equal [ '/a', '/z' ]
 ✓ implementer.spec.ts > R3 · summaries are ordered by earliest page_view, then by path > uses the earliest page_view for a page, even if a later page_view for it appears first in the array 0ms
 × implementer.spec.ts > R3 · summaries are ordered by earliest page_view, then by path > breaks a tie in earliest page_view time by comparing paths with < 0ms
   → expected [ '/b', '/a' ] to deeply equal [ '/a', '/b' ]
 ✓ implementer.spec.ts > R4 · views counts page_view events for the page > counts every page_view for the page 0ms
 ✓ implementer.spec.ts > R4 · views counts page_view events for the page > does not count heartbeat, scroll, or page_leave events as views 0ms
 × implementer.spec.ts > R4 · views counts page_view events for the page > counts page_views for raw path spellings that normalize to the same page 0ms
   → expected [ Array(2) ] to have a length of 1 but got 2
 ✓ implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > sums durationMs across heartbeat and page_leave events for the page 0ms
 ✓ implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > ignores durationMs on any kind other than heartbeat or page_leave 0ms
 ✓ implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > a durationMs that is absent contributes 0 to dwell 0ms
 × implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > a durationMs that is negative contributes 0 to dwell 0ms
   → expected -500 to be +0 // Object.is equality
 × implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > a durationMs that is NaN contributes 0 to dwell 0ms
   → expected NaN to be +0 // Object.is equality
 × implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > a durationMs that is Infinity contributes 0 to dwell 0ms
   → expected Infinity to be +0 // Object.is equality
 × implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > a durationMs that is -Infinity contributes 0 to dwell 0ms
   → expected -Infinity to be +0 // Object.is equality
 ✓ implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > two events an hour apart with no durations have a dwell of 0: timestamps never contribute 0ms
 × implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > caps a single event contribution at 30 minutes (1,800,000ms) 0ms
   → expected 2000000 to be 1800000 // Object.is equality
 ✓ implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > does not reduce a value already exactly at the 30-minute cap 0ms
 × implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > applies the 30-minute cap per event, not to the running total: two capped events sum past the cap 0ms
   → expected 4000000 to be 3600000 // Object.is equality
 ✓ implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > takes the greatest depth among the page’s scroll and page_leave events 0ms
 ✓ implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > ignores depth on any kind other than scroll or page_leave 0ms
 ✓ implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > a page with no usable depth has depth 0 0ms
 ✓ implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > a depth that is absent is ignored, not treated as 0 or as a maximum 0ms
 × implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > a depth that is NaN is ignored, not treated as 0 or as a maximum 1ms
   → expected NaN to be 40 // Object.is equality
 × implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > a depth that is Infinity is ignored, not treated as 0 or as a maximum 0ms
   → expected Infinity to be 40 // Object.is equality
 ✓ implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > a depth that is -Infinity is ignored, not treated as 0 or as a maximum 0ms
 × implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > clamps a depth above 100 down to 100 0ms
   → expected 150 to be 100 // Object.is equality
 ✓ implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > clamps a negative depth up to 0 0ms
 × implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > rounds a fractional depth down to a whole number 0ms
   → expected 63.9 to be 63 // Object.is equality
 × implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > clamps and floors together for an out-of-range fractional value 0ms
   → expected 100.9 to be 100 // Object.is equality
 ✓ implementer.spec.ts > R7 · milestones lists each of 25/50/75/100 that is <= depth, ascending > depth +0 yields milestones [] 0ms
 ✓ implementer.spec.ts > R7 · milestones lists each of 25/50/75/100 that is <= depth, ascending > depth 24 yields milestones [] 0ms
 ✓ implementer.spec.ts > R7 · milestones lists each of 25/50/75/100 that is <= depth, ascending > depth 25 yields milestones [ 25 ] 0ms
 ✓ implementer.spec.ts > R7 · milestones lists each of 25/50/75/100 that is <= depth, ascending > depth 49 yields milestones [ 25 ] 0ms
 ✓ implementer.spec.ts > R7 · milestones lists each of 25/50/75/100 that is <= depth, ascending > depth 50 yields milestones [ 25, 50 ] 0ms
 ✓ implementer.spec.ts > R7 · milestones lists each of 25/50/75/100 that is <= depth, ascending > depth 74 yields milestones [ 25, 50 ] 0ms
 ✓ implementer.spec.ts > R7 · milestones lists each of 25/50/75/100 that is <= depth, ascending > depth 75 yields milestones [ 25, 50, 75 ] 0ms
 ✓ implementer.spec.ts > R7 · milestones lists each of 25/50/75/100 that is <= depth, ascending > depth 99 yields milestones [ 25, 50, 75 ] 0ms
 ✓ implementer.spec.ts > R7 · milestones lists each of 25/50/75/100 that is <= depth, ascending > depth 100 yields milestones [ 25, 50, 75, 100 ] 0ms
 ✓ implementer.spec.ts > R8 · engaged requires dwellMs >= 10,000 AND depth >= 50 > 'both exactly at their thresholds' -> engaged is true 0ms
 × implementer.spec.ts > R8 · engaged requires dwellMs >= 10,000 AND depth >= 50 > 'dwell just under its threshold' -> engaged is false 0ms
   → expected true to be false // Object.is equality
 ✓ implementer.spec.ts > R8 · engaged requires dwellMs >= 10,000 AND depth >= 50 > 'depth just under its threshold' -> engaged is false 0ms
 × implementer.spec.ts > R8 · engaged requires dwellMs >= 10,000 AND depth >= 50 > 'ample dwell but shallow scroll' -> engaged is false 0ms
   → expected true to be false // Object.is equality
 × implementer.spec.ts > R8 · engaged requires dwellMs >= 10,000 AND depth >= 50 > 'deep scroll but almost no dwell' -> engaged is false 0ms
   → expected true to be false // Object.is equality
 ✓ implementer.spec.ts > R8 · engaged requires dwellMs >= 10,000 AND depth >= 50 > 'neither dwell nor depth qualifies' -> engaged is false 0ms
 ✓ implementer.spec.ts > R8 · engaged requires dwellMs >= 10,000 AND depth >= 50 > 'comfortably over both thresholds' -> engaged is true 0ms
 × implementer.spec.ts > R8 · engaged requires dwellMs >= 10,000 AND depth >= 50 > is a conjunction, not a disjunction: satisfying only one condition is never enough 0ms
   → expected true to be false // Object.is equality
 × implementer.spec.ts > R9 · the result depends only on which events are present, never on their order > produces an identical result for a reversed event array 0ms
   → expected [ Array(2) ] to deeply equal [ Array(2) ]
 × implementer.spec.ts > R9 · the result depends only on which events are present, never on their order > produces an identical result for an arbitrary shuffle 0ms
   → expected [ Array(2) ] to deeply equal [ Array(2) ]
 ✓ implementer.spec.ts > R10 · summarizeReading does not mutate its input and is deterministic > does not modify the input array or its events (frozen input must survive the call) 0ms
 ✓ implementer.spec.ts > R10 · summarizeReading does not mutate its input and is deterministic > produces an equal result when called twice on the same input 0ms
 ✓ implementer.spec.ts > R11 · an empty array produces an empty array > returns an empty array for no events 0ms

 Test Files  1 failed (1)
      Tests  24 failed | 33 passed (57)


⎯⎯⎯⎯⎯⎯ Failed Tests 24 ⎯⎯⎯⎯⎯⎯⎯

 FAIL  implementer.spec.ts > R1 · a summary exists only for pages with at least one page_view > a page with no page_view produces no summary, no matter what other events it has
AssertionError: expected [ { path: '/never-viewed', …(5) } ] to deeply equal []

- Expected
+ Received

- []
+ [
+   {
+     "depth": 90,
+     "dwellMs": 8000,
+     "engaged": true,
+     "milestones": [
+       25,
+       50,
+       75,
+     ],
+     "path": "/never-viewed",
+     "views": 0,
+   },
+ ]

 ❯ implementer.spec.ts:54:36
     52|   ];
     53|
     54|   expect(summarizeReading(events)).toEqual([]);
       |                                    ^
     55|  });
     56|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/24]⎯

 FAIL  implementer.spec.ts > R1 · a summary exists only for pages with at least one page_view > an unviewed page is invisible to the result even when it sits among viewed pages
AssertionError: expected [ '/viewed', '/never-viewed' ] to deeply equal [ '/viewed' ]

- Expected
+ Received

  [
    "/viewed",
+   "/never-viewed",
  ]

 ❯ implementer.spec.ts:66:37
     64|   const result = summarizeReading(events);
     65|
     66|   expect(result.map((s) => s.path)).toEqual(['/viewed']);
       |                                     ^
     67|   expect(result[0].dwellMs).toBe(0);
     68|   expect(result[0].depth).toBe(0);

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/24]⎯

 FAIL  implementer.spec.ts > R2 · two paths equal after removing one trailing slash name the same page > merges "/about" and "/about/" into one summary named "/about"
AssertionError: expected [ { path: '/about/', …(5) }, …(1) ] to have a length of 1 but got 2

- Expected
+ Received

- 1
+ 2

 ❯ implementer.spec.ts:102:18
    100|   const result = summarizeReading(events);
    101|
    102|   expect(result).toHaveLength(1);
       |                  ^
    103|   expect(result[0].path).toBe('/about');
    104|   expect(result[0].views).toBe(2);

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/24]⎯

 FAIL  implementer.spec.ts > R2 · two paths equal after removing one trailing slash name the same page > does not collapse a run of slashes: only a single trailing slash is stripped
AssertionError: expected [ '/foo//', '/foo/' ] to deeply equal [ '/foo/', '/foo' ]

- Expected
+ Received

  [
+   "/foo//",
    "/foo/",
-   "/foo",
  ]

 ❯ implementer.spec.ts:131:37
    129|   const result = summarizeReading(events);
    130|
    131|   expect(result.map((s) => s.path)).toEqual(['/foo/', '/foo']);
       |                                     ^
    132|  });
    133| });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/24]⎯

 FAIL  implementer.spec.ts > R3 · summaries are ordered by earliest page_view, then by path > orders pages by their earliest page_view time, earliest first
AssertionError: expected [ '/z', '/a' ] to deeply equal [ '/a', '/z' ]

- Expected
+ Received

  [
-   "/a",
    "/z",
+   "/a",
  ]

 ❯ implementer.spec.ts:144:37
    142|   const result = summarizeReading(events);
    143|
    144|   expect(result.map((s) => s.path)).toEqual(['/a', '/z']);
       |                                     ^
    145|  });
    146|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/24]⎯

 FAIL  implementer.spec.ts > R3 · summaries are ordered by earliest page_view, then by path > breaks a tie in earliest page_view time by comparing paths with <
AssertionError: expected [ '/b', '/a' ] to deeply equal [ '/a', '/b' ]

- Expected
+ Received

  [
-   "/a",
    "/b",
+   "/a",
  ]

 ❯ implementer.spec.ts:164:37
    162|   const result = summarizeReading(events);
    163|
    164|   expect(result.map((s) => s.path)).toEqual(['/a', '/b']);
       |                                     ^
    165|  });
    166| });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/24]⎯

 FAIL  implementer.spec.ts > R4 · views counts page_view events for the page > counts page_views for raw path spellings that normalize to the same page
AssertionError: expected [ Array(2) ] to have a length of 1 but got 2

- Expected
+ Received

- 1
+ 2

 ❯ implementer.spec.ts:198:18
    196|   const result = summarizeReading(events);
    197|
    198|   expect(result).toHaveLength(1);
       |                  ^
    199|   expect(result[0].views).toBe(2);
    200|  });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[7/24]⎯

 FAIL  implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > a durationMs that is negative contributes 0 to dwell
AssertionError: expected -500 to be +0 // Object.is equality

- Expected
+ Received

- 0
+ -500

 ❯ implementer.spec.ts:237:47
    235|   const events: ReadingEvent[] = [pageView('/a', T0), heartbeat('/a', …
    236|
    237|   expect(summarizeReading(events)[0].dwellMs).toBe(0);
       |                                               ^
    238|  });
    239|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[8/24]⎯

 FAIL  implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > a durationMs that is NaN contributes 0 to dwell
AssertionError: expected NaN to be +0 // Object.is equality

- Expected
+ Received

- 0
+ NaN

 ❯ implementer.spec.ts:237:47
    235|   const events: ReadingEvent[] = [pageView('/a', T0), heartbeat('/a', …
    236|
    237|   expect(summarizeReading(events)[0].dwellMs).toBe(0);
       |                                               ^
    238|  });
    239|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[9/24]⎯

 FAIL  implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > a durationMs that is Infinity contributes 0 to dwell
AssertionError: expected Infinity to be +0 // Object.is equality

- Expected
+ Received

- 0
+ Infinity

 ❯ implementer.spec.ts:237:47
    235|   const events: ReadingEvent[] = [pageView('/a', T0), heartbeat('/a', …
    236|
    237|   expect(summarizeReading(events)[0].dwellMs).toBe(0);
       |                                               ^
    238|  });
    239|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[10/24]⎯

 FAIL  implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > a durationMs that is -Infinity contributes 0 to dwell
AssertionError: expected -Infinity to be +0 // Object.is equality

- Expected
+ Received

- 0
+ -Infinity

 ❯ implementer.spec.ts:237:47
    235|   const events: ReadingEvent[] = [pageView('/a', T0), heartbeat('/a', …
    236|
    237|   expect(summarizeReading(events)[0].dwellMs).toBe(0);
       |                                               ^
    238|  });
    239|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[11/24]⎯

 FAIL  implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > caps a single event contribution at 30 minutes (1,800,000ms)
AssertionError: expected 2000000 to be 1800000 // Object.is equality

- Expected
+ Received

- 1800000
+ 2000000

 ❯ implementer.spec.ts:256:47
    254|   ];
    255|
    256|   expect(summarizeReading(events)[0].dwellMs).toBe(DWELL_CAP_MS);
       |                                               ^
    257|  });
    258|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[12/24]⎯

 FAIL  implementer.spec.ts > R5 · dwellMs sums durationMs from heartbeat and page_leave only > applies the 30-minute cap per event, not to the running total: two capped events sum past the cap
AssertionError: expected 4000000 to be 3600000 // Object.is equality

- Expected
+ Received

- 3600000
+ 4000000

 ❯ implementer.spec.ts:272:47
    270|   ];
    271|
    272|   expect(summarizeReading(events)[0].dwellMs).toBe(DWELL_CAP_MS * 2);
       |                                               ^
    273|  });
    274| });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[13/24]⎯

 FAIL  implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > a depth that is NaN is ignored, not treated as 0 or as a maximum
AssertionError: expected NaN to be 40 // Object.is equality

- Expected
+ Received

- 40
+ NaN

 ❯ implementer.spec.ts:319:45
    317|
    318|   // The invalid value must not win, and must not suppress the valid o…
    319|   expect(summarizeReading(events)[0].depth).toBe(40);
       |                                             ^
    320|  });
    321|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[14/24]⎯

 FAIL  implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > a depth that is Infinity is ignored, not treated as 0 or as a maximum
AssertionError: expected Infinity to be 40 // Object.is equality

- Expected
+ Received

- 40
+ Infinity

 ❯ implementer.spec.ts:319:45
    317|
    318|   // The invalid value must not win, and must not suppress the valid o…
    319|   expect(summarizeReading(events)[0].depth).toBe(40);
       |                                             ^
    320|  });
    321|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[15/24]⎯

 FAIL  implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > clamps a depth above 100 down to 100
AssertionError: expected 150 to be 100 // Object.is equality

- Expected
+ Received

- 100
+ 150

 ❯ implementer.spec.ts:324:45
    322|  it('clamps a depth above 100 down to 100', () => {
    323|   const events: ReadingEvent[] = [pageView('/a', T0), scroll('/a', T0 …
    324|   expect(summarizeReading(events)[0].depth).toBe(100);
       |                                             ^
    325|  });
    326|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[16/24]⎯

 FAIL  implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > rounds a fractional depth down to a whole number
AssertionError: expected 63.9 to be 63 // Object.is equality

- Expected
+ Received

- 63
+ 63.9

 ❯ implementer.spec.ts:334:45
    332|  it('rounds a fractional depth down to a whole number', () => {
    333|   const events: ReadingEvent[] = [pageView('/a', T0), scroll('/a', T0 …
    334|   expect(summarizeReading(events)[0].depth).toBe(63);
       |                                             ^
    335|  });
    336|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[17/24]⎯

 FAIL  implementer.spec.ts > R6 · depth is the greatest clamped, floored depth from scroll and page_leave > clamps and floors together for an out-of-range fractional value
AssertionError: expected 100.9 to be 100 // Object.is equality

- Expected
+ Received

- 100
+ 100.9

 ❯ implementer.spec.ts:339:45
    337|  it('clamps and floors together for an out-of-range fractional value',…
    338|   const events: ReadingEvent[] = [pageView('/a', T0), scroll('/a', T0 …
    339|   expect(summarizeReading(events)[0].depth).toBe(100);
       |                                             ^
    340|  });
    341| });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[18/24]⎯

 FAIL  implementer.spec.ts > R8 · engaged requires dwellMs >= 10,000 AND depth >= 50 > 'dwell just under its threshold' -> engaged is false
 FAIL  implementer.spec.ts > R8 · engaged requires dwellMs >= 10,000 AND depth >= 50 > 'ample dwell but shallow scroll' -> engaged is false
 FAIL  implementer.spec.ts > R8 · engaged requires dwellMs >= 10,000 AND depth >= 50 > 'deep scroll but almost no dwell' -> engaged is false
AssertionError: expected true to be false // Object.is equality

- Expected
+ Received

- false
+ true

 ❯ implementer.spec.ts:391:27
    389|   expect(summary.dwellMs).toBe(dwellMs);
    390|   expect(summary.depth).toBe(depth);
    391|   expect(summary.engaged).toBe(expected);
       |                           ^
    392|  });
    393|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[19/24]⎯

 FAIL  implementer.spec.ts > R8 · engaged requires dwellMs >= 10,000 AND depth >= 50 > is a conjunction, not a disjunction: satisfying only one condition is never enough
AssertionError: expected true to be false // Object.is equality

- Expected
+ Received

- false
+ true

 ❯ implementer.spec.ts:400:56
    398|   const result = summarizeReading([...dwellOnly, ...depthOnly]);
    399|
    400|   expect(result.find((s) => s.path === '/a')?.engaged).toBe(false);
       |                                                        ^
    401|   expect(result.find((s) => s.path === '/b')?.engaged).toBe(false);
    402|  });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[20/24]⎯

 FAIL  implementer.spec.ts > R9 · the result depends only on which events are present, never on their order > produces an identical result for a reversed event array
AssertionError: expected [ Array(2) ] to deeply equal [ Array(2) ]

- Expected
+ Received

  [
    {
-     "depth": 0,
-     "dwellMs": 7000,
-     "engaged": false,
-     "milestones": [],
-     "path": "/a",
-     "views": 1,
-   },
-   {
      "depth": 80,
      "dwellMs": 2000,
      "engaged": true,
      "milestones": [
        25,
        50,
        75,
      ],
      "path": "/b",
+     "views": 1,
+   },
+   {
+     "depth": 0,
+     "dwellMs": 7000,
+     "engaged": false,
+     "milestones": [],
+     "path": "/a",
      "views": 1,
    },
  ]

 ❯ implementer.spec.ts:423:20
    421|   const reversed = summarizeReading([...events].reverse());
    422|
    423|   expect(reversed).toEqual(forward);
       |                    ^
    424|  });
    425|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[21/24]⎯

 FAIL  implementer.spec.ts > R9 · the result depends only on which events are present, never on their order > produces an identical result for an arbitrary shuffle
AssertionError: expected [ Array(2) ] to deeply equal [ Array(2) ]

- Expected
+ Received

  [
    {
-     "depth": 0,
-     "dwellMs": 7000,
-     "engaged": false,
-     "milestones": [],
-     "path": "/a",
-     "views": 1,
-   },
-   {
      "depth": 80,
      "dwellMs": 2000,
      "engaged": true,
      "milestones": [
        25,
        50,
        75,
      ],
      "path": "/b",
+     "views": 1,
+   },
+   {
+     "depth": 0,
+     "dwellMs": 7000,
+     "engaged": false,
+     "milestones": [],
+     "path": "/a",
      "views": 1,
    },
  ]

 ❯ implementer.spec.ts:438:38
    436|   const shuffled = [events[3], events[0], events[5], events[1], events…
    437|
    438|   expect(summarizeReading(shuffled)).toEqual(summarizeReading(events));
       |                                      ^
    439|  });
    440| });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[22/24]⎯
$ echo $?
1
Where the agent’s reading and the product’s contract parted
RuleThe agent decidedThe contract says
R1Every path with any event gets a summary.Only pages with a page_view. The rest are ignored.
R2/about and /about/ are two pages.One trailing slash is removed first. They are one page.
R3Pages come out in the order they first appear.Earliest page_view first, ties by path.
R5Any duration adds up, including negative or hours long.Unusable durations count 0; one slice counts at most 30 minutes.
R6Depth is taken as reported: 140, 74.6, NaN.Clamped to 0–100, rounded down, non-finite ignored.
R8Engaged means 15 seconds or half the page.Engaged means 10 seconds and half the page.

The first agent’s code is not wrong about its ticket. Every decision in that table is defensible. It is wrong about a contract it never received, and its own tests could not know, because the same reader wrote them. That is the closed circle: not a bad agent, but a suite with no source of truth outside its author.

The ticket, the code, and its testsExactly as the agent wrote them
the ticket, as given
You're working in a SvelteKit + TypeScript repo at /Users/sj/Desktop/dev/builds/heyrian. Work only inside `src/lib/features/reading-summary/` (it is empty). Don't read or modify anything else in the repository or elsewhere on disk; everything you need is in this message.

Ticket SBC-101: Add `summarizeReading(events)` in `src/lib/features/reading-summary/reading.ts` for the analytics dashboard. It turns the browser tracker's raw events for one reader into one summary per page.

```ts
export type ReadingEventKind = 'page_view' | 'heartbeat' | 'scroll' | 'page_leave';

export interface ReadingEvent {
	kind: ReadingEventKind;
	/** The page's path, as the tracker saw it. */
	path: string;
	/** When the event happened, in milliseconds since the epoch. */
	at: number;
	/** heartbeat and page_leave: visible time since the previous slice, in milliseconds. */
	durationMs?: number;
	/** scroll and page_leave: how far down the page the reader has been, 0 to 100. */
	depth?: number;
}

export type Milestone = 25 | 50 | 75 | 100;

export interface PageSummary {
	path: string;
	views: number;
	dwellMs: number;
	depth: number;
	milestones: Milestone[];
	engaged: boolean;
}

export function summarizeReading(events: readonly ReadingEvent[]): PageSummary[];
```

`dwellMs` is how long the reader spent on the page, `depth` is how far they scrolled (0–100), `milestones` are the 25/50/75/100 marks they passed, and `engaged` means they actually read the page rather than bouncing.

Write vitest tests in `src/lib/features/reading-summary/reading.spec.ts` (import `describe`, `it`, `expect` from 'vitest'; no other packages) and make sure they pass. Run them from the repository root with:

```sh
npx vitest run --project server src/lib/features/reading-summary/
```

When you're done, reply with a short summary of the decisions you made, how many tests you wrote, and the final test output line.
reading.ts · instruction run
/**
 * SBC-101: turns the browser tracker's raw events for one reader into one
 * summary per page, for the analytics dashboard.
 */

export type ReadingEventKind = 'page_view' | 'heartbeat' | 'scroll' | 'page_leave';

export interface ReadingEvent {
	kind: ReadingEventKind;
	/** The page's path, as the tracker saw it. */
	path: string;
	/** When the event happened, in milliseconds since the epoch. */
	at: number;
	/** heartbeat and page_leave: visible time since the previous slice, in milliseconds. */
	durationMs?: number;
	/** scroll and page_leave: how far down the page the reader has been, 0 to 100. */
	depth?: number;
}

export type Milestone = 25 | 50 | 75 | 100;

export interface PageSummary {
	path: string;
	views: number;
	dwellMs: number;
	depth: number;
	milestones: Milestone[];
	engaged: boolean;
}

/** Depth marks we report, in ascending order. */
const ALL_MILESTONES: readonly Milestone[] = [25, 50, 75, 100];

/**
 * A page counts as "engaged" (actually read, not bounced) once the reader
 * has either stuck around for a meaningful stretch of visible time, or
 * scrolled past the halfway point. Either signal alone is enough - a long
 * idle tab with no scroll, or a fast skim to the bottom, both look like a
 * real read rather than a bounce.
 */
const ENGAGED_DWELL_MS = 15_000;
const ENGAGED_DEPTH = 50;

interface MutablePageSummary {
	path: string;
	views: number;
	dwellMs: number;
	depth: number;
}

/**
 * Groups a reader's raw events by page path and reduces each group to a
 * PageSummary. Pages are returned in the order their first event appears in
 * `events`; the order of events for a given path does not otherwise matter,
 * since every field is either a sum or a running max.
 */
export function summarizeReading(events: readonly ReadingEvent[]): PageSummary[] {
	const order: string[] = [];
	const byPath = new Map<string, MutablePageSummary>();

	for (const event of events) {
		let summary = byPath.get(event.path);
		if (!summary) {
			summary = { path: event.path, views: 0, dwellMs: 0, depth: 0 };
			byPath.set(event.path, summary);
			order.push(event.path);
		}

		switch (event.kind) {
			case 'page_view':
				summary.views += 1;
				break;
			case 'heartbeat':
				summary.dwellMs += event.durationMs ?? 0;
				break;
			case 'scroll':
				summary.depth = Math.max(summary.depth, event.depth ?? 0);
				break;
			case 'page_leave':
				summary.dwellMs += event.durationMs ?? 0;
				summary.depth = Math.max(summary.depth, event.depth ?? 0);
				break;
		}
	}

	return order.map((path) => {
		const summary = byPath.get(path)!;
		const milestones = ALL_MILESTONES.filter((milestone) => summary.depth >= milestone);
		const engaged = summary.dwellMs >= ENGAGED_DWELL_MS || summary.depth >= ENGAGED_DEPTH;

		return {
			path: summary.path,
			views: summary.views,
			dwellMs: summary.dwellMs,
			depth: summary.depth,
			milestones,
			engaged
		};
	});
}
reading.spec.ts · instruction run
import { describe, it, expect } from 'vitest';
import { summarizeReading, type ReadingEvent } from './reading';

describe('summarizeReading', () => {
	it('returns an empty array for no events', () => {
		expect(summarizeReading([])).toEqual([]);
	});

	it('summarizes a single well-read page', () => {
		const events: ReadingEvent[] = [
			{ kind: 'page_view', path: '/a', at: 0 },
			{ kind: 'heartbeat', path: '/a', at: 5_000, durationMs: 5_000 },
			{ kind: 'scroll', path: '/a', at: 6_000, depth: 40 },
			{ kind: 'heartbeat', path: '/a', at: 10_000, durationMs: 5_000 },
			{ kind: 'scroll', path: '/a', at: 12_000, depth: 80 },
			{ kind: 'page_leave', path: '/a', at: 15_000, durationMs: 5_000, depth: 80 }
		];

		expect(summarizeReading(events)).toEqual([
			{
				path: '/a',
				views: 1,
				dwellMs: 15_000,
				depth: 80,
				milestones: [25, 50, 75],
				engaged: true
			}
		]);
	});

	it('treats a bare page_view with nothing else as a bounce', () => {
		const events: ReadingEvent[] = [{ kind: 'page_view', path: '/bounce', at: 0 }];

		expect(summarizeReading(events)).toEqual([
			{
				path: '/bounce',
				views: 1,
				dwellMs: 0,
				depth: 0,
				milestones: [],
				engaged: false
			}
		]);
	});

	it('preserves first-appearance order across multiple pages', () => {
		const events: ReadingEvent[] = [
			{ kind: 'page_view', path: '/second', at: 0 },
			{ kind: 'page_view', path: '/first', at: 1 },
			{ kind: 'page_view', path: '/second', at: 2 }
		];

		const summaries = summarizeReading(events);
		expect(summaries.map((s) => s.path)).toEqual(['/second', '/first']);
	});

	it('counts repeat page_view events as separate views on the same page', () => {
		const events: ReadingEvent[] = [
			{ kind: 'page_view', path: '/a', at: 0 },
			{ kind: 'heartbeat', path: '/a', at: 1_000, durationMs: 1_000 },
			{ kind: 'page_view', path: '/a', at: 2_000 },
			{ kind: 'heartbeat', path: '/a', at: 3_000, durationMs: 1_000 }
		];

		const [summary] = summarizeReading(events);
		expect(summary.views).toBe(2);
		expect(summary.dwellMs).toBe(2_000);
	});

	it('is engaged from dwell time alone, even with no scroll', () => {
		const events: ReadingEvent[] = [
			{ kind: 'page_view', path: '/a', at: 0 },
			{ kind: 'heartbeat', path: '/a', at: 20_000, durationMs: 20_000 }
		];

		const [summary] = summarizeReading(events);
		expect(summary.depth).toBe(0);
		expect(summary.milestones).toEqual([]);
		expect(summary.engaged).toBe(true);
	});

	it('is engaged from scroll depth alone, even with a short visit', () => {
		const events: ReadingEvent[] = [
			{ kind: 'page_view', path: '/a', at: 0 },
			{ kind: 'heartbeat', path: '/a', at: 500, durationMs: 500 },
			{ kind: 'scroll', path: '/a', at: 500, depth: 100 }
		];

		const [summary] = summarizeReading(events);
		expect(summary.dwellMs).toBe(500);
		expect(summary.milestones).toEqual([25, 50, 75, 100]);
		expect(summary.engaged).toBe(true);
	});

	it('is not engaged when both dwell time and depth are low', () => {
		const events: ReadingEvent[] = [
			{ kind: 'page_view', path: '/a', at: 0 },
			{ kind: 'heartbeat', path: '/a', at: 1_000, durationMs: 1_000 },
			{ kind: 'scroll', path: '/a', at: 1_000, depth: 20 }
		];

		const [summary] = summarizeReading(events);
		expect(summary.engaged).toBe(false);
	});

	it('only reports milestones the reader actually passed', () => {
		const events: ReadingEvent[] = [
			{ kind: 'page_view', path: '/a', at: 0 },
			{ kind: 'scroll', path: '/a', at: 1_000, depth: 60 }
		];

		const [summary] = summarizeReading(events);
		expect(summary.depth).toBe(60);
		expect(summary.milestones).toEqual([25, 50]);
	});

	it('tracks depth as a running maximum, ignoring later drops', () => {
		const events: ReadingEvent[] = [
			{ kind: 'page_view', path: '/a', at: 0 },
			{ kind: 'scroll', path: '/a', at: 1_000, depth: 90 },
			{ kind: 'scroll', path: '/a', at: 2_000, depth: 30 }
		];

		const [summary] = summarizeReading(events);
		expect(summary.depth).toBe(90);
		expect(summary.milestones).toEqual([25, 50, 75]);
	});

	it('folds page_leave duration and depth into the running totals', () => {
		const events: ReadingEvent[] = [
			{ kind: 'page_view', path: '/a', at: 0 },
			{ kind: 'heartbeat', path: '/a', at: 1_000, durationMs: 1_000 },
			{ kind: 'page_leave', path: '/a', at: 2_000, durationMs: 1_000, depth: 55 }
		];

		const [summary] = summarizeReading(events);
		expect(summary.dwellMs).toBe(2_000);
		expect(summary.depth).toBe(55);
		expect(summary.milestones).toEqual([25, 50]);
		expect(summary.engaged).toBe(true);
	});

	it('keeps separate pages fully independent of each other', () => {
		const events: ReadingEvent[] = [
			{ kind: 'page_view', path: '/a', at: 0 },
			{ kind: 'heartbeat', path: '/a', at: 20_000, durationMs: 20_000 },
			{ kind: 'scroll', path: '/a', at: 20_000, depth: 100 },
			{ kind: 'page_view', path: '/b', at: 21_000 }
		];

		const summaries = summarizeReading(events);
		const a = summaries.find((s) => s.path === '/a')!;
		const b = summaries.find((s) => s.path === '/b')!;

		expect(a.engaged).toBe(true);
		expect(a.depth).toBe(100);
		expect(b.engaged).toBe(false);
		expect(b.depth).toBe(0);
		expect(b.dwellMs).toBe(0);
	});
});
04 / Score the suites

A first pass means nothing. Break the code and count.

A second agent implemented the same ticket with the contract. Unasked, it found a spec-testing skill installed here and delegated its tests to a write-only writer, the one that produced the second blind suite above. Its report of its own mutation score is set aside, because an author grading itself is what this lesson is about.

Against a stub that throws

64 of 64 fail

Red-first: proves every test reaches the function, not that any can tell right from wrong.

Every failure mentions not implemented.

Full output835 lines, as recorded
the blind suite against a stub that throws · 12 Sep 2026
$ vitest run   # red-first: the blind suite against the stub, whose body throws

 RUN  v4.1.11 <run>

 × barrier.spec.ts > R11 - nothing to read > an empty array of events produces an empty array of summaries 3ms
   → not implemented
 × barrier.spec.ts > R1 - a page only exists in the output if it was viewed > a page with only heartbeat/scroll/page_leave events, never a page_view, produces no summary 0ms
   → not implemented
 × barrier.spec.ts > R1 - a page only exists in the output if it was viewed > an unviewed page's events do not leak into a different, viewed page's summary 0ms
   → not implemented
 × barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > a trailing slash does not create a second page 0ms
   → not implemented
 × barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > the root path / is left exactly as it is 0ms
   → not implemented
 × barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > // normalizes to / by removing its single trailing slash, colliding with the root page 0ms
   → not implemented
 × barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > a path with two trailing slashes only has one removed, so it differs from the single-trailing-slash form 0ms
   → not implemented
 × barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > case is never normalized: /Blog and /blog are different pages 0ms
   → not implemented
 × barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > an internal double slash is never collapsed, only a trailing slash is touched 0ms
   → not implemented
 × barrier.spec.ts > R3 - summaries are ordered by earliest page_view time, then by path > the page viewed first comes first 0ms
   → not implemented
 × barrier.spec.ts > R3 - summaries are ordered by earliest page_view time, then by path > a page's earliest page_view is its minimum across all its page_views, regardless of array position 0ms
   → not implemented
 × barrier.spec.ts > R3 - summaries are ordered by earliest page_view time, then by path > ties on earliest page_view time break on path using plain <, not a locale-aware compare 0ms
   → not implemented
 × barrier.spec.ts > R4 - views counts page_view events for the page > duplicate page_view events are each counted, not deduplicated 0ms
   → not implemented
 × barrier.spec.ts > R4 - views counts page_view events for the page > non-page_view events never add to views 0ms
   → not implemented
 × barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > heartbeat and page_leave durations are added together 0ms
   → not implemented
 × barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > durationMs on any other kind of event is ignored 0ms
   → not implemented
 × barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a page_view carrying durationMs contributes nothing to dwell 0ms
   → not implemented
 × barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a negative durationMs contributes 0, it does not subtract from other events' dwell 0ms
   → not implemented
 × barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > an absent durationMs contributes 0 0ms
   → not implemented
 × barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a non-finite durationMs (NaN) contributes 0 0ms
   → not implemented
 × barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > an infinite durationMs contributes 0, distinct from the cap that applies to large finite values 0ms
   → not implemented
 × barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a single event contributes at most 1,800,000ms; a longer finite value is capped exactly there 1ms
   → not implemented
 × barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a duration exactly at the cap is not reduced further 0ms
   → not implemented
 × barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > the cap applies per event, not to the running total 0ms
   → not implemented
 × barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > timestamps never contribute to dwell: a gap with no durations dwells for 0ms 0ms
   → not implemented
 × barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > the maximum among scroll and page_leave depths wins, not the latest one 0ms
   → not implemented
 × barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > depth on any other kind of event is ignored 0ms
   → not implemented
 × barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a depth above 100 is clamped to 100, not ignored 0ms
   → not implemented
 × barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a negative depth clamps to 0 0ms
   → not implemented
 × barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a fractional depth is rounded down, not to the nearest whole number 0ms
   → not implemented
 × barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > an absent depth is ignored 0ms
   → not implemented
 × barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a non-finite depth (NaN) is ignored, it does not win the max over a real value 0ms
   → not implemented
 × barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a page with no usable depth data defaults to 0 0ms
   → not implemented
 × barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 0, milestones are [] 0ms
   → not implemented
 × barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 10, milestones are [] 0ms
   → not implemented
 × barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 24, milestones are [] 0ms
   → not implemented
 × barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 25, milestones are [25] 0ms
   → not implemented
 × barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 49, milestones are [25] 0ms
   → not implemented
 × barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 50, milestones are [25,50] 0ms
   → not implemented
 × barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 74, milestones are [25,50] 0ms
   → not implemented
 × barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 75, milestones are [25,50,75] 0ms
   → not implemented
 × barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 99, milestones are [25,50,75] 0ms
   → not implemented
 × barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 100, milestones are [25,50,75,100] 0ms
   → not implemented
 × barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > a depth that clamps to 100 still yields every milestone 0ms
   → not implemented
 × barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=10000, depth=50 -> engaged=true (meets both minimums exactly) 0ms
   → not implemented
 × barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=9999, depth=100 -> engaged=false (one millisecond short of the dwell minimum, however deep) 0ms
   → not implemented
 × barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=100000, depth=49 -> engaged=false (one point short of the depth minimum, however long) 0ms
   → not implemented
 × barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=0, depth=0 -> engaged=false (no engagement signal at all) 0ms
   → not implemented
 × barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=50000, depth=100 -> engaged=true (comfortably exceeds both minimums) 0ms
   → not implemented
 × barrier.spec.ts > every event kind affects only the fields the contract assigns to it > page_view affects only views and ordering, never dwell or depth 1ms
   → not implemented
 × barrier.spec.ts > every event kind affects only the fields the contract assigns to it > heartbeat affects only dwell, never views or depth 0ms
   → not implemented
 × barrier.spec.ts > every event kind affects only the fields the contract assigns to it > scroll affects only depth, never views or dwell 0ms
   → not implemented
 × barrier.spec.ts > every event kind affects only the fields the contract assigns to it > page_leave affects both dwell and depth, never views 0ms
   → not implemented
 × barrier.spec.ts > fail-closed on data that does not match the compile-time shape (inference, see comment above) > a string where durationMs should be contributes 0; it is not parsed or coerced 1ms
   → expected [Function] to not throw an error but 'Error: not implemented' was thrown
 × barrier.spec.ts > fail-closed on data that does not match the compile-time shape (inference, see comment above) > null where depth should be is ignored; it does not throw and does not win a comparison it should lose 0ms
   → expected [Function] to not throw an error but 'Error: not implemented' was thrown
 × barrier.spec.ts > R9 - the result depends only on which events are present, never their order > a small hand-picked reordering produces an identical result 0ms
   → not implemented
 × barrier.spec.ts > R9 - the result depends only on which events are present, never their order > property: random shuffles of the same events match a reference model built directly from the contract's prose 0ms
   → not implemented
 × barrier.spec.ts > R10 - purity: the input is never modified, and the same input always yields an equal result > the events array and its elements are the same references after the call 0ms
   → not implemented
 × barrier.spec.ts > R10 - purity: the input is never modified, and the same input always yields an equal result > a deep-frozen input does not throw, proving no mutation is attempted 0ms
   → a pure function must not need to write to its input: expected [Function] to not throw an error but 'Error: not implemented' was thrown
 × barrier.spec.ts > R10 - purity: the input is never modified, and the same input always yields an equal result > calling summarizeReading twice on the same input yields an equal result both times 0ms
   → not implemented
 × barrier.spec.ts > property: adding a dwell-bearing event never decreases a page's dwellMs > dwellMs is monotone under any additional heartbeat/page_leave event, valid or not 0ms
   → not implemented
 × barrier.spec.ts > property: adding a depth-bearing event never decreases a page's depth > depth is monotone under any additional scroll/page_leave event, valid or not 0ms
   → not implemented
 × barrier.spec.ts > worked example: a full pipeline from raw events to one summary > views, dwell, depth, milestones and engaged are all derived consistently from the same events 0ms
   → not implemented
 × barrier.spec.ts > a lone page_view with no other signal starts from a clean baseline > views=1, dwellMs=0, depth=0, milestones=[], engaged=false 0ms
   → not implemented

 Test Files  1 failed (1)
      Tests  64 failed (64)


⎯⎯⎯⎯⎯⎯ Failed Tests 64 ⎯⎯⎯⎯⎯⎯⎯

 FAIL  barrier.spec.ts > R11 - nothing to read > an empty array of events produces an empty array of summaries
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:241:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/64]⎯

 FAIL  barrier.spec.ts > R1 - a page only exists in the output if it was viewed > a page with only heartbeat/scroll/page_leave events, never a page_view, produces no summary
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:253:4

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/64]⎯

 FAIL  barrier.spec.ts > R1 - a page only exists in the output if it was viewed > an unviewed page's events do not leak into a different, viewed page's summary
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:260:18

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/64]⎯

 FAIL  barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > a trailing slash does not create a second page
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:277:18

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/64]⎯

 FAIL  barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > the root path / is left exactly as it is
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:284:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/64]⎯

 FAIL  barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > // normalizes to / by removing its single trailing slash, colliding with the root page
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:292:18

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/64]⎯

 FAIL  barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > a path with two trailing slashes only has one removed, so it differs from the single-trailing-slash form
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:304:18

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[7/64]⎯

 FAIL  barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > case is never normalized: /Blog and /blog are different pages
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:314:4

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[8/64]⎯

 FAIL  barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > an internal double slash is never collapsed, only a trailing slash is touched
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:323:18

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[9/64]⎯

 FAIL  barrier.spec.ts > R3 - summaries are ordered by earliest page_view time, then by path > the page viewed first comes first
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:339:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[10/64]⎯

 FAIL  barrier.spec.ts > R3 - summaries are ordered by earliest page_view time, then by path > a page's earliest page_view is its minimum across all its page_views, regardless of array position
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:345:4

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[11/64]⎯

 FAIL  barrier.spec.ts > R3 - summaries are ordered by earliest page_view time, then by path > ties on earliest page_view time break on path using plain <, not a locale-aware compare
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:356:4

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[12/64]⎯

 FAIL  barrier.spec.ts > R4 - views counts page_view events for the page > duplicate page_view events are each counted, not deduplicated
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:369:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[13/64]⎯

 FAIL  barrier.spec.ts > R4 - views counts page_view events for the page > non-page_view events never add to views
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:374:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[14/64]⎯

 FAIL  barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > heartbeat and page_leave durations are added together
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:385:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[15/64]⎯

 FAIL  barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > durationMs on any other kind of event is ignored
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:391:4

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[16/64]⎯

 FAIL  barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a page_view carrying durationMs contributes nothing to dwell
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:398:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[17/64]⎯

 FAIL  barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a negative durationMs contributes 0, it does not subtract from other events' dwell
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:404:4

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[18/64]⎯

 FAIL  barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > an absent durationMs contributes 0
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:411:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[19/64]⎯

 FAIL  barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a non-finite durationMs (NaN) contributes 0
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:416:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[20/64]⎯

 FAIL  barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > an infinite durationMs contributes 0, distinct from the cap that applies to large finite values
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:422:4

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[21/64]⎯

 FAIL  barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a single event contributes at most 1,800,000ms; a longer finite value is capped exactly there
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:429:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[22/64]⎯

 FAIL  barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a duration exactly at the cap is not reduced further
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:436:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[23/64]⎯

 FAIL  barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > the cap applies per event, not to the running total
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:442:4

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[24/64]⎯

 FAIL  barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > timestamps never contribute to dwell: a gap with no durations dwells for 0ms
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:450:4

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[25/64]⎯

 FAIL  barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > the maximum among scroll and page_leave depths wins, not the latest one
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:463:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[26/64]⎯

 FAIL  barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > depth on any other kind of event is ignored
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:468:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[27/64]⎯

 FAIL  barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a depth above 100 is clamped to 100, not ignored
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:475:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[28/64]⎯

 FAIL  barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a negative depth clamps to 0
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:482:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[29/64]⎯

 FAIL  barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a fractional depth is rounded down, not to the nearest whole number
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:487:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[30/64]⎯

 FAIL  barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > an absent depth is ignored
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:492:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[31/64]⎯

 FAIL  barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a non-finite depth (NaN) is ignored, it does not win the max over a real value
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:497:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[32/64]⎯

 FAIL  barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a page with no usable depth data defaults to 0
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:502:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[33/64]⎯

 FAIL  barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 0, milestones are []
 FAIL  barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 10, milestones are []
 FAIL  barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 24, milestones are []
 FAIL  barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 25, milestones are [25]
 FAIL  barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 49, milestones are [25]
 FAIL  barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 50, milestones are [25,50]
 FAIL  barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 74, milestones are [25,50]
 FAIL  barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 75, milestones are [25,50,75]
 FAIL  barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 99, milestones are [25,50,75]
 FAIL  barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 100, milestones are [25,50,75,100]
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:524:19

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[34/64]⎯

 FAIL  barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > a depth that clamps to 100 still yields every milestone
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:534:10

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[35/64]⎯

 FAIL  barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=10000, depth=50 -> engaged=true (meets both minimums exactly)
 FAIL  barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=9999, depth=100 -> engaged=false (one millisecond short of the dwell minimum, however deep)
 FAIL  barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=100000, depth=49 -> engaged=false (one point short of the depth minimum, however long)
 FAIL  barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=0, depth=0 -> engaged=false (no engagement signal at all)
 FAIL  barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=50000, depth=100 -> engaged=true (comfortably exceeds both minimums)
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:552:4

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[36/64]⎯

 FAIL  barrier.spec.ts > every event kind affects only the fields the contract assigns to it > page_view affects only views and ordering, never dwell or depth
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:565:18

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[37/64]⎯

 FAIL  barrier.spec.ts > every event kind affects only the fields the contract assigns to it > heartbeat affects only dwell, never views or depth
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:577:18

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[38/64]⎯

 FAIL  barrier.spec.ts > every event kind affects only the fields the contract assigns to it > scroll affects only depth, never views or dwell
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:589:18

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[39/64]⎯

 FAIL  barrier.spec.ts > every event kind affects only the fields the contract assigns to it > page_leave affects both dwell and depth, never views
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:601:18

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[40/64]⎯

 FAIL  barrier.spec.ts > fail-closed on data that does not match the compile-time shape (inference, see comment above) > a string where durationMs should be contributes 0; it is not parsed or coerced
AssertionError: expected [Function] to not throw an error but 'Error: not implemented' was thrown

- Expected:
undefined

+ Received:
"Error: not implemented"

 ❯ barrier.spec.ts:623:46
    621|  it('a string where durationMs should be contributes 0; it is not pars…
    622|   const events = [pv('/p', 0), mkEvent({ kind: 'heartbeat', path: '/p'…
    623|   expect(() => summarizeReading(events)).not.toThrow();
       |                                              ^
    624|   expect(summarizeReading(events)[0].dwellMs, 'a non-number durationMs…
    625|  });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[41/64]⎯

 FAIL  barrier.spec.ts > fail-closed on data that does not match the compile-time shape (inference, see comment above) > null where depth should be is ignored; it does not throw and does not win a comparison it should lose
AssertionError: expected [Function] to not throw an error but 'Error: not implemented' was thrown

- Expected:
undefined

+ Received:
"Error: not implemented"

 ❯ barrier.spec.ts:629:46
    627|  it('null where depth should be is ignored; it does not throw and does…
    628|   const events = [pv('/p', 0), sc('/p', 1, 40), mkEvent({ kind: 'scrol…
    629|   expect(() => summarizeReading(events)).not.toThrow();
       |                                              ^
    630|   expect(summarizeReading(events)[0].depth).toBe(40);
    631|  });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[42/64]⎯

 FAIL  barrier.spec.ts > R9 - the result depends only on which events are present, never their order > a small hand-picked reordering produces an identical result
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:643:4

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[43/64]⎯

 FAIL  barrier.spec.ts > R9 - the result depends only on which events are present, never their order > property: random shuffles of the same events match a reference model built directly from the contract's prose
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:653:26

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[44/64]⎯

 FAIL  barrier.spec.ts > R10 - purity: the input is never modified, and the same input always yields an equal result > the events array and its elements are the same references after the call
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:674:3

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[45/64]⎯

 FAIL  barrier.spec.ts > R10 - purity: the input is never modified, and the same input always yields an equal result > a deep-frozen input does not throw, proving no mutation is attempted
AssertionError: a pure function must not need to write to its input: expected [Function] to not throw an error but 'Error: not implemented' was thrown

- Expected:
undefined

+ Received:
"Error: not implemented"

 ❯ barrier.spec.ts:686:101
    684|   events.forEach(deepFreeze);
    685|   deepFreeze(events);
    686|   expect(() => summarizeReading(events), 'a pure function must not nee…
       |                                                                                                     ^
    687|  });
    688|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[46/64]⎯

 FAIL  barrier.spec.ts > R10 - purity: the input is never modified, and the same input always yields an equal result > calling summarizeReading twice on the same input yields an equal result both times
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:691:17

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[47/64]⎯

 FAIL  barrier.spec.ts > property: adding a dwell-bearing event never decreases a page's dwellMs > dwellMs is monotone under any additional heartbeat/page_leave event, valid or not
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:709:30

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[48/64]⎯

 FAIL  barrier.spec.ts > property: adding a depth-bearing event never decreases a page's depth > depth is monotone under any additional scroll/page_leave event, valid or not
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:732:30

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[49/64]⎯

 FAIL  barrier.spec.ts > worked example: a full pipeline from raw events to one summary > views, dwell, depth, milestones and engaged are all derived consistently from the same events
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:755:21

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[50/64]⎯

 FAIL  barrier.spec.ts > a lone page_view with no other signal starts from a clean baseline > views=1, dwellMs=0, depth=0, milestones=[], engaged=false
Error: not implemented
 ❯ summarizeReading reading.ts:30:8
     28|
     29| export function summarizeReading(_events: readonly ReadingEvent[]): Pa…
     30|  throw new Error('not implemented');
       |        ^
     31| }
     32|
 ❯ barrier.spec.ts:767:19

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[51/64]⎯
$ echo $?
1
Against the contract run’s code

64 of 64 pass

A first pass is consistent with correct code and with a suite that asserts almost nothing.

Full output74 lines, as recorded
the blind suite against the contract run · 12 Sep 2026
$ vitest run   # first run: the blind suite against the contract run’s code, before any triage

 RUN  v4.1.11 <run>

 ✓ barrier.spec.ts > R11 - nothing to read > an empty array of events produces an empty array of summaries 1ms
 ✓ barrier.spec.ts > R1 - a page only exists in the output if it was viewed > a page with only heartbeat/scroll/page_leave events, never a page_view, produces no summary 0ms
 ✓ barrier.spec.ts > R1 - a page only exists in the output if it was viewed > an unviewed page's events do not leak into a different, viewed page's summary 0ms
 ✓ barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > a trailing slash does not create a second page 0ms
 ✓ barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > the root path / is left exactly as it is 0ms
 ✓ barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > // normalizes to / by removing its single trailing slash, colliding with the root page 0ms
 ✓ barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > a path with two trailing slashes only has one removed, so it differs from the single-trailing-slash form 0ms
 ✓ barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > case is never normalized: /Blog and /blog are different pages 0ms
 ✓ barrier.spec.ts > R2 - two paths name the same page after removing one trailing slash > an internal double slash is never collapsed, only a trailing slash is touched 0ms
 ✓ barrier.spec.ts > R3 - summaries are ordered by earliest page_view time, then by path > the page viewed first comes first 0ms
 ✓ barrier.spec.ts > R3 - summaries are ordered by earliest page_view time, then by path > a page's earliest page_view is its minimum across all its page_views, regardless of array position 0ms
 ✓ barrier.spec.ts > R3 - summaries are ordered by earliest page_view time, then by path > ties on earliest page_view time break on path using plain <, not a locale-aware compare 0ms
 ✓ barrier.spec.ts > R4 - views counts page_view events for the page > duplicate page_view events are each counted, not deduplicated 0ms
 ✓ barrier.spec.ts > R4 - views counts page_view events for the page > non-page_view events never add to views 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > heartbeat and page_leave durations are added together 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > durationMs on any other kind of event is ignored 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a page_view carrying durationMs contributes nothing to dwell 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a negative durationMs contributes 0, it does not subtract from other events' dwell 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > an absent durationMs contributes 0 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a non-finite durationMs (NaN) contributes 0 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > an infinite durationMs contributes 0, distinct from the cap that applies to large finite values 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a single event contributes at most 1,800,000ms; a longer finite value is capped exactly there 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > a duration exactly at the cap is not reduced further 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > the cap applies per event, not to the running total 0ms
 ✓ barrier.spec.ts > R5 - dwellMs sums durationMs from heartbeat and page_leave only > timestamps never contribute to dwell: a gap with no durations dwells for 0ms 0ms
 ✓ barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > the maximum among scroll and page_leave depths wins, not the latest one 0ms
 ✓ barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > depth on any other kind of event is ignored 0ms
 ✓ barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a depth above 100 is clamped to 100, not ignored 0ms
 ✓ barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a negative depth clamps to 0 0ms
 ✓ barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a fractional depth is rounded down, not to the nearest whole number 0ms
 ✓ barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > an absent depth is ignored 0ms
 ✓ barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a non-finite depth (NaN) is ignored, it does not win the max over a real value 0ms
 ✓ barrier.spec.ts > R6 - depth is the greatest usable depth from scroll and page_leave > a page with no usable depth data defaults to 0 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 0, milestones are [] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 10, milestones are [] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 24, milestones are [] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 25, milestones are [25] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 49, milestones are [25] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 50, milestones are [25,50] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 74, milestones are [25,50] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 75, milestones are [25,50,75] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 99, milestones are [25,50,75] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > at depth 100, milestones are [25,50,75,100] 0ms
 ✓ barrier.spec.ts > R7 - milestones list every threshold at or below depth, ascending > a depth that clamps to 100 still yields every milestone 0ms
 ✓ barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=10000, depth=50 -> engaged=true (meets both minimums exactly) 0ms
 ✓ barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=9999, depth=100 -> engaged=false (one millisecond short of the dwell minimum, however deep) 0ms
 ✓ barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=100000, depth=49 -> engaged=false (one point short of the depth minimum, however long) 0ms
 ✓ barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=0, depth=0 -> engaged=false (no engagement signal at all) 0ms
 ✓ barrier.spec.ts > R8 - engaged requires both dwellMs >= 10,000 and depth >= 50 > dwellMs=50000, depth=100 -> engaged=true (comfortably exceeds both minimums) 0ms
 ✓ barrier.spec.ts > every event kind affects only the fields the contract assigns to it > page_view affects only views and ordering, never dwell or depth 0ms
 ✓ barrier.spec.ts > every event kind affects only the fields the contract assigns to it > heartbeat affects only dwell, never views or depth 0ms
 ✓ barrier.spec.ts > every event kind affects only the fields the contract assigns to it > scroll affects only depth, never views or dwell 0ms
 ✓ barrier.spec.ts > every event kind affects only the fields the contract assigns to it > page_leave affects both dwell and depth, never views 0ms
 ✓ barrier.spec.ts > fail-closed on data that does not match the compile-time shape (inference, see comment above) > a string where durationMs should be contributes 0; it is not parsed or coerced 0ms
 ✓ barrier.spec.ts > fail-closed on data that does not match the compile-time shape (inference, see comment above) > null where depth should be is ignored; it does not throw and does not win a comparison it should lose 0ms
 ✓ barrier.spec.ts > R9 - the result depends only on which events are present, never their order > a small hand-picked reordering produces an identical result 0ms
 ✓ barrier.spec.ts > R9 - the result depends only on which events are present, never their order > property: random shuffles of the same events match a reference model built directly from the contract's prose 3ms
 ✓ barrier.spec.ts > R10 - purity: the input is never modified, and the same input always yields an equal result > the events array and its elements are the same references after the call 0ms
 ✓ barrier.spec.ts > R10 - purity: the input is never modified, and the same input always yields an equal result > a deep-frozen input does not throw, proving no mutation is attempted 0ms
 ✓ barrier.spec.ts > R10 - purity: the input is never modified, and the same input always yields an equal result > calling summarizeReading twice on the same input yields an equal result both times 0ms
 ✓ barrier.spec.ts > property: adding a dwell-bearing event never decreases a page's dwellMs > dwellMs is monotone under any additional heartbeat/page_leave event, valid or not 0ms
 ✓ barrier.spec.ts > property: adding a depth-bearing event never decreases a page's depth > depth is monotone under any additional scroll/page_leave event, valid or not 0ms
 ✓ barrier.spec.ts > worked example: a full pipeline from raw events to one summary > views, dwell, depth, milestones and engaged are all derived consistently from the same events 0ms
 ✓ barrier.spec.ts > a lone page_view with no other signal starts from a clean baseline > views=1, dwellMs=0, depth=0, milestones=[], engaged=false 0ms

 Test Files  1 passed (1)
      Tests  64 passed (64)
$ echo $?
0

To tell those apart, a third agent read that code without the contract and wrote its own suite: option B. That code’s comments cite and restate several contract rules, so option B saw part of the contract second-hand. Then 24 mutants, each breaking one rule, nine of them putting back a decision the first agent really made. A probe first confirmed 24 of 24 change the output or the input on 407 inputs, so no survivor can be dismissed as harmless.

  • 21/24From the codeAn agent read the implementation and wrote the tests.
  • 24/24BlindA write-only agent derived the tests from the contract.
  • 23/24Second blindAnother write-only agent, same contract, never saw the first.
Recorded mutation run

Break the code on purpose. See which suite notices.

Recorded 12 September 2026. Each suite unchanged from its writer, run against the contract run’s code with one rule broken at a time, in a fresh copy per mutant.

Controls held. The unmutated code was green in every suite, and code that throws on import was red in every suite. Only then are the ratios below worth reading.
  • From the code21/24killed
  • Blind24/24killed
  • Second blind23/24killed
  • Any suite24/24killed
Show
Each row changes one rule in the code. Each cell is one suite’s verdict.
MutantFrom the codeBlindSecond blindAny suite
KilledKilledKilledKilled
KilledKilledKilledKilled
SurvivedKilledKilledKilled
KilledKilledKilledKilled
KilledKilledKilledKilled
KilledKilledKilledKilled
KilledKilledKilledKilled
KilledKilledKilledKilled
KilledKilledKilledKilled
KilledKilledKilledKilled
KilledKilledKilledKilled
KilledKilledKilledKilled
SurvivedKilledKilledKilled
KilledKilledKilledKilled
KilledKilledKilledKilled
KilledKilledKilledKilled
KilledKilledSurvivedKilled
KilledKilledKilledKilled
KilledKilledKilledKilled
KilledKilledKilledKilled
KilledKilledKilledKilled
KilledKilledKilledKilled
SurvivedKilledKilledKilled
KilledKilledKilledKilled

Select a mutant to see the change and the tests that caught it.

A replay of one recorded run: tests do not run in your browser. Each suite is the file an agent wrote, run unchanged in a fresh copy of the code with exactly one rule broken.

From the code · R2

Every trailing slash is stripped

Its test feeds //, which becomes / whether one slash or all are stripped, so the name promises more than the input can check.

From the code · R5

page_view durations count

Its test is named for page_view and scroll events, and only builds a scroll event.

From the code · R10

The input events are rewritten

It has no purity test. The code keeps that promise by not doing something, so there was no line to read.

Second blind · R6

Heartbeat depth counts

Its heartbeat helper cannot carry a depth, a gap its writer reported before the run.

The blind suite and its predictionWritten with one tool: Write

One of its kills, an infinite depth treated as usable, came only from a property test that compares shuffled inputs against a model written from the contract. Its writer also predicted two risks before anything ran; neither failed on this code, and both mutants that encode them were killed.

the writer’s report, recorded before anything ran
Test suite written to:
`/private/tmp/claude-501/-Users-sj-Desktop-dev-builds-heyrian/34b1d195-a224-45c1-9e2c-97702c7842f9/scratchpad/sbc/barrier/reading.barrier.spec.ts`

## Counts

- **Property tests (3):** model-equivalence + shuffle-invariance across 25 random trials (R9, backed by a from-scratch reference model transcribed from the prose, not the implementation); dwell-monotonicity under any additional heartbeat/page_leave event (20 trials); depth-monotonicity under any additional scroll/page_leave event (20 trials).
- **Contract tests (~59):** one block per rule (R1–R8, R10, R11), a "kind isolation" group making the four "on any other kind, X is ignored" prose statements total, and two clearly-marked fail-closed tests for runtime type violations.
- **Example tests (2):** one worked full-pipeline example, one "lone page_view" baseline — both fully derivable from the rules, included only as concrete illustrations for a human reader, not as guessed values.

## Silences / ambiguities and what I did

1. **Empty string path (`""`) or paths not starting with `/`** — R2's algorithm doesn't say what happens outside the `/…` shape the tracker is assumed to send. Left untested rather than guessing.
2. **Trailing-slash removal is single-pass, not iterative** — the word "single" makes this explicit, not truly ambiguous, but it's the kind of detail a regex-happy implementation gets wrong (stripping *all* trailing slashes). I wrote a discriminating test (`/blog//` vs `/blog/` must stay distinct) and flagged in-line why it exists.
3. **Negative depth: "clamped to 0" vs "ignored"** — behaviorally indistinguishable when it's the only depth signal (both yield 0), so I didn't assert a mechanism there. I instead tested the direction that *is* observable: depth > 100 must clamp to 100, not fall back to the ignored-default of 0.
4. **Runtime values of the wrong type** (a string or `null` where a number is typed) — the contract's "not a finite number" is written against the declared `number` type, so treating a string as "not a finite number" is my inference, not a literal contract statement. I isolated these two tests into their own describe block, marked inline as inference, and deliberately kept string/null out of the shared random-fuzz pools so a failure there is never confused with a failure of the core aggregation logic.

## Predicted failures

- **Real risk:** "a path with two trailing slashes only has one removed" (`/blog//` vs `/blog/`). A very natural but non-compliant implementation strips all trailing slashes via something like `/\/+$/`, which would collapse these into the same page and fail this test.
- **Real risk:** "an infinite durationMs contributes 0, distinct from the cap." A plain `Math.min(Math.max(d,0),cap)` clamp without an explicit finiteness check turns `Infinity` into the 1,800,000 cap instead of 0 — a very plausible way to write the cap logic.
- **Moderate risk:** the two fail-closed runtime-type tests (string/null in numeric fields) — genuinely uncertain since it's my inference beyond the declared type; I'd call this close to a coin flip.
- **Low risk:** the locale-vs-code-unit tie-break test (`/Cherry` before `/banana`) — `Array.prototype.sort()` without a comparator already does code-unit comparison by default, so only a deliberate `localeCompare` would break this.
- **None expected** elsewhere: R1, R4, R7, R8, R11, purity, and the monotonicity/order-independence properties test very literal, unambiguous statements.

## Could not test

Nothing in the contract's stated promises was untestable from the outside — R10's "never modified" is covered by both reference-identity checks and deep-freeze-doesn't-throw; "same input always equal result" is covered by a direct repeat-call check. The only gaps are the two genuine silences above (empty-string/non-absolute paths), which I left out rather than encode a guess.
reading.barrier.spec.ts · as written
import { describe, it, expect } from 'vitest';
import { summarizeReading, type ReadingEvent, type PageSummary, type Milestone } from './reading';

// ---------------------------------------------------------------------------
// Deterministic PRNG (mulberry32) so property-test runs are reproducible.
// ---------------------------------------------------------------------------
function mulberry32(seed: number): () => number {
	let a = seed >>> 0;
	return function () {
		a |= 0;
		a = (a + 0x6d2b79f5) | 0;
		let t = Math.imul(a ^ (a >>> 15), 1 | a);
		t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
		return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
	};
}

function randInt(rng: () => number, minInclusive: number, maxInclusive: number): number {
	return Math.floor(rng() * (maxInclusive - minInclusive + 1)) + minInclusive;
}

function pick<T>(rng: () => number, items: readonly T[]): T {
	return items[randInt(rng, 0, items.length - 1)];
}

function shuffled<T>(rng: () => number, items: readonly T[]): T[] {
	const copy = items.slice();
	for (let i = copy.length - 1; i > 0; i--) {
		const j = randInt(rng, 0, i);
		[copy[i], copy[j]] = [copy[j], copy[i]];
	}
	return copy;
}

function deepFreeze<T>(value: T): T {
	if (value && typeof value === 'object' && !Object.isFrozen(value)) {
		Object.getOwnPropertyNames(value).forEach((key) => {
			deepFreeze((value as Record<string, unknown>)[key]);
		});
		Object.freeze(value);
	}
	return value;
}

// Builds a ReadingEvent while deliberately allowing values that violate the
// compile-time type (e.g. NaN, or, in a couple of clearly-marked tests,
// wrong runtime types) for fields the contract discusses in terms of runtime
// shape ("not a finite number", "absent"). This mirrors data a browser
// tracker could actually send regardless of what the type declares.
function mkEvent(fields: {
	kind: ReadingEvent['kind'];
	path: string;
	at: number;
	durationMs?: unknown;
	depth?: unknown;
}): ReadingEvent {
	return fields as unknown as ReadingEvent;
}

const KINDS: ReadingEvent['kind'][] = ['page_view', 'heartbeat', 'scroll', 'page_leave'];

const PATH_POOL = [
	'/',
	'//',
	'/blog',
	'/blog/',
	'/blog//',
	'/Blog',
	'/a/b',
	'/a/b/',
	'/a//b',
	'/products/42',
	'/products/42/',
];

// Numeric edge cases only. R5/R6 explicitly discuss negative, absent and
// non-finite numbers, so these are squarely in-contract for randomized
// fuzzing. Values of the wrong *type* (strings, null) are deliberately kept
// out of this shared pool and confined to a separately marked test group,
// so a failure there can never be confused with a failure of the core
// aggregation logic.
const DURATION_POOL: unknown[] = [
	undefined,
	-1,
	-1000,
	0,
	1,
	500,
	1_800_000,
	1_800_001,
	5_000_000,
	NaN,
	Infinity,
	-Infinity,
	123.456,
];

const DEPTH_POOL: unknown[] = [
	undefined,
	-50,
	-0.5,
	0,
	24.9,
	25,
	49.99,
	50,
	74.5,
	75,
	99.99,
	100,
	150,
	NaN,
	Infinity,
	-Infinity,
];

// ---------------------------------------------------------------------------
// Reference model, built directly from the prose (R1-R8, R11). This is the
// oracle for the model-equivalence property test: it is not a peek at the
// implementation, it is a second, independent transcription of the contract.
// ---------------------------------------------------------------------------

function normalizePath(path: string): string {
	if (path === '/') return path;
	if (path.endsWith('/')) return path.slice(0, -1);
	return path;
}

function clampDuration(raw: unknown): number {
	if (typeof raw !== 'number' || !Number.isFinite(raw)) return 0;
	if (raw < 0) return 0;
	return Math.min(raw, 1_800_000);
}

function clampDepth(raw: unknown): number | null {
	if (typeof raw !== 'number' || !Number.isFinite(raw)) return null;
	const clamped = Math.min(100, Math.max(0, raw));
	return Math.floor(clamped);
}

function milestonesFor(depth: number): Milestone[] {
	return ([25, 50, 75, 100] as const).filter((m) => depth >= m) as unknown as Milestone[];
}

function engagedFor(dwellMs: number, depth: number): boolean {
	return dwellMs >= 10_000 && depth >= 50;
}

interface Acc {
	path: string;
	views: number;
	dwellMs: number;
	depth: number;
	earliest: number;
}

function referenceModel(events: readonly ReadingEvent[]): PageSummary[] {
	const byPath = new Map<string, Acc>();
	for (const e of events) {
		const path = normalizePath(e.path);
		let acc = byPath.get(path);
		if (!acc) {
			acc = { path, views: 0, dwellMs: 0, depth: 0, earliest: Infinity };
			byPath.set(path, acc);
		}
		if (e.kind === 'page_view') {
			acc.views += 1;
			if (e.at < acc.earliest) acc.earliest = e.at;
		}
		if (e.kind === 'heartbeat' || e.kind === 'page_leave') {
			acc.dwellMs += clampDuration(e.durationMs);
		}
		if (e.kind === 'scroll' || e.kind === 'page_leave') {
			const d = clampDepth(e.depth);
			if (d !== null && d > acc.depth) acc.depth = d;
		}
	}
	const rows: PageSummary[] = [];
	for (const acc of byPath.values()) {
		if (acc.views === 0) continue;
		rows.push({
			path: acc.path,
			views: acc.views,
			dwellMs: acc.dwellMs,
			depth: acc.depth,
			milestones: milestonesFor(acc.depth),
			engaged: engagedFor(acc.dwellMs, acc.depth),
		});
	}
	rows.sort((a, b) => {
		const ea = byPath.get(a.path)!.earliest;
		const eb = byPath.get(b.path)!.earliest;
		if (ea !== eb) return ea - eb;
		return a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
	});
	return rows;
}

function summaryFor(summaries: readonly PageSummary[], path: string): PageSummary | undefined {
	return summaries.find((s) => s.path === path);
}

function pv(path: string, at: number): ReadingEvent {
	return mkEvent({ kind: 'page_view', path, at });
}
function hb(path: string, at: number, durationMs?: unknown): ReadingEvent {
	return mkEvent({ kind: 'heartbeat', path, at, durationMs });
}
function sc(path: string, at: number, depth?: unknown): ReadingEvent {
	return mkEvent({ kind: 'scroll', path, at, depth });
}
function pl(path: string, at: number, durationMs?: unknown, depth?: unknown): ReadingEvent {
	return mkEvent({ kind: 'page_leave', path, at, durationMs, depth });
}

function randomEvents(rng: () => number, count: number): ReadingEvent[] {
	const events: ReadingEvent[] = [];
	for (let i = 0; i < count; i++) {
		const path = pick(rng, PATH_POOL);
		const kind = pick(rng, KINDS);
		const at = randInt(rng, 0, 100_000);
		if (kind === 'page_view') {
			events.push(pv(path, at));
		} else if (kind === 'heartbeat') {
			events.push(hb(path, at, pick(rng, DURATION_POOL)));
		} else if (kind === 'scroll') {
			events.push(sc(path, at, pick(rng, DEPTH_POOL)));
		} else {
			events.push(pl(path, at, pick(rng, DURATION_POOL), pick(rng, DEPTH_POOL)));
		}
	}
	return events;
}

// =============================================================================
// R11 - nothing to read
// =============================================================================

describe('R11 - nothing to read', () => {
	it('an empty array of events produces an empty array of summaries', () => {
		expect(summarizeReading([]), 'no events means no page was ever read').toEqual([]);
	});
});

// =============================================================================
// R1 - which pages get a summary
// =============================================================================

describe('R1 - a page only exists in the output if it was viewed', () => {
	it('a page with only heartbeat/scroll/page_leave events, never a page_view, produces no summary', () => {
		const events = [hb('/never-viewed', 1, 5000), sc('/never-viewed', 2, 80), pl('/never-viewed', 3, 1000, 90)];
		expect(
			summarizeReading(events),
			'events for an unviewed page must not fabricate a summary for it'
		).toEqual([]);
	});

	it("an unviewed page's events do not leak into a different, viewed page's summary", () => {
		const events = [pv('/viewed', 1), hb('/never-viewed', 2, 999_999), sc('/never-viewed', 3, 100)];
		const result = summarizeReading(events);
		expect(
			result.map((s) => s.path),
			'only the viewed page should appear in the output'
		).toEqual(['/viewed']);
		expect(result[0].dwellMs, 'dwell from an unrelated unviewed page must not bleed across pages').toBe(0);
		expect(result[0].depth, 'depth from an unrelated unviewed page must not bleed across pages').toBe(0);
	});
});

// =============================================================================
// R2 - same page
// =============================================================================

describe('R2 - two paths name the same page after removing one trailing slash', () => {
	it('a trailing slash does not create a second page', () => {
		const events = [pv('/blog', 100), pv('/blog/', 200)];
		const result = summarizeReading(events);
		expect(result, 'trailing slash is the same page, so both views land on one summary').toHaveLength(1);
		expect(result[0].views, '/blog and /blog/ are the same page and both page_views must be counted together').toBe(2);
	});

	it('the root path / is left exactly as it is', () => {
		const events = [pv('/', 1)];
		expect(summarizeReading(events)[0].path, 'root path must render as / itself').toBe('/');
	});

	it('// normalizes to / by removing its single trailing slash, colliding with the root page', () => {
		// Derived from R2's algorithm: "/" is left as-is because it has no
		// non-root trailing slash to remove; "//" has one trailing slash
		// removed, leaving "/". The two therefore name the same page.
		const events = [pv('/', 1), pv('//', 2)];
		const result = summarizeReading(events);
		expect(result, 'root and // must merge into a single page named /').toHaveLength(1);
		expect(result[0].path).toBe('/');
		expect(result[0].views, 'both page_views count toward the merged page').toBe(2);
	});

	it('a path with two trailing slashes only has one removed, so it differs from the single-trailing-slash form', () => {
		// "/blog//" -> "/blog/" (one slash removed). This is NOT the same as
		// "/blog/" -> "/blog". Removal is single, not iterative/greedy. A
		// common (but non-compliant) implementation strips all trailing
		// slashes with something like /\/+$/ - this test exists to catch that.
		const events = [pv('/blog/', 10), pv('/blog//', 20)];
		const result = summarizeReading(events);
		expect(
			result.map((s) => s.path).sort(),
			'trailing-slash removal is a single pass, not repeated, so /blog/ and /blog// are different pages'
		).toEqual(['/blog', '/blog/']);
	});

	it('case is never normalized: /Blog and /blog are different pages', () => {
		const events = [pv('/Blog', 1), pv('/blog', 2)];
		expect(
			summarizeReading(events)
				.map((s) => s.path)
				.sort(),
			'path comparison is case-sensitive'
		).toEqual(['/Blog', '/blog']);
	});

	it('an internal double slash is never collapsed, only a trailing slash is touched', () => {
		const events = [pv('/a//b', 1), pv('/a//b/', 2)];
		const result = summarizeReading(events);
		expect(
			result,
			'these normalize to the same page: only the trailing slash is removed, the internal // stays'
		).toHaveLength(1);
		expect(result[0].path).toBe('/a//b');
	});
});

// =============================================================================
// R3 - order
// =============================================================================

describe('R3 - summaries are ordered by earliest page_view time, then by path', () => {
	it('the page viewed first comes first', () => {
		const events = [pv('/second', 200), pv('/first', 100)];
		expect(summarizeReading(events).map((s) => s.path)).toEqual(['/first', '/second']);
	});

	it("a page's earliest page_view is its minimum across all its page_views, regardless of array position", () => {
		const events = [pv('/late-then-early', 500), pv('/middle', 300), pv('/late-then-early', 50)];
		expect(
			summarizeReading(events).map((s) => s.path),
			"the earliest of a page's several page_views decides its position, not the first one encountered in the array"
		).toEqual(['/late-then-early', '/middle']);
	});

	it('ties on earliest page_view time break on path using plain <, not a locale-aware compare', () => {
		// 'C' (0x43) is less than 'b' (0x62) under strict <, so /Cherry sorts
		// before /banana even though a case-insensitive/locale compare would
		// put banana first. Chosen specifically to catch a localeCompare sort.
		const events = [pv('/banana', 1000), pv('/Cherry', 1000)];
		expect(
			summarizeReading(events).map((s) => s.path),
			'ties must be broken with the plain < operator on paths, not a locale-aware compare'
		).toEqual(['/Cherry', '/banana']);
	});
});

// =============================================================================
// R4 - views
// =============================================================================

describe('R4 - views counts page_view events for the page', () => {
	it('duplicate page_view events are each counted, not deduplicated', () => {
		const events = [pv('/p', 1), pv('/p', 1), pv('/p', 1)];
		expect(summarizeReading(events)[0].views, 'views is a count of events, not a count of distinct visits').toBe(3);
	});

	it('non-page_view events never add to views', () => {
		const events = [pv('/p', 1), hb('/p', 2, 1000), sc('/p', 3, 50), pl('/p', 4, 1000, 50)];
		expect(summarizeReading(events)[0].views, 'only page_view events count as views').toBe(1);
	});
});

// =============================================================================
// R5 - dwell
// =============================================================================

describe('R5 - dwellMs sums durationMs from heartbeat and page_leave only', () => {
	it('heartbeat and page_leave durations are added together', () => {
		const events = [pv('/p', 0), hb('/p', 1, 1000), pl('/p', 2, 2000)];
		expect(summarizeReading(events)[0].dwellMs).toBe(3000);
	});

	it('durationMs on any other kind of event is ignored', () => {
		const events = [pv('/p', 0), mkEvent({ kind: 'scroll', path: '/p', at: 1, depth: 10, durationMs: 999_999 })];
		expect(
			summarizeReading(events)[0].dwellMs,
			"a scroll event's durationMs must never be added to dwell time"
		).toBe(0);
	});

	it('a page_view carrying durationMs contributes nothing to dwell', () => {
		const events = [mkEvent({ kind: 'page_view', path: '/p', at: 0, durationMs: 500_000 })];
		expect(summarizeReading(events)[0].dwellMs, 'page_view is not a dwell-bearing kind').toBe(0);
	});

	it("a negative durationMs contributes 0, it does not subtract from other events' dwell", () => {
		const events = [pv('/p', 0), hb('/p', 1, 1000), hb('/p', 2, -500)];
		expect(
			summarizeReading(events)[0].dwellMs,
			'a negative duration must floor at 0 contribution, it must never reduce the running total'
		).toBe(1000);
	});

	it('an absent durationMs contributes 0', () => {
		const events = [pv('/p', 0), hb('/p', 1)];
		expect(summarizeReading(events)[0].dwellMs).toBe(0);
	});

	it('a non-finite durationMs (NaN) contributes 0', () => {
		const events = [pv('/p', 0), hb('/p', 1, NaN)];
		expect(summarizeReading(events)[0].dwellMs).toBe(0);
	});

	it('an infinite durationMs contributes 0, distinct from the cap that applies to large finite values', () => {
		const events = [pv('/p', 0), hb('/p', 1, Infinity)];
		expect(
			summarizeReading(events)[0].dwellMs,
			'Infinity is not finite and must contribute 0; it must not be silently clamped to the 1,800,000 cap'
		).toBe(0);
	});

	it('a single event contributes at most 1,800,000ms; a longer finite value is capped exactly there', () => {
		const events = [pv('/p', 0), hb('/p', 1, 5_000_000)];
		expect(summarizeReading(events)[0].dwellMs, "a single event's dwell contribution is capped at 30 minutes").toBe(
			1_800_000
		);
	});

	it('a duration exactly at the cap is not reduced further', () => {
		const events = [pv('/p', 0), hb('/p', 1, 1_800_000)];
		expect(summarizeReading(events)[0].dwellMs).toBe(1_800_000);
	});

	it('the cap applies per event, not to the running total', () => {
		const events = [pv('/p', 0), hb('/p', 1, 1_800_000), hb('/p', 2, 1_800_000)];
		expect(
			summarizeReading(events)[0].dwellMs,
			"two capped events must each contribute their own 1,800,000; the cap is not a ceiling on the sum"
		).toBe(3_600_000);
	});

	it('timestamps never contribute to dwell: a gap with no durations dwells for 0ms', () => {
		const events = [pv('/p', 0), hb('/p', 0), hb('/p', 3_600_000)];
		expect(
			summarizeReading(events)[0].dwellMs,
			'an hour between two events with no durationMs must not be mistaken for an hour of dwell time'
		).toBe(0);
	});
});

// =============================================================================
// R6 - depth
// =============================================================================

describe('R6 - depth is the greatest usable depth from scroll and page_leave', () => {
	it('the maximum among scroll and page_leave depths wins, not the latest one', () => {
		const events = [pv('/p', 0), sc('/p', 1, 30), sc('/p', 2, 10), pl('/p', 3, 0, 20)];
		expect(summarizeReading(events)[0].depth, 'depth is the greatest value seen; order must not matter').toBe(30);
	});

	it('depth on any other kind of event is ignored', () => {
		const events = [pv('/p', 0), hb('/p', 1, 1000), mkEvent({ kind: 'page_view', path: '/p', at: 2, depth: 90 })];
		expect(summarizeReading(events)[0].depth, 'only scroll and page_leave carry usable depth information').toBe(0);
	});

	it('a depth above 100 is clamped to 100, not ignored', () => {
		// The discriminating case: if out-of-range values were treated like
		// absent/non-finite ones, the result would be the default 0, not 100.
		const events = [pv('/p', 0), sc('/p', 1, 150)];
		expect(summarizeReading(events)[0].depth, 'an out-of-range depth is clamped into range, it is not dropped').toBe(
			100
		);
	});

	it('a negative depth clamps to 0', () => {
		const events = [pv('/p', 0), sc('/p', 1, -20)];
		expect(summarizeReading(events)[0].depth).toBe(0);
	});

	it('a fractional depth is rounded down, not to the nearest whole number', () => {
		const events = [pv('/p', 0), sc('/p', 1, 87.9)];
		expect(summarizeReading(events)[0].depth, 'depth rounds down (floor); 87.9 must not become 88').toBe(87);
	});

	it('an absent depth is ignored', () => {
		const events = [pv('/p', 0), sc('/p', 1)];
		expect(summarizeReading(events)[0].depth).toBe(0);
	});

	it('a non-finite depth (NaN) is ignored, it does not win the max over a real value', () => {
		const events = [pv('/p', 0), sc('/p', 1, 40), sc('/p', 2, NaN)];
		expect(summarizeReading(events)[0].depth, 'NaN must not be treated as larger than every real depth').toBe(40);
	});

	it('a page with no usable depth data defaults to 0', () => {
		const events = [pv('/p', 0), hb('/p', 1, 5000)];
		expect(summarizeReading(events)[0].depth).toBe(0);
	});
});

// =============================================================================
// R7 - milestones
// =============================================================================

describe('R7 - milestones list every threshold at or below depth, ascending', () => {
	it.each([
		[0, [] as Milestone[]],
		[10, [] as Milestone[]],
		[24, [] as Milestone[]],
		[25, [25] as Milestone[]],
		[49, [25] as Milestone[]],
		[50, [25, 50] as Milestone[]],
		[74, [25, 50] as Milestone[]],
		[75, [25, 50, 75] as Milestone[]],
		[99, [25, 50, 75] as Milestone[]],
		[100, [25, 50, 75, 100] as Milestone[]],
	])('at depth %i, milestones are %j', (depthInput, expectedMilestones) => {
		const events = [pv('/p', 0), sc('/p', 1, depthInput)];
		const summary = summarizeReading(events)[0];
		expect(summary.depth).toBe(depthInput);
		expect(
			summary.milestones,
			`milestones must list every one of 25/50/75/100 that is <= the page's depth (${depthInput}), in ascending order`
		).toEqual(expectedMilestones);
	});

	it('a depth that clamps to 100 still yields every milestone', () => {
		const events = [pv('/p', 0), sc('/p', 1, 500)];
		expect(summarizeReading(events)[0].milestones).toEqual([25, 50, 75, 100]);
	});
});

// =============================================================================
// R8 - engaged
// =============================================================================

describe('R8 - engaged requires both dwellMs >= 10,000 and depth >= 50', () => {
	it.each([
		[10_000, 50, true, 'meets both minimums exactly'],
		[9_999, 100, false, 'one millisecond short of the dwell minimum, however deep'],
		[100_000, 49, false, 'one point short of the depth minimum, however long'],
		[0, 0, false, 'no engagement signal at all'],
		[50_000, 100, true, 'comfortably exceeds both minimums'],
	])('dwellMs=%i, depth=%i -> engaged=%s (%s)', (dwellMs, depth, expected) => {
		const events = [pv('/p', 0), hb('/p', 1, dwellMs), sc('/p', 2, depth)];
		expect(
			summarizeReading(events)[0].engaged,
			'engaged requires both conditions to hold; neither one alone is sufficient'
		).toBe(expected);
	});
});

// =============================================================================
// Kind isolation - the "on any other kind, X is ignored" promises, made total
// =============================================================================

describe('every event kind affects only the fields the contract assigns to it', () => {
	it('page_view affects only views and ordering, never dwell or depth', () => {
		const base = [pv('/p', 100), hb('/p', 101, 1000), sc('/p', 102, 40)];
		const before = summarizeReading(base)[0];
		const after = summarizeReading([
			...base,
			mkEvent({ kind: 'page_view', path: '/p', at: 103, durationMs: 999_999, depth: 100 }),
		])[0];
		expect(after.views, 'a second page_view must increment views').toBe(before.views + 1);
		expect(after.dwellMs, "a page_view's stray durationMs/depth fields must not affect dwell").toBe(before.dwellMs);
		expect(after.depth, "a page_view's stray durationMs/depth fields must not affect depth").toBe(before.depth);
	});

	it('heartbeat affects only dwell, never views or depth', () => {
		const base = [pv('/p', 100), sc('/p', 101, 40)];
		const before = summarizeReading(base)[0];
		const after = summarizeReading([
			...base,
			mkEvent({ kind: 'heartbeat', path: '/p', at: 102, durationMs: 5000, depth: 100 }),
		])[0];
		expect(after.dwellMs, "a heartbeat's duration must be added to dwell").toBe(before.dwellMs + 5000);
		expect(after.views, 'heartbeat must never affect views').toBe(before.views);
		expect(after.depth, "a heartbeat's stray depth field must not affect depth").toBe(before.depth);
	});

	it('scroll affects only depth, never views or dwell', () => {
		const base = [pv('/p', 100), hb('/p', 101, 3000)];
		const before = summarizeReading(base)[0];
		const after = summarizeReading([
			...base,
			mkEvent({ kind: 'scroll', path: '/p', at: 102, depth: 90, durationMs: 999_999 }),
		])[0];
		expect(after.depth, 'a scroll to a new maximum must raise depth').toBe(90);
		expect(after.views, 'scroll must never affect views').toBe(before.views);
		expect(after.dwellMs, "a scroll's stray durationMs field must not affect dwell").toBe(before.dwellMs);
	});

	it('page_leave affects both dwell and depth, never views', () => {
		const base = [pv('/p', 100)];
		const before = summarizeReading(base)[0];
		const after = summarizeReading([...base, pl('/p', 101, 4000, 60)])[0];
		expect(after.dwellMs, 'page_leave duration must be added to dwell').toBe(before.dwellMs + 4000);
		expect(after.depth, "page_leave depth must be able to raise the page's depth").toBe(60);
		expect(after.views, 'page_leave must never affect views').toBe(before.views);
	});
});

// =============================================================================
// Fail-closed on data that does not match the compile-time shape.
//
// INFERENCE: the contract's "not a finite number" / "absent" wording is read
// here to also cover values of the wrong runtime *type* (a string, null),
// as real tracker payloads might send despite what the TypeScript type
// declares. The contract does not say this explicitly. These two tests are
// kept out of the shared random-fuzz pools above so a failure here is never
// confused with a failure of the core aggregation logic.
// =============================================================================

describe('fail-closed on data that does not match the compile-time shape (inference, see comment above)', () => {
	it('a string where durationMs should be contributes 0; it is not parsed or coerced', () => {
		const events = [pv('/p', 0), mkEvent({ kind: 'heartbeat', path: '/p', at: 1, durationMs: '5000' })];
		expect(() => summarizeReading(events)).not.toThrow();
		expect(summarizeReading(events)[0].dwellMs, 'a non-number durationMs must not be parsed or coerced').toBe(0);
	});

	it('null where depth should be is ignored; it does not throw and does not win a comparison it should lose', () => {
		const events = [pv('/p', 0), sc('/p', 1, 40), mkEvent({ kind: 'scroll', path: '/p', at: 2, depth: null })];
		expect(() => summarizeReading(events)).not.toThrow();
		expect(summarizeReading(events)[0].depth).toBe(40);
	});
});

// =============================================================================
// R9 - arrival order
// =============================================================================

describe('R9 - the result depends only on which events are present, never their order', () => {
	it('a small hand-picked reordering produces an identical result', () => {
		const inOrder = [pv('/a', 10), hb('/a', 11, 2000), pv('/b', 5), sc('/b', 6, 70)];
		const reordered = [sc('/b', 6, 70), pv('/b', 5), hb('/a', 11, 2000), pv('/a', 10)];
		expect(
			summarizeReading(reordered),
			'array position must never change the outcome, only which events exist'
		).toEqual(summarizeReading(inOrder));
	});

	it("property: random shuffles of the same events match a reference model built directly from the contract's prose", () => {
		for (let trial = 0; trial < 25; trial++) {
			const rng = mulberry32(20260912 + trial);
			const events = randomEvents(rng, randInt(rng, 0, 40));
			const expected = referenceModel(events);
			const inOrderActual = summarizeReading(events);
			const shuffledActual = summarizeReading(shuffled(rng, events));
			expect(
				inOrderActual,
				`trial ${trial}: the implementation must match the contract's own rules for aggregation, ordering and filtering`
			).toEqual(expected);
			expect(shuffledActual, `trial ${trial}: shuffling the same events must not change the result`).toEqual(
				expected
			);
		}
	});
});

// =============================================================================
// R10 - purity
// =============================================================================

describe('R10 - purity: the input is never modified, and the same input always yields an equal result', () => {
	it('the events array and its elements are the same references after the call', () => {
		const events = [pv('/p', 1), hb('/p', 2, 500), sc('/p', 3, 40)];
		const refs = events.slice();
		summarizeReading(events);
		expect(events.length, 'the input array must not grow or shrink').toBe(refs.length);
		expect(
			events.every((e, i) => e === refs[i]),
			'the input array\'s elements must not be reordered, replaced, added or removed'
		).toBe(true);
	});

	it('a deep-frozen input does not throw, proving no mutation is attempted', () => {
		const events = [pv('/p', 1), hb('/p', 2, 500), sc('/p', 3, 40), pl('/p', 4, 100, 60)];
		events.forEach(deepFreeze);
		deepFreeze(events);
		expect(() => summarizeReading(events), 'a pure function must not need to write to its input').not.toThrow();
	});

	it('calling summarizeReading twice on the same input yields an equal result both times', () => {
		const events = [pv('/x', 1), hb('/x', 2, 3000), pv('/y', 5)];
		const first = summarizeReading(events);
		const second = summarizeReading(events);
		expect(second, 'the same input must always produce an equal result').toEqual(first);
	});
});

// =============================================================================
// Property: monotonicity
// =============================================================================

describe("property: adding a dwell-bearing event never decreases a page's dwellMs", () => {
	it('dwellMs is monotone under any additional heartbeat/page_leave event, valid or not', () => {
		for (let trial = 0; trial < 20; trial++) {
			const rng = mulberry32(555 + trial);
			const base: ReadingEvent[] = [
				pv('/monotonic-dwell', 0),
				...randomEvents(rng, randInt(rng, 0, 10)).map((e) => mkEvent({ ...e, path: '/monotonic-dwell' })),
			];
			const before = summaryFor(summarizeReading(base), '/monotonic-dwell')!.dwellMs;
			const extraKind = pick(rng, ['heartbeat', 'page_leave'] as const);
			const extra =
				extraKind === 'heartbeat'
					? hb('/monotonic-dwell', 999, pick(rng, DURATION_POOL))
					: pl('/monotonic-dwell', 999, pick(rng, DURATION_POOL), pick(rng, DEPTH_POOL));
			const after = summaryFor(summarizeReading([...base, extra]), '/monotonic-dwell')!.dwellMs;
			expect(
				after,
				`trial ${trial}: a minimum contribution of 0 means dwell can only stay the same or grow, never shrink`
			).toBeGreaterThanOrEqual(before);
		}
	});
});

describe("property: adding a depth-bearing event never decreases a page's depth", () => {
	it('depth is monotone under any additional scroll/page_leave event, valid or not', () => {
		for (let trial = 0; trial < 20; trial++) {
			const rng = mulberry32(777 + trial);
			const base: ReadingEvent[] = [
				pv('/monotonic-depth', 0),
				...randomEvents(rng, randInt(rng, 0, 10)).map((e) => mkEvent({ ...e, path: '/monotonic-depth' })),
			];
			const before = summaryFor(summarizeReading(base), '/monotonic-depth')!.depth;
			const extraKind = pick(rng, ['scroll', 'page_leave'] as const);
			const extra =
				extraKind === 'scroll'
					? sc('/monotonic-depth', 999, pick(rng, DEPTH_POOL))
					: pl('/monotonic-depth', 999, pick(rng, DURATION_POOL), pick(rng, DEPTH_POOL));
			const after = summaryFor(summarizeReading([...base, extra]), '/monotonic-depth')!.depth;
			expect(
				after,
				`trial ${trial}: depth is a running maximum; it can only stay the same or grow, never shrink`
			).toBeGreaterThanOrEqual(before);
		}
	});
});

// =============================================================================
// Worked examples (fully derivable from the rules above; kept small and
// concrete for a human reader, not a source of unstated expectations)
// =============================================================================

describe('worked example: a full pipeline from raw events to one summary', () => {
	it('views, dwell, depth, milestones and engaged are all derived consistently from the same events', () => {
		const events = [pv('/guide', 1_000), hb('/guide', 2_000, 4_000), sc('/guide', 3_000, 55), pl('/guide', 4_000, 8_000, 40)];
		const [summary] = summarizeReading(events);
		expect(summary.path).toBe('/guide');
		expect(summary.views, 'one page_view').toBe(1);
		expect(summary.dwellMs, 'heartbeat 4000 + page_leave 8000').toBe(12_000);
		expect(summary.depth, 'greatest of scroll 55 and page_leave 40').toBe(55);
		expect(summary.milestones, '25 and 50 are <= 55; 75 and 100 are not').toEqual([25, 50]);
		expect(summary.engaged, 'dwell 12000 >= 10000 and depth 55 >= 50').toBe(true);
	});
});

describe('a lone page_view with no other signal starts from a clean baseline', () => {
	it('views=1, dwellMs=0, depth=0, milestones=[], engaged=false', () => {
		const summary = summarizeReading([pv('/only-viewed', 1)])[0];
		expect(
			summary,
			'a page with no dwell/depth signal at all must report the zero-value baseline for every derived field, not throw or omit fields'
		).toEqual({
			path: '/only-viewed',
			views: 1,
			dwellMs: 0,
			depth: 0,
			milestones: [],
			engaged: false,
		});
	});
});
The suite written from the codeOption B, as written
reading.spec.ts · written from the code
import { describe, it, expect } from 'vitest';
import { summarizeReading, type ReadingEvent } from './reading';

const MIN = 60 * 1000;
const MAX_DWELL_PER_EVENT_MS = 30 * MIN;

function pageView(path: string, at: number): ReadingEvent {
	return { kind: 'page_view', path, at };
}

function heartbeat(path: string, at: number, durationMs: number): ReadingEvent {
	return { kind: 'heartbeat', path, at, durationMs };
}

function scroll(path: string, at: number, depth: number): ReadingEvent {
	return { kind: 'scroll', path, at, depth };
}

function pageLeave(
	path: string,
	at: number,
	durationMs?: number,
	depth?: number
): ReadingEvent {
	return { kind: 'page_leave', path, at, durationMs, depth };
}

describe('summarizeReading', () => {
	describe('basic shape', () => {
		it('returns an empty array for no events', () => {
			expect(summarizeReading([])).toEqual([]);
		});

		it('produces a default summary for a lone page_view', () => {
			const result = summarizeReading([pageView('/a', 100)]);
			expect(result).toEqual([
				{
					path: '/a',
					views: 1,
					dwellMs: 0,
					depth: 0,
					milestones: [],
					engaged: false
				}
			]);
		});
	});

	describe('R1: only viewed pages get a summary', () => {
		it('drops events for a page that never had a page_view', () => {
			const result = summarizeReading([
				heartbeat('/never-viewed', 100, 5000),
				scroll('/never-viewed', 200, 80),
				pageLeave('/never-viewed', 300, 1000, 90)
			]);
			expect(result).toEqual([]);
		});

		it('only includes viewed pages when mixed with unviewed ones', () => {
			const result = summarizeReading([
				pageView('/viewed', 100),
				heartbeat('/unviewed', 150, 5000)
			]);
			expect(result).toHaveLength(1);
			expect(result[0].path).toBe('/viewed');
		});
	});

	describe('R2: path normalization', () => {
		it('treats a trailing-slash path as the same page as the bare path', () => {
			const result = summarizeReading([pageView('/about', 100), pageView('/about/', 200)]);
			expect(result).toHaveLength(1);
			expect(result[0].path).toBe('/about');
			expect(result[0].views).toBe(2);
		});

		it('does not strip the root path', () => {
			const result = summarizeReading([pageView('/', 100)]);
			expect(result[0].path).toBe('/');
		});

		it('merges events across trailing-slash variants of the same path', () => {
			const result = summarizeReading([
				pageView('/blog/', 100),
				scroll('/blog', 200, 60),
				heartbeat('/blog/', 300, 5000)
			]);
			expect(result).toHaveLength(1);
			expect(result[0]).toMatchObject({ path: '/blog', depth: 60, dwellMs: 5000 });
		});

		it('normalizes only a single trailing slash', () => {
			// '//' -> strip one trailing slash -> '/', which is left alone thereafter.
			const result = summarizeReading([pageView('//', 100), pageView('/', 200)]);
			expect(result).toHaveLength(1);
			expect(result[0].path).toBe('/');
			expect(result[0].views).toBe(2);
		});
	});

	describe('views counting', () => {
		it('counts every page_view event for a page, not just the first', () => {
			const result = summarizeReading([
				pageView('/a', 100),
				pageView('/a', 200),
				pageView('/a', 300)
			]);
			expect(result[0].views).toBe(3);
		});

		it('keeps view counts independent per page', () => {
			const result = summarizeReading([
				pageView('/a', 100),
				pageView('/b', 150),
				pageView('/a', 200)
			]);
			const byPath = Object.fromEntries(result.map((s) => [s.path, s.views]));
			expect(byPath['/a']).toBe(2);
			expect(byPath['/b']).toBe(1);
		});
	});

	describe('dwell time (R5)', () => {
		it('sums durationMs from heartbeat events', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				heartbeat('/a', 100, 1000),
				heartbeat('/a', 200, 2000)
			]);
			expect(result[0].dwellMs).toBe(3000);
		});

		it('sums durationMs from page_leave events', () => {
			const result = summarizeReading([pageView('/a', 0), pageLeave('/a', 100, 4000, 10)]);
			expect(result[0].dwellMs).toBe(4000);
		});

		it('combines heartbeat and page_leave durations', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				heartbeat('/a', 100, 1000),
				pageLeave('/a', 200, 2000, 0)
			]);
			expect(result[0].dwellMs).toBe(3000);
		});

		it('caps a single event contribution at 30 minutes', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				heartbeat('/a', 100, MAX_DWELL_PER_EVENT_MS + 5 * MIN)
			]);
			expect(result[0].dwellMs).toBe(MAX_DWELL_PER_EVENT_MS);
		});

		it('caps each event individually before summing (two long events both cap)', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				heartbeat('/a', 100, MAX_DWELL_PER_EVENT_MS * 2),
				heartbeat('/a', 200, MAX_DWELL_PER_EVENT_MS * 2)
			]);
			expect(result[0].dwellMs).toBe(MAX_DWELL_PER_EVENT_MS * 2);
		});

		it('allows a duration of exactly the cap without truncation', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				heartbeat('/a', 100, MAX_DWELL_PER_EVENT_MS)
			]);
			expect(result[0].dwellMs).toBe(MAX_DWELL_PER_EVENT_MS);
		});

		it('ignores a missing durationMs on heartbeat', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				{ kind: 'heartbeat', path: '/a', at: 100 }
			]);
			expect(result[0].dwellMs).toBe(0);
		});

		it('ignores a negative durationMs', () => {
			const result = summarizeReading([pageView('/a', 0), heartbeat('/a', 100, -500)]);
			expect(result[0].dwellMs).toBe(0);
		});

		it('ignores a non-finite durationMs (NaN / Infinity)', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				heartbeat('/a', 100, NaN),
				heartbeat('/a', 200, Infinity)
			]);
			expect(result[0].dwellMs).toBe(0);
		});

		it('ignores a non-numeric durationMs', () => {
			const bad = { kind: 'heartbeat', path: '/a', at: 100, durationMs: '5000' } as unknown as ReadingEvent;
			const result = summarizeReading([pageView('/a', 0), bad]);
			expect(result[0].dwellMs).toBe(0);
		});

		it('allows a durationMs of exactly zero', () => {
			const result = summarizeReading([pageView('/a', 0), heartbeat('/a', 100, 0)]);
			expect(result[0].dwellMs).toBe(0);
		});

		it('ignores durationMs on a page_view or scroll event', () => {
			const withExtra = {
				kind: 'scroll',
				path: '/a',
				at: 100,
				depth: 10,
				durationMs: 5000
			} as unknown as ReadingEvent;
			const result = summarizeReading([pageView('/a', 0), withExtra]);
			expect(result[0].dwellMs).toBe(0);
		});
	});

	describe('depth (R6)', () => {
		it('takes depth from scroll events', () => {
			const result = summarizeReading([pageView('/a', 0), scroll('/a', 100, 42)]);
			expect(result[0].depth).toBe(42);
		});

		it('takes depth from page_leave events', () => {
			const result = summarizeReading([pageView('/a', 0), pageLeave('/a', 100, 1000, 77)]);
			expect(result[0].depth).toBe(77);
		});

		it('keeps the maximum depth seen, ignoring later smaller values', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				scroll('/a', 100, 80),
				scroll('/a', 200, 40)
			]);
			expect(result[0].depth).toBe(80);
		});

		it('increases depth when a later event goes deeper', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				scroll('/a', 100, 20),
				scroll('/a', 200, 60)
			]);
			expect(result[0].depth).toBe(60);
		});

		it('clamps depth above 100 down to 100', () => {
			const result = summarizeReading([pageView('/a', 0), scroll('/a', 100, 150)]);
			expect(result[0].depth).toBe(100);
		});

		it('clamps a negative depth up to 0', () => {
			const result = summarizeReading([pageView('/a', 0), scroll('/a', 100, -10)]);
			expect(result[0].depth).toBe(0);
		});

		it('floors a fractional depth', () => {
			const result = summarizeReading([pageView('/a', 0), scroll('/a', 100, 50.9)]);
			expect(result[0].depth).toBe(50);
		});

		it('ignores a missing depth on scroll', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				{ kind: 'scroll', path: '/a', at: 100 }
			]);
			expect(result[0].depth).toBe(0);
		});

		it('ignores a non-finite depth (NaN / Infinity)', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				scroll('/a', 100, NaN),
				scroll('/a', 200, Infinity)
			]);
			expect(result[0].depth).toBe(0);
		});

		it('ignores a non-numeric depth', () => {
			const bad = { kind: 'scroll', path: '/a', at: 100, depth: '90' } as unknown as ReadingEvent;
			const result = summarizeReading([pageView('/a', 0), bad]);
			expect(result[0].depth).toBe(0);
		});

		it('ignores depth on a heartbeat event', () => {
			const withExtra = {
				kind: 'heartbeat',
				path: '/a',
				at: 100,
				durationMs: 1000,
				depth: 90
			} as unknown as ReadingEvent;
			const result = summarizeReading([pageView('/a', 0), withExtra]);
			expect(result[0].depth).toBe(0);
		});

		it('accepts a depth of exactly 0 as usable (does not lower an existing max)', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				scroll('/a', 100, 30),
				scroll('/a', 200, 0)
			]);
			expect(result[0].depth).toBe(30);
		});
	});

	describe('milestones', () => {
		it('reports no milestones below 25', () => {
			const result = summarizeReading([pageView('/a', 0), scroll('/a', 100, 24)]);
			expect(result[0].milestones).toEqual([]);
		});

		it('reports 25 at exactly the boundary', () => {
			const result = summarizeReading([pageView('/a', 0), scroll('/a', 100, 25)]);
			expect(result[0].milestones).toEqual([25]);
		});

		it('reports 25 and 50 at 60% depth', () => {
			const result = summarizeReading([pageView('/a', 0), scroll('/a', 100, 60)]);
			expect(result[0].milestones).toEqual([25, 50]);
		});

		it('reports all four milestones at 100% depth', () => {
			const result = summarizeReading([pageView('/a', 0), scroll('/a', 100, 100)]);
			expect(result[0].milestones).toEqual([25, 50, 75, 100]);
		});

		it('reports all four milestones when depth clamps up to 100', () => {
			const result = summarizeReading([pageView('/a', 0), scroll('/a', 100, 250)]);
			expect(result[0].milestones).toEqual([25, 50, 75, 100]);
		});
	});

	describe('engaged flag', () => {
		it('is false when dwell is high but depth is below 50', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				heartbeat('/a', 100, 60_000),
				scroll('/a', 200, 40)
			]);
			expect(result[0].engaged).toBe(false);
		});

		it('is false when depth is high but dwell is below 10s', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				heartbeat('/a', 100, 5000),
				scroll('/a', 200, 90)
			]);
			expect(result[0].engaged).toBe(false);
		});

		it('is true right at both thresholds', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				heartbeat('/a', 100, 10_000),
				scroll('/a', 200, 50)
			]);
			expect(result[0].engaged).toBe(true);
		});

		it('is false just under the dwell threshold', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				heartbeat('/a', 100, 9_999),
				scroll('/a', 200, 50)
			]);
			expect(result[0].engaged).toBe(false);
		});

		it('is false just under the depth threshold', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				heartbeat('/a', 100, 10_000),
				scroll('/a', 200, 49)
			]);
			expect(result[0].engaged).toBe(false);
		});

		it('is true well above both thresholds', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				heartbeat('/a', 100, 120_000),
				scroll('/a', 200, 100)
			]);
			expect(result[0].engaged).toBe(true);
		});
	});

	describe('R3: ordering', () => {
		it('orders pages by earliest page_view time, regardless of event array order', () => {
			const result = summarizeReading([
				pageView('/b', 50),
				pageView('/a', 100)
			]);
			expect(result.map((s) => s.path)).toEqual(['/b', '/a']);
		});

		it('uses the earliest page_view when a page has several', () => {
			const result = summarizeReading([
				pageView('/a', 300),
				pageView('/b', 200),
				pageView('/a', 50) // earliest for /a, appears last in the array
			]);
			expect(result.map((s) => s.path)).toEqual(['/a', '/b']);
		});

		it('breaks ties in earliest view time by path, ascending', () => {
			const result = summarizeReading([
				pageView('/z', 100),
				pageView('/a', 100),
				pageView('/m', 100)
			]);
			expect(result.map((s) => s.path)).toEqual(['/a', '/m', '/z']);
		});

		it('orders three pages with distinct times correctly', () => {
			const result = summarizeReading([
				pageView('/third', 300),
				pageView('/first', 100),
				pageView('/second', 200)
			]);
			expect(result.map((s) => s.path)).toEqual(['/first', '/second', '/third']);
		});
	});

	describe('multi-page independence and realistic mixes', () => {
		it('keeps dwell, depth and milestones independent across pages', () => {
			const result = summarizeReading([
				pageView('/a', 0),
				pageView('/b', 10),
				heartbeat('/a', 100, 20_000),
				scroll('/a', 150, 80),
				heartbeat('/b', 200, 1000),
				scroll('/b', 250, 20)
			]);
			const byPath = Object.fromEntries(result.map((s) => [s.path, s]));
			expect(byPath['/a']).toMatchObject({
				dwellMs: 20_000,
				depth: 80,
				milestones: [25, 50, 75],
				engaged: true
			});
			expect(byPath['/b']).toMatchObject({
				dwellMs: 1000,
				depth: 20,
				milestones: [],
				engaged: false
			});
		});

		it('handles a realistic single-page session end to end', () => {
			const result = summarizeReading([
				pageView('/blog/post', 1000),
				scroll('/blog/post', 2000, 30),
				heartbeat('/blog/post', 5000, 4000),
				scroll('/blog/post', 8000, 70),
				heartbeat('/blog/post', 12000, 4000),
				pageLeave('/blog/post', 15000, 3000, 65)
			]);
			expect(result).toEqual([
				{
					path: '/blog/post',
					views: 1,
					dwellMs: 11000,
					depth: 70,
					milestones: [25, 50],
					engaged: true
				}
			]);
		});

		it('handles a full multi-page browsing session', () => {
			const result = summarizeReading([
				pageView('/home', 0),
				scroll('/home', 500, 100),
				heartbeat('/home', 1000, 15_000),
				pageView('/pricing', 2000),
				heartbeat('/pricing', 3000, 3000),
				scroll('/pricing', 3500, 45),
				pageView('/home', 4000) // revisit
			]);
			expect(result.map((s) => s.path)).toEqual(['/home', '/pricing']);
			const home = result[0];
			const pricing = result[1];
			expect(home).toMatchObject({ views: 2, dwellMs: 15_000, depth: 100, engaged: true });
			expect(pricing).toMatchObject({ views: 1, dwellMs: 3000, depth: 45, engaged: false });
		});
	});
});
The mutants and the harness controlsControls run before any ratio is trusted
mutants.mjs
/* Mutants for the contract run's implementation. Each breaks exactly one named
 * rule of the contract, so a survivor points at a clause no suite pinned rather
 * than at a line. Mutants marked `restores` put back a decision the instruction
 * run actually made without the contract: the strongest single check, because
 * that behaviour is known to be something an agent will really write. */
export const mutants = [
	{ id: 'unviewed pages get summaries', rule: 'R1', restores: true,
		why: 'Every event opens a page, not only a page_view.',
		from: "\t\tif (event.kind !== 'page_view') continue;\n", to: '' },
	{ id: 'a trailing slash makes a second page', rule: 'R2', restores: true,
		why: '/about and /about/ are counted as two pages.',
		from: "\tif (path.endsWith('/')) return path.slice(0, -1);\n", to: '' },
	{ id: 'every trailing slash is stripped', rule: 'R2',
		why: 'A regex removes a run of slashes, the bug the blind writer predicted.',
		from: "return path.slice(0, -1);", to: "return path.replace(/\\/+$/, '') || '/';" },
	{ id: 'the root path loses its slash', rule: 'R2',
		why: 'The special case for / is gone, so / becomes an empty path.',
		from: "\tif (path === '/') return path;\n", to: '' },
	{ id: 'pages keep arrival order', rule: 'R3', restores: true,
		why: 'The sort runs on a copy, so pages come out in the order they were first viewed in the array.',
		from: "summaries.sort((a, b) => {", to: "[...summaries].sort((a, b) => {" },
	{ id: 'the latest page_view decides order', rule: 'R3',
		why: 'Order uses the most recent view instead of the earliest.',
		from: "} else if (event.at < existing.earliestViewAt) {", to: "} else if (event.at > existing.earliestViewAt) {" },
	{ id: 'ties are not broken by path', rule: 'R3',
		why: 'Two pages first viewed at the same moment stay in arrival order.',
		from: "\t\tif (a.path < b.path) return -1;\n\t\tif (a.path > b.path) return 1;\n", to: '' },
	{ id: 'views count every event but scroll', rule: 'R4',
		why: 'Heartbeats and leaves are counted as views.',
		from: "if (event.kind === 'page_view') {", to: "if (event.kind !== 'scroll') {" },
	{ id: 'the 30-minute cap is gone', rule: 'R5', restores: true,
		why: 'A laptop that slept with the tab open reports hours of reading.',
		from: "Math.min(event.durationMs, MAX_DWELL_PER_EVENT_MS)", to: "event.durationMs" },
	{ id: 'the cap applies to the total', rule: 'R5',
		why: 'The whole page is capped at 30 minutes instead of each slice.',
		from: "acc.dwellMs += Math.min(event.durationMs, MAX_DWELL_PER_EVENT_MS);", to: "acc.dwellMs = Math.min(acc.dwellMs + event.durationMs, MAX_DWELL_PER_EVENT_MS);" },
	{ id: 'negative durations subtract', rule: 'R5', restores: true,
		why: 'A negative slice reduces dwell instead of contributing nothing.',
		from: "Number.isFinite(value) && value >= 0;", to: "Number.isFinite(value);" },
	{ id: 'an infinite duration becomes the cap', rule: 'R5',
		why: 'Infinity is treated as a long slice, the bug the blind writer predicted.',
		from: "return typeof value === 'number' && Number.isFinite(value) && value >= 0;", to: "return typeof value === 'number' && !Number.isNaN(value) && value >= 0;" },
	{ id: 'page_view durations count', rule: 'R5',
		why: 'A duration on any kind but scroll adds to dwell.',
		from: "if (event.kind === 'heartbeat' || event.kind === 'page_leave') {", to: "if (event.kind !== 'scroll') {" },
	{ id: 'depth is not clamped at 100', rule: 'R6', restores: true,
		why: 'A depth of 140 is reported as 140.',
		from: "Math.min(100, Math.max(0, depth))", to: "Math.max(0, depth)" },
	{ id: 'depth rounds to nearest', rule: 'R6', restores: true,
		why: '74.6 becomes 75 and crosses a milestone the reader never reached.',
		from: "return Math.floor(clamped);", to: "return Math.round(clamped);" },
	{ id: 'an infinite depth is usable', rule: 'R6',
		why: 'Infinity clamps to 100 instead of being ignored.',
		from: "return typeof value === 'number' && Number.isFinite(value);\n}", to: "return typeof value === 'number';\n}" },
	{ id: 'heartbeat depth counts', rule: 'R6',
		why: 'A depth on any kind but page_view is read.',
		from: "if (event.kind === 'scroll' || event.kind === 'page_leave') {", to: "if (event.kind !== 'page_view') {" },
	{ id: 'the last depth wins', rule: 'R9',
		why: 'Depth follows arrival order instead of taking the greatest.',
		from: "if (depth > acc.depth) {", to: "if (true) {" },
	{ id: 'a milestone needs to be passed, not reached', rule: 'R7',
		why: 'A reader exactly at 50 is not credited with 50.',
		from: "ALL_MILESTONES.filter((m) => m <= acc.depth)", to: "ALL_MILESTONES.filter((m) => m < acc.depth)" },
	{ id: 'either signal means engaged', rule: 'R8', restores: true,
		why: 'The instruction run’s rule: time OR depth, instead of both.',
		from: "acc.dwellMs >= 10_000 && acc.depth >= 50", to: "acc.dwellMs >= 10_000 || acc.depth >= 50" },
	{ id: 'engaged needs 15 seconds', rule: 'R8', restores: true,
		why: 'The instruction run’s threshold instead of the contract’s.',
		from: "acc.dwellMs >= 10_000 && acc.depth >= 50", to: "acc.dwellMs >= 15_000 && acc.depth >= 50" },
	{ id: 'exactly ten seconds is not enough', rule: 'R8',
		why: 'The dwell threshold is exclusive.',
		from: "acc.dwellMs >= 10_000 && acc.depth >= 50", to: "acc.dwellMs > 10_000 && acc.depth >= 50" },
	{ id: 'the input events are rewritten', rule: 'R10',
		why: 'Normalizing a path writes it back onto the caller’s event.',
		from: "const path = normalizePath(event.path);", to: "const path = ((event as { path: string }).path = normalizePath(event.path));" },
	{ id: 'an empty read returns a phantom page', rule: 'R11',
		why: 'Nothing to read still produces a summary.',
		from: "\treturn summaries;\n}", to: "\treturn summaries.length ? summaries : [{ path: '/', views: 0, dwellMs: 0, depth: 0, milestones: [], engaged: false }];\n}" }
];
mutate.mjs
export function mutate({ subject, suites, mutants }) {
	const original = readFileSync(subject, 'utf8');
	const results = { controls: {}, mutants: [] };
	const clean = runOnce(original, suites);
	const broken = runOnce(original + '\nthrow new Error("negative control");\n', suites);
	const brokenFn = runOnce(
		original.replace(/export function summarizeReading\(/, 'export function summarizeReading__real('),
		suites
	);
	results.controls.clean = clean;
	results.controls.moduleThrows = broken;
	results.controls.exportMissing = brokenFn;
	const columns = Object.keys(suites);
	const cleanOk = clean.ran && columns.every((c) => clean.columns[c].state === 'survived');
	const brokenOk = broken.ran && columns.every((c) => broken.columns[c].state !== 'survived');
	results.controls.passed = cleanOk && brokenOk;
	console.log(`negative control: clean ${cleanOk ? 'GREEN in every column' : 'NOT GREEN'} · broken ${brokenOk ? 'RED in every column' : 'NOT RED'}`);
	if (!results.controls.passed) {
		console.log('control FAILED — no ratio will be reported');
		return results;
	}
	// …then one fresh run per mutant
}
05 / When it loses

A blind suite can never know more than its document.

21/24 is a good suite. The agent that wrote it from the code covered every rule the code’s comments named, and the three mutants it missed are gaps any reviewer could fix in minutes. On this code, the contract happened to be complete, so a writer who had only the contract was at no disadvantage.

Change that condition and the result reverses. If defects are fixed by hand and the fixes never make it into the document, a blind suite cannot protect them, while a suite written from the code already pins them. The owner ran the same procedure on a separate, multi-package project and saw exactly that; that record is kept outside this repository, so this page cannot show it. What this page can show is the ceiling: a blind suite can never know more than its document. Below that ceiling the writer still matters. Both blind suites here came from the same complete contract and scored 24/24 and 23/24; the second missed one mutant because its heartbeat helper could not carry a depth.

What spec-first cannot do

  • Exceed its document. A rule missing from the contract is missing from the suite, and nothing will say so.
  • Test a discipline. “This module never calls the network” is a review property or an import rule, not a test.
  • Settle a decision nobody made. Where behavior is a real choice, a test can pin it only once someone writes the choice down.
  • Survive a stale document. Every correction has to go into the contract, then the test. Never the test alone.
06 / Make the call

Pay for a contract where being wrong is expensive.

The question is not which option is best. It is what a wrong reading would cost here, and whether anything outside the author’s head can notice one.

Visible, reversible, used once

Let the agent test its own work.

A throwaway script run on a copy has an outside check already: you, looking at the result. Revisit the moment the output feeds something you do not look at.

Existing code, no document, needs a safety net

Write tests from the code, and read the survivors.

It pins current behavior, which is what a refactor needs. Mutation-test it, because tests whose names promise more than their inputs can observe are common. Write down whatever you find the code is supposed to do.

New behavior an agent will implement, and it matters

Write the contract first, and derive the tests blind.

Give the writer no way to see the code, run the suite yourself, triage failures as findings, and score it by breaking the code. Revisit when fixes start landing that the contract does not record.

A module whose fixes outran its document

Keep both suites until the contract catches up.

Write each correction into the contract, derive again, and retire the author’s tests for those fixes only once a blind suite kills the mutants that restore them.

Make the call

Where should the truth come from this time?

Each situation has a different cost of being wrong. Choose before you compare.

An agent implemented how a mid-month plan change is billed, wrote forty tests, and reports every one passing. The rules exist only in the ticket’s two sentences. Money moves on this code.

Your decision
07 / Keep the reason

Keep where the truth came from.

When you make this choice on real work, leave a note next to the tests. A suite with no record of its source of truth is indistinguishable from one whose author graded itself.

Why
The dashboard’s numbers drive product decisions, and an agent writes the code.
What
A contract written before the code, hashed and dated, and a suite derived from it by a writer that could only write.
Constraint
The suite can never know more than the contract, and it is scored by mutants killed out of mutants shown to be observable, after controls held. Not a test count.
Fallback
Where the contract lags the code, keep the tests written from the code for those fixes until the contract records them.
Reconsider when
A fix lands that the contract does not record, or a survivor points at a rule the document never stated.
Take the idea with you

Explain the choice without saying “spec before code.”

“Tests written by whoever wrote the code can only agree with them. Write what the code should do somewhere the code did not write it, and let someone who cannot see the code turn that into tests.” That is the whole choice. The name is what you call it in a review.

Before moving on, explain three things without the name: why twelve green tests said nothing about the contract, why the suite written from the code missed a promise with no line behind it, and why a blind suite still needs its mutation score before you trust it. The closed circle, the information barrier, and mutation testing each get their own lesson in this area; they are not written yet.

Connections to follow nextRelated lessons
  • Enforcement layer runs a check like this where nobody has to remember it.
  • Architecture as rules writes the structure down as a declaration a check can hold, the same move for imports instead of behavior.
  • Notes protocol keeps the reasoning behind a contract rule where the next person can find it.

Pick one function an agent wrote for you this week. Write its rules down in five numbered sentences before rereading the code, then check whether its tests would fail if each rule broke.

Back to working with agents →