> ## Documentation Index
> Fetch the complete documentation index at: https://docs.highailabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Notebooks & Daily Stories — Behavior Spec

> Every behavior the Notebooks feature must have, and the exact test that proves each one — generation from orders and stash, streaming states, journal and photos, archive and delete, and the Daily Stories quarantine.

## What this feature is

A **notebook** is the document High IQ builds about what a person actually bought
and smoked. It is assembled from their own recorded rows — an order's line items,
or a hand-picked set of stash jars — enriched with an allowlisted read of the
public strain database. One is created automatically when a Pro member commits an
order with the notebook toggle on, and one can be requested by hand from Order
Detail or by picking jars from the stash. While a notebook builds, the person
watches section-by-section progress and can cancel; if it fails they can retry the
whole thing or just the failed sections, and each attempt publishes a new,
immutable revision without destroying the previous one. Every notebook carries a
free-tier **journal** — a per-strain rating, a note, and photos — and can be
shared, archived, or deleted from its detail screen. **Daily Stories** is a
notebook-adjacent surface that is deliberately switched off end to end; it is
specified here as a quarantine, not as a feature.

<Note>
  The live generation pipeline contains **no AI calls**. A notebook is a
  deterministic, template-assembled document: Convex `prepareAndStartReportV2` →
  `POST /api/v1/notebooks/report-v2/source-batch` (a research preflight, no model)
  → the Trigger.dev `notebook-report-v2` task → a Convex webhook that promotes the
  result. Model enrichment is pinned off in three independent places. Tier C here
  proves a distributed deterministic pipeline, not model output.
</Note>

## The behavior contract

### Creating a notebook from an order

| Behavior                                                                                                                                                    | Verified by            | Tier(s) | Where (file basename)                                                                |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------- | ------------------------------------------------------------------------------------ |
| Committing an order with the notebook toggle on opens a readable notebook shell before any generation work finishes.                                        | NB-043                 | A       | `instantNotebookShell.test.ts`                                                       |
| An order gets at most one shell — a retried effect reuses it, a deleted order produces none, and deleting the shell cancels the effect queued for it.       | NB-024, NB-044, NB-045 | A       | `instantNotebookShell.test.ts`                                                       |
| A generation effect stranded mid-flight is recovered by reconciliation rather than left pending forever.                                                    | NB-046                 | A       | `notebooks.featuretests.test.ts`                                                     |
| An order whose strains are still being researched reports `waiting`, not failure, and the order screen reads that as progress rather than an error.         | NB-030, NB-117         | A, B    | `notebooks.featuretests.part2.test.ts`, `useOrderDetailReport.featuretests.test.tsx` |
| Research completing unblocks a waiting notebook source so generation proceeds on its own.                                                                   | NB-047                 | A       | `notebooks.featuretests.test.ts`                                                     |
| A free member committing an order gets the order and no notebook — the intent is dropped rather than creating a shell they cannot open.                     | NB-042                 | A       | `orderPostSave.test.ts`                                                              |
| Tapping "Generate" on Order Detail sends `{orderId}` exactly once per tap, and asking for a notebook that does not exist returns null rather than throwing. | NB-117, NB-010         | A, B    | `useOrderDetailReport.featuretests.test.tsx`, `notebooks.test.ts`                    |

### Creating a notebook from stash

| Behavior                                                                                                                                                                      | Verified by            | Tier(s) | Where (file basename)                |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------- | ------------------------------------ |
| A stash notebook dedupes by the fingerprint of the chosen jars, so the same selection never produces two notebooks.                                                           | NB-025                 | A       | `instantNotebookShell.test.ts`       |
| A stash request is Pro-only at the server and writes nothing for a signed-out caller; when generation is unavailable it still returns the openable shell instead of an error. | NB-026, NB-027, NB-028 | A       | `instantNotebookShell.test.ts`       |
| The stash picker refuses a selection past the subject cap and explains why, while a selection exactly at the cap submits.                                                     | STASH-241              | B       | `StashSelection.test.tsx`            |
| The stash-select footer never overlaps the list while stash is still loading.                                                                                                 | NB-120                 | B       | `NotebookStashSelectScreen.test.tsx` |
| Neither generation entry point can name another person's order or stash rows as its source.                                                                                   | NB-023                 | A       | `notebooks.featuretests.test.ts`     |

### Who may generate, and how often

| Behavior                                                                                                                                                               | Verified by    | Tier(s) | Where (file basename)                                                  |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------- | ---------------------------------------------------------------------- |
| A free member asking for an order notebook is rejected with a **client-detectable `PAYWALL` code**, not a generic failure.                                             | NB-029         | A       | `notebooks.featuretests.part2.test.ts`                                 |
| The order screen classifies a Pro-or-historical rejection and a historical-import rejection as paywalls, and preserves a thrown `PAYWALL` code through the order path. | NB-116         | B       | `useOrderDetailReport.featuretests.test.tsx`                           |
| A non-paywall failure — a generic error or a network throw — must never raise the paywall sheet.                                                                       | NB-117         | B       | `useOrderDetailReport.featuretests.test.tsx`                           |
| A second generate inside the 30-second cooldown is rejected.                                                                                                           | NB-036         | A       | `notebooks.featuretests.part2.test.ts`                                 |
| One account may run 5 generations a day and 30 a month; the global daily ceiling of 50 holds atomically under concurrency.                                             | NB-037, NB-038 | A       | `notebooks.featuretests.part2.test.ts`, `paidWorkReservations.test.ts` |
| Staging is unmetered but still records usage, and reservation cleanup refunds without dropping history.                                                                | NB-039, NB-040 | A       | `paidWorkReservations.test.ts`                                         |
| Paid work is refunded when the research preflight throws, and settled exactly once when it succeeds.                                                                   | NB-034, NB-035 | A       | `notebooks.featuretests.part2.test.ts`                                 |
| A per-source kill switch can disable order or stash generation independently.                                                                                          | NB-041         | A       | `notebooks.featuretests.part2.test.ts`                                 |

### Watching a notebook build

| Behavior                                                                                                                                                                                 | Verified by            | Tier(s) | Where (file basename)                           |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------- | ----------------------------------------------- |
| The generation hook subscribes to sections and runtime with `{notebookId}`, skips both while auth is unresolved or the id is absent, and reports loading until both resolve.             | NB-104                 | B       | `useNotebookGeneration.featuretests.test.tsx`   |
| Progress reads expose status and counts only — never prose, error text, or the identity of the underlying run.                                                                           | NB-012, NB-051, NB-052 | A       | `notebooks.test.ts`, `featureNotebooks.test.ts` |
| The detail screen renders each of its five generation states, and a malformed payload fails closed rather than rendering a blank notebook.                                               | NB-114, NB-115         | B       | `notebookV2.adapter.test.ts`                    |
| The global progress accessory treats open, dismiss, and cancel as three independent intents.                                                                                             | NB-118                 | B       | `NotebookProgressAccessory.test.tsx`            |
| A notebook's status reports `cancellable` truthfully, including through the cancel-pending window.                                                                                       | NB-009                 | A       | `notebooks.featuretests.test.ts`                |
| Cancelling keeps the sheet up while the cancel is pending, and restores it if the cancel fails.                                                                                          | NB-119                 | B       | `useNotebooksScreenState.test.ts`               |
| Exactly one cancel wins the three-phase barrier; a rollback restores the controls without resurrecting a hung run; a fresh attempt supersedes a confirmed cancellation.                  | NB-055, NB-056, NB-057 | A       | `notebookReportV2Promotion.test.ts`             |
| A run's progress stream surfaces `COMPLETED`, `FAILED`, and `CANCELED` terminal states with their messages, and reports a non-OK subscription as an error rather than a terminal status. | NB-126                 | B       | `runProgressStream.featuretests.test.ts`        |
| The run timeout guard aborts the in-flight request first and calls `onTimeout` exactly once, and a settled run can clear it so it never fires.                                           | NB-125                 | B       | `runTimeoutGuard.featuretests.test.ts`          |

### Failure, retry, and revisions

| Behavior                                                                                                                                                                                               | Verified by            | Tier(s) | Where (file basename)                                                    |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------- | ------- | ------------------------------------------------------------------------ |
| Retrying sections sends `{notebookId, sectionKeys:[key]}`, never reaches Convex without a notebook id, and flags a paywall rejection instead of reporting a bare failure.                              | NB-105                 | B       | `useNotebookGeneration.featuretests.test.tsx`                            |
| Starting and retrying both reject an unauthenticated caller, and retry is Pro-gated.                                                                                                                   | NB-048, NB-049         | A       | `featureNotebooks.test.ts`                                               |
| A build failure settles idempotently and increments the revision, and each promotion publishes exactly one immutable publication per revision.                                                         | NB-058, NB-059         | A       | `notebookReportV2Promotion.test.ts`, `notebooks.featuretests.test.ts`    |
| A terminal run lost in transit can be replayed exactly once.                                                                                                                                           | NB-061                 | A       | `notebooks.featuretests.part2.test.ts`                                   |
| Settling a terminal run requires a provider-proven run — a forged run id is rejected — and a callback arriving while the surface is disabled retires the run's identity without accepting its content. | NB-060, NB-062         | A       | `notebooks.featuretests.test.ts`, `notebooks.featuretests.part2.test.ts` |
| A stuck schedule exposes owner-safe recovery state and can be replayed, and every recovery read surface returns owner-safe, bounded pages.                                                             | NB-053, NB-054, NB-063 | A       | `notebooks.featuretests.test.ts`                                         |
| A notebook's terminal status can never regress to an earlier one.                                                                                                                                      | NB-022                 | A       | `notebooks.featuretests.test.ts`                                         |

### The notebooks list and the archive

| Behavior                                                                                                                                                                                                             | Verified by                    | Tier(s) | Where (file basename)                                           |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------- | --------------------------------------------------------------- |
| The default stream excludes archived notebooks, runs status and type filters on the declared index, keeps a cursor stable when a status changes mid-scroll, and projects out heavy `content` / `payloadJson` fields. | NB-001, NB-002, NB-003, NB-004 | A       | `notebooks.featuretests.test.ts`, `notebooks.test.ts`           |
| The archive stream returns only the caller's archives and respects an in-progress deletion.                                                                                                                          | NB-005, NB-006                 | A       | `notebooks.featuretests.test.ts`                                |
| The list hook calls exactly the four declared queries with their declared args, forwards caller filters, and paginates the archive with `{}` at 10 per page.                                                         | NB-100, NB-108                 | B       | `useNotebooks.featuretests.test.tsx`                            |
| Every query is skipped while Clerk auth is pending and for a signed-out session.                                                                                                                                     | NB-101                         | B       | `useNotebooks.featuretests.test.tsx`                            |
| Rows belonging to another Clerk user are dropped client-side, and an active generation with no owner-tagged row to explain it is refused.                                                                            | NB-102                         | B       | `useNotebooks.featuretests.test.tsx`                            |
| Switching directly from owner A to owner B fails the list closed rather than briefly showing A's notebooks.                                                                                                          | NB-113                         | B       | `notebooksOfflineState.test.ts`                                 |
| The list renders five distinct states — loading, empty, cold/offline, error, and data.                                                                                                                               | NB-112                         | B       | `NotebooksColdOffline.test.tsx`, `NotebooksEmptyState.test.tsx` |
| Signed-out pending reads soft-fail to an empty list, and the latest-notebook read returns null.                                                                                                                      | NB-011, NB-013                 | A       | `notebooks.test.ts`                                             |

### Opening, sharing, archiving, deleting

| Behavior                                                                                                                                                                                                         | Verified by                    | Tier(s) | Where (file basename)                                                            |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------- | -------------------------------------------------------------------------------- |
| Opening a notebook denies a foreign owner and a tombstoned row.                                                                                                                                                  | NB-008                         | A       | `notebooks.test.ts`                                                              |
| Opening records exactly one view per mount, waits for real data first, never records for preview data, and releases its guard so a failed view can be recorded next time.                                        | NB-111                         | B       | `useNotebookDetailLifecycle.featuretests.test.tsx`                               |
| A view is deduped server-side and schedules the achievement check once, is refused for a notebook being deleted, and the resulting count is the caller's own distinct notebooks read in O(1) rather than a scan. | NB-014, NB-015, NB-016, NB-017 | A       | `notebooks.featuretests.test.ts`, `achievements.test.ts`                         |
| Publication history requests `{notebookId, limit: 20}`, reads a historical revision by publication id only, and skips both reads when no notebook is selected.                                                   | NB-107                         | B       | `useNotebooks.featuretests.test.tsx`                                             |
| A publication limit outside 1..MAX is rejected, and a foreign notebook's publication is denied.                                                                                                                  | NB-067, NB-068, NB-069         | A       | `notebooks.featuretests.test.ts`, `notebookReportV2Promotion.test.ts`            |
| Sharing emits real notebook metadata once report data has loaded, and falls back to the generic payload only while it has not.                                                                                   | NB-124                         | B       | `useShareActions.featuretests.test.ts`                                           |
| Archiving requires ownership and mutates only the target notebook.                                                                                                                                               | NB-007                         | A       | `notebooks.featuretests.test.ts`                                                 |
| Deleting cascades journal entries, photos, the artifact ledger and publications; it denies a foreign owner; and it fails closed while a ledger-owned storage object still exists.                                | NB-018, NB-019, NB-020         | A       | `notebookJournal.test.ts`, `notebookArtifactLedger.test.ts`, `notebooks.test.ts` |
| Purging an account's notebooks drains in bounded foreign-key order.                                                                                                                                              | NB-021                         | A       | `notebooks.featuretests.test.ts`                                                 |
| Every action wrapper binds to its declared Convex function, forwards validator-shaped args verbatim, and refuses to reach Convex at all while the device is offline.                                             | NB-103                         | B       | `useNotebooks.featuretests.test.tsx`                                             |

### Journal and photos

| Behavior                                                                                                                                                                                                                                                                                   | Verified by            | Tier(s) | Where (file basename)                                      |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------- | ------- | ---------------------------------------------------------- |
| A journal entry validates against the full matrix of ratings, notes and link fields, and its subject key stays stable as the source strain gains identity.                                                                                                                                 | NB-074, NB-075         | A       | `notebookJournal.test.ts`                                  |
| Saving reconstructs `strainCanonical` only when the subject key is not already explained by slug or name, forwards every optional link field, never fires without a notebook id, and surfaces a server rejection as a failed result rather than a throw.                                   | NB-106                 | B       | `useNotebookJournal.featuretests.test.tsx`                 |
| Once deletion is active, writing an entry is blocked but deleting one is still allowed.                                                                                                                                                                                                    | NB-076                 | A       | `notebookJournal.test.ts`                                  |
| A long strain history truncates predictably instead of growing without bound.                                                                                                                                                                                                              | NB-077                 | A       | `notebooks.featuretests.test.ts`                           |
| A photo upload posts to the derived `.convex.site` route with the token in the header only and calls `attachPhoto` with validator-shaped metadata — and makes no request at all when the picker is cancelled or permission is denied, and never attaches when the route rejects the bytes. | NB-110                 | B       | `useJournalPhotoUpload.featuretests.test.tsx`              |
| The upload route ignores forged owner and storage headers, rejects a MIME/magic-byte mismatch before storing anything, and fails its quota and kill switch closed with uploads off by default in production.                                                                               | NB-078, NB-079, NB-080 | A       | `notebookPhotos.test.ts`                                   |
| Attaching is idempotent and cross-account isolated, and a metadata mismatch deletes only that exact pending blob.                                                                                                                                                                          | NB-081, NB-082         | A       | `notebookPhotos.test.ts`                                   |
| A blob whose compensation failed is durably retained rather than orphaned, and expired-upload cleanup makes fair progress under a backlog.                                                                                                                                                 | NB-083, NB-084         | A       | `notebooks.featuretests.test.ts`, `notebookPhotos.test.ts` |
| The add-photo tile appears only when every gate allows it, and disappears at the notebook cap, at the per-subject maximum, when the flag is off, or when the server says uploads are not allowed.                                                                                          | NB-121                 | B       | `NotebookJournal.test.tsx`                                 |

### Isolation, retirement, and the schema surface

| Behavior                                                                                                                                                                        | Verified by            | Tier(s) | Where (file basename)                                                 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------- | --------------------------------------------------------------------- |
| The generation webhook rejects a wrong or missing bearer in constant time, rejects a payload failing the callback schema, and permanently rejects a forged empty artifact plan. | NB-064, NB-065, NB-066 | A       | `notebookReportV2Promotion.test.ts`, `notebookArtifactLedger.test.ts` |
| Every planned media artifact reports `blocked / executor_unregistered_or_unapproved`, and cancelling an attempt cancels only that exact attempt.                                | NB-070, NB-073         | A       | `notebookArtifactLedger.test.ts`, `notebooks.featuretests.test.ts`    |
| Private media access denies a foreign owner before signing anything, caps signed-URL expiry, and fails closed without credentials.                                              | NB-071, NB-072         | A       | `notebookPrivateMediaAccess.test.ts`                                  |
| The archive upgrade leaves the V1 row byte-identical while writes are off, and with writes on produces a V2 publication.                                                        | NB-031, NB-032         | A       | `notebooks.test.ts`, `notebooks.featuretests.part2.test.ts`           |
| The V2 preparation short-circuits while writes are off, and the V1 section retry is a no-op while V1 writes are off.                                                            | NB-033, NB-050         | A       | `notebookReportV2.test.ts`, `featureNotebooks.test.ts`                |
| Drain evidence for the retired V1 path reports blockers while any V1 state remains, and deletion identity helpers resolve run ids and storage locators exactly.                 | NB-085, NB-094         | A       | `notebooks.featuretests.test.ts`                                      |
| Run events are purged on a 30-day retention with a per-batch cap, and all 24 notebook-domain tables declare an export policy and a deletion policy bound to declared indexes.   | NB-093, NB-095         | A       | `notebookRunEventRetention.test.ts`, `schema-surface.test.ts`         |

### Daily Stories — quarantined by design

| Behavior                                                                                                                                                                              | Verified by    | Tier(s) | Where (file basename)                        |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------- | -------------------------------------------- |
| Every daily-stories query and mutation rejects an unauthenticated caller, and one person can neither read nor record a view on another person's story.                                | NB-086, NB-088 | A       | `dailyStories.test.ts`                       |
| All four quarantine lifecycles stay hidden from the story's own owner, and marking a foreign or quarantined story viewed is a silent no-op.                                           | NB-087, NB-090 | A       | `dailyStories.test.ts`                       |
| The month-range read handles a year boundary and returns nothing for a non-positive month count.                                                                                      | NB-089         | A       | `dailyStories.test.ts`                       |
| The longest-view-streak read comes from the projection and stays O(1) in view count.                                                                                                  | NB-091         | A       | `notebooks.featuretests.test.ts`             |
| The nightly cron creates nothing while no story executor is selected.                                                                                                                 | NB-092         | A       | `dailyStories.test.ts`                       |
| The week hook sends seven Monday-anchored date keys plus today's, anchors to Monday even on a Sunday, and skips both subscriptions when the flag is off or auth is unresolved.        | NB-109         | B       | `useDailyStories.featuretests.test.tsx`      |
| The story viewer writes at most twice — once on open and once on completion — collapses both into one write for a single-page story, and writes nothing when there is no ready story. | NB-123         | B       | `StoryViewerContainer.featuretests.test.tsx` |
| The calendar's skeleton row count is derived from the month rather than a fixed guess.                                                                                                | NB-122         | B       | `dailyStoriesHistorySkeleton.test.ts`        |

## What the live tier proves end-to-end

Tier C signs in as a real Clerk identity, drives the deployed staging Convex
deployment and the production Hono API, waits on real Trigger.dev runs, and
cleans up after itself.

* **A stash notebook completes the whole pipeline to `ready`, and the research layer really arrives** — the shared run every other case reads from, guarding against a silently degraded notebook that looks fine and contains nothing (NB-200, NB-201).
* **Committing an order auto-generates a notebook that reaches `ready`**, and one whose strains are still being researched waits and then proceeds rather than failing (NB-202, NB-203).
* **Manual order generation works end to end**, and a free account is denied at the server for every generation entry point with the enforcement state asserted first so the case cannot pass vacuously (NB-204, NB-205).
* **Double-tapping generate yields exactly one notebook and one Trigger run**, cancelling mid-generation confirms and leaves no hung run, and a cancel racing the terminal callback resolves to exactly one winner (NB-207, NB-208, NB-209).
* **Retrying a failed notebook produces a new revision while the previous publication stays intact**, and upgrading an archive publishes V2 while keeping the archive readable (NB-210, NB-213).
* **Journal and photo round-trips work against the real backend** — including a real JPEG through the `.convex.site` route, a PNG mislabelled as JPEG being rejected, and the photo cap enforced against the real quota tables (NB-220, NB-221, NB-222, NB-223).
* **Deleting a notebook cascades on staging** down to the storage blob returning 404, and cross-user isolation, unauthenticated rejection, and validator-drift replay all hold on the deployed validators (NB-224, NB-231, NB-232, NB-234).
* **The retired surfaces stay retired on the real hosts**: the archived SSE route returns 410, the `/api/v1/reports/*` alias behaves identically, source-batch answers a missing internal secret with 404 rather than 401, every planned artifact reports `blocked`, and the daily-stories cron and webhook both produce nothing (NB-214, NB-216, NB-217, NB-218, NB-228, NB-229, NB-230).

## Deliberate gaps & conditions

**Daily Stories is withheld on purpose, on both sides.** There is no producer: the
nightly cron returns `daily-story-executor-unselected` and no story-insert path
exists outside test seeding. The server quarantine is one named constant
(`DAILY_STORY_CUSTOMER_VISIBILITY_ENABLED = false as const`) and the client flag is
hard-disabled to match, so the header strip, calendar, viewer and deep link are
invisible rather than live-and-permanently-empty. NB-229 asserts the guarantee
that follows: a story seeded directly into the table as owned and `ready` is
**still withheld from every customer read**. The two halves must be lifted in the
same change — lifting one alone reproduces the shipped defect — and a coupling test
pins exactly that. NB-229 is a plain passing test, not a pin; whoever lifts the
quarantine has to rewrite it.

**Media artifacts are structurally unreachable, and that is the specified state.**
The artifact registry's only entry is a `candidate`, and executor selection filters
on `selected`, so every planned artifact is immediately `blocked`. The artifact
routes are separately quarantined in the client and their CTAs are now hidden
rather than dead-ending. The ledger's `executorTaskId` is deliberately kept: it is
read as executor identity by the anti-forgery check, so removing it would silently
disarm the guard.

**Two cases cannot be written as specified.**

| Case                                                                                    | Why                                                                                                                                                                                                              |
| --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| NB-206 — a historical-import member may generate only their historical order's notebook | Needs a Gmail-historical-import-committed order **and** the one-time archive entitlement. No Tier C fixture can create that pair, so the case documents the gap instead of asserting it.                         |
| NB-211 — paid-work usage recorded on staging even though unmetered                      | No reader exists for `paidWorkUsageBuckets` / `paidWorkGlobalUsageBuckets`. Asserting it would require new production code written only for a test. The metering half is covered at Tier A by NB-036 and NB-037. |

**One case is blocked, not deferred.** NB-212 (paid work refunded when the research
preflight fails) would require temporarily invalidating the internal preflight
secret on **shared** staging, which would break every other suite running at the
time. Tier A NB-034 covers the same contract deterministically.

**Two cases are proven in another area's suite.** Order-commit paywall behavior is
owned by the Orders suite (NB-042 → ORD-074), and the notebook-view achievement
projection by the Achievements Tier C suite (NB-225 → ACH-145). Build only the
primary.

## Where to extend

A new backend rule — a validator, an ownership check, a scheduled effect — gets a
case in `apps/mobile/convex/tests/notebooks.featuretests.test.ts`, or
`notebooks.featuretests.part2.test.ts` if it touches generation, paid work, or
promotion. A new hook argument or screen state gets a co-located
`*.featuretests.test.tsx` next to its source, or a jest-expo suite under
`apps/mobile/test/ui/features/`. Anything that only fails on a real deployment —
JWT verification, an outbound fetch from an action, pipeline completion — goes in
`apps/mobile/test/integration/suites/notebooks.integration.test.ts` and must be
scoped to its own test user.

See [Mobile Feature Test Platform](/planning/testing/feature-test-platform) for
tier selection, naming, and the Tier C isolation rules.
