> ## 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.

# Stash — Behavior Spec

> Every behavior the Stash feature must have and the exact test that proves each one — adding and editing jars, emptying versus archiving, bulk actions, history, the stats cards, and the order-to-stash merge contract.

## What this feature is

The **stash** is the shelf: what a person currently owns, in the units they own it
in. Jars arrive three ways — added by hand from a strain page, added in bulk from an
order's line items, or merged in automatically when an order is confirmed. Each jar
is a line keyed by strain, product type and unit, so a second eighth of the same
flower tops up the existing line while a cartridge of the same strain stays
separate. From the Stash tab a person adjusts quantities, marks a jar **empty** when
they have finished it, **archives** one they no longer want to track, or
**permanently deletes** a hand-entered mistake; the same actions work across a
multi-select. Every one of those writes lands in a searchable **history timeline**,
and the timeline plus the current shelf feed the Stats cards — consumption velocity,
restock prediction, cost per gram, and the stash hero forecast.

<Warning>
  **Archiving is not consumption.** Emptying and archiving make the identical state
  change — quantity 0, inactive — but they record different events, and only
  `stash_marked_empty` counts toward consumption analytics. Archiving a mis-entered
  28 g line used to report 28 g smoked; it no longer does.
</Warning>

## The behavior contract

### Adding a jar

| Behavior                                                                                                                                                                                                             | Verified by                     | Tier(s) | Where (file basename)                                 |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------- | ----------------------------------------------------- |
| Adding a strain that is not on the shelf creates a new line with its defaults, while the same strain in the same product type and unit tops up the existing line.                                                    | STASH-001, STASH-002            | A       | `stash.test.ts`                                       |
| Different product types of one strain stay separate lines, and so do different units.                                                                                                                                | STASH-003, STASH-004            | A       | `stash.test.ts`, `stash.featuretests.test.ts`         |
| Adding a strain that is on the shelf but inactive reactivates that line with the new quantity **replacing** the old, not summed onto it.                                                                             | STASH-005                       | A       | `stash.featuretests.test.ts`                          |
| A quantity of zero, negative, `NaN` or `Infinity` is rejected.                                                                                                                                                       | STASH-006                       | A       | `stash.featuretests.test.ts`                          |
| Line matching prefers strain id, then slug, then canonical name, then raw name; ties break to the newest purchase date; and canonical matching ignores punctuation and case, so "GG #4" and "gg4" are the same line. | STASH-007, STASH-008, STASH-009 | A       | `stash.featuretests.test.ts`                          |
| An unauthenticated add is rejected.                                                                                                                                                                                  | STASH-018                       | A       | `stash.featuretests.test.ts`                          |
| The add form sends exactly the validator's shape and omits blank optional fields instead of sending empty strings.                                                                                                   | STASH-200                       | B       | `StashAdd.test.tsx`                                   |
| A double tap in one frame submits once.                                                                                                                                                                              | STASH-201                       | B       | `StashAdd.test.tsx`                                   |
| Quantity 0 blocks submit with an explanatory toast, a mutation failure surfaces its message while keeping the sheet and its state open, and switching product type reseeds the quantity and unit defaults.           | STASH-202, STASH-203, STASH-204 | B       | `StashAdd.test.tsx`, `addToStashForm.helpers.test.ts` |
| The add route renders a not-found state without strain params, and shows a locked preview instead of the form for an account without entitlement.                                                                    | STASH-205, STASH-206            | B       | `StashAdd.test.tsx`                                   |

### Adding a whole order's worth at once

| Behavior                                                                                                                                                                                  | Verified by                       | Tier(s) | Where (file basename)                                            |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------- | ---------------------------------------------------------------- |
| A bulk add keeps distinct product types separate and excludes count units from the gram total it reports.                                                                                 | STASH-066, STASH-067              | A       | `stash.test.ts`                                                  |
| A non-positive quantity is rejected **naming the offending strain**, and a negative price is rejected.                                                                                    | STASH-068, STASH-069              | A       | `stash.featuretests.test.ts`                                     |
| A price supplied with a bulk item is persisted on the new-line, revive and merge paths alike, and a bulk add reactivates inactive lines with a default source of `bulk_import`.           | STASH-070, STASH-073, STASH-179   | A       | `stash.featuretests.test.ts`, `stash.featuretests.part2.test.ts` |
| Two items in one batch that resolve to the same line accumulate serially onto that one line.                                                                                              | STASH-071                         | A       | `stash.featuretests.test.ts`                                     |
| A whole batch schedules exactly **one** achievement re-evaluation and exactly **one** history-event mutation, however many items it carries; a single-item write still schedules its own. | STASH-072, STASH-177a, STASH-177c | A       | `stash.featuretests.test.ts`                                     |
| A bulk add followed by a retire still writes one history event per item — batching the transaction never collapses the record.                                                            | STASH-177b                        | A       | `stash.featuretests.test.ts`                                     |

### Changing quantities

| Behavior                                                                                                                                                                                                                               | Verified by                       | Tier(s) | Where (file basename)                         |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------- | --------------------------------------------- |
| Setting a quantity to zero deactivates the line and a positive quantity reactivates it, while a negative, `NaN` or `Infinity` quantity is rejected.                                                                                    | STASH-020, STASH-021              | A       | `stash.test.ts`, `stash.featuretests.test.ts` |
| Emptying a jar by editing its quantity to zero is visible to consumption analytics — via the single-item path and via the batch path alike.                                                                                            | STASH-022, STASH-022b, STASH-022c | A       | `stash.featuretests.test.ts`                  |
| Changing the unit at the same time computes the gram delta across both units, and editing another person's jar is rejected.                                                                                                            | STASH-023, STASH-024              | A       | `stash.featuretests.test.ts`                  |
| A batch quantity update validates the **whole batch before the first write** so one bad row cannot half-apply it, deactivates a row set to zero exactly as the single-item path does, and silently skips rows the caller does not own. | STASH-054, STASH-055, STASH-056   | A       | `stash.featuretests.test.ts`                  |
| Reactivating rejects a negative or `NaN` quantity, accepts zero as a live line with no stock, and is rejected across users.                                                                                                            | STASH-037, STASH-038, STASH-039   | A       | `stash.featuretests.test.ts`                  |
| The optimistic update for a quantity edit patches the row in place without changing which lists it belongs to, and keeps the existing unit when the mutation omits one.                                                                | STASH-208                         | B       | `useStash.featuretests.test.tsx`              |

### Consuming

| Behavior                                                                                                                                                                                                      | Verified by                                | Tier(s) | Where (file basename)                         |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ------- | --------------------------------------------- |
| Consuming reduces the quantity, consuming the last of it deactivates the line and records it as emptied, and consuming more than is on the shelf clamps to zero while recording only the weight that existed. | STASH-025, STASH-026, STASH-027            | A       | `stash.test.ts`                               |
| Consuming a count unit adds no gram delta and throws rather than guessing if a different unit is named; cross-unit consumption converts through grams and never drifts below zero on repeat.                  | STASH-028, STASH-029, STASH-030, STASH-031 | A       | `stash.test.ts`, `stash.featuretests.test.ts` |
| A consumption of zero, negative or `NaN` is rejected, and consuming from another person's jar is rejected.                                                                                                    | STASH-032, STASH-033                       | A       | `stash.featuretests.test.ts`, `stash.test.ts` |

### Emptying, archiving, and permanent deletion

| Behavior                                                                                                                                                                                                             | Verified by                                | Tier(s) | Where (file basename)                                   |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ------- | ------------------------------------------------------- |
| Emptying a jar zeroes it, deactivates it, records a negative gram delta, and is idempotent when the jar is already empty.                                                                                            | STASH-034, STASH-035                       | A       | `stash.featuretests.test.ts`                            |
| Emptying and archiving share one write path and make the identical state change, but record different event types.                                                                                                   | STASH-036                                  | A       | `stash.featuretests.test.ts`                            |
| **Archiving is never counted as consumption** — the archived grams do not appear in consumption analytics.                                                                                                           | STASH-036b                                 | A       | `stash.featuretests.test.ts`                            |
| Permanent deletion removes the grouped hand-entered rows **and** their owner-matching history events, normalizing duplicate ids into one deletion.                                                                   | STASH-057, STASH-058                       | A       | `stash.test.ts`                                         |
| An empty batch or one over the 50-id cap is rejected with no writes; mixed ownership and a missing id are both rejected before anything is deleted.                                                                  | STASH-059, STASH-060, STASH-061            | A       | `stash.test.ts`                                         |
| Order-backed rows cannot be permanently deleted and a legacy row with no recorded source fails closed; a permitted deletion decrements the lifetime item counter but leaves the order's own upload event in history. | STASH-062, STASH-063, STASH-064, STASH-065 | A       | `stash.featuretests.test.ts`, `achievements.test.ts`    |
| The UI blocks permanent delete for an order-backed card with an explanatory toast, and a declined confirmation deletes nothing.                                                                                      | STASH-215, STASH-216                       | B       | `StashSelection.test.tsx`, `useStashSelection.test.tsx` |

### Bulk actions from the selection bar

| Behavior                                                                                                                                                         | Verified by          | Tier(s) | Where (file basename)                                   |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | ------- | ------------------------------------------------------- |
| A bulk retire empties every owned, active id in the selection, skipping foreign and already-inactive ids silently rather than failing the batch.                 | STASH-050, STASH-051 | A       | `stash.featuretests.test.ts`                            |
| A 500-id batch completes — there is no cap on this path today, and the case records that ceiling.                                                                | STASH-052            | A       | `stash.featuretests.test.ts`                            |
| A duplicated id in one batch is processed **once**, so one jar never produces two events or a doubled gram delta.                                                | STASH-053            | A       | `stash.featuretests.test.ts`                            |
| A grouped card routes to the bulk mutation while a single card routes to the single-item one, and select-all expands to every underlying stash id exactly once.  | STASH-214, STASH-219 | B       | `useStashSelection.test.tsx`, `StashSelection.test.tsx` |
| The bulk confirmation dialog appears only at three or more selected cards.                                                                                       | STASH-218            | B       | `StashSelection.test.tsx`                               |
| A failed bulk keeps the selection intact and offers a retry that works.                                                                                          | STASH-217            | B       | `useStashSelection.test.tsx`                            |
| The optimistic update for marking empty removes the row from all three lists at once, and every optimistic update is a no-op when the query is not in the store. | STASH-207, STASH-209 | B       | `useStash.featuretests.test.tsx`                        |

### Reading the shelf

| Behavior                                                                                                                                                                                                                   | Verified by                                  | Tier(s) | Where (file basename)                                                                                          |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------- |
| Stats total grams and count by product type, excluding count units from grams; a millilitre counts as one gram, and the stash stats and the restock forecast agree on that.                                                | STASH-017, STASH-081, STASH-082, STASH-083   | A       | `stash.test.ts`, `stash.featuretests.test.ts`                                                                  |
| The low-stock list honours its threshold, excludes count units, and sorts ascending.                                                                                                                                       | STASH-084, STASH-147                         | A       | `stash.featuretests.test.ts`, `stash.featuretests.part2.test.ts`                                               |
| Fetching one jar returns not-found for a missing id and forbidden for a foreign one, and looking a strain up by name is exact-match, serves only a **live** line deterministically, and returns null when nothing is held. | STASH-087, STASH-088, STASH-088b, STASH-088c | A       | `stash.featuretests.test.ts`                                                                                   |
| The stash read honours `activeOnly` and never returns another person's lines, and per-strain purchase stats return null with no purchases, include inactive purchases newest-first, and stay isolated per account.         | STASH-080, STASH-089, STASH-090, STASH-091   | A       | `stash.featuretests.test.ts`                                                                                   |
| The list aggregates multi-product lines into a gram total when every line converts, shows the primary row's quantity when units are incompatible, and counts low stock with the shared `@tiwih/constants` table.           | STASH-211, STASH-212, STASH-213              | B       | `useStashItems.test.ts`, `useStashItems.featuretests.test.ts`                                                  |
| An empty stash renders its empty state only after the query resolves — never as a flash of "nothing here" while loading — and sort chips change the field and toggle direction on a repeat tap.                            | STASH-220, STASH-221                         | B       | `StashColdOffline.test.tsx`, `StashSelection.test.tsx`                                                         |
| Strain enrichment issues one lookup per unique strain identity, still renders the row when that lookup fails, and filters out rows belonging to another Clerk user or predating a fixture reset.                           | STASH-231, STASH-232, STASH-233, STASH-234   | B       | `useStashWithStrainDetails.observers.test.tsx`, `StashSelection.test.tsx`, `useStashWithStrainDetails.test.ts` |
| The Update Stash screen splits active from inactive rows, counts each tab, confirms before removing, and selects the right tab for a prefilled id.                                                                         | STASH-228, STASH-229, STASH-230              | B       | `StashUpdate.test.tsx`                                                                                         |
| Every stash unit converts identically across all four conversion tables, with a millilitre as one gram and count units as null.                                                                                            | STASH-243, STASH-244                         | B       | `stash.featuretests.test.ts` (`@tiwih/constants`)                                                              |

### History and the timeline

| Behavior                                                                                                                                                                                                                   | Verified by                       | Tier(s) | Where (file basename)                                            |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------- | ---------------------------------------------------------------- |
| Event-type paging advances its cursor without duplicates or skipped events, plain paging returns a usable cursor with an honest `isDone`, and the search cursor reaches matches sitting behind pages of non-matching rows. | STASH-100, STASH-101, STASH-102   | A       | `stashEvents.test.ts`, `stash.featuretests.part2.test.ts`        |
| Order upload events are enriched with their order's items, and an order that has since been deleted yields an empty item list rather than an error.                                                                        | STASH-103, STASH-104              | A       | `stash.featuretests.part2.test.ts`                               |
| A whitespace-only search is treated as no search, a real search matches order name and both strain names, and history is isolated per account.                                                                             | STASH-105, STASH-106, STASH-109   | A       | `stash.featuretests.part2.test.ts`                               |
| Hitting the search scan cap keeps `isDone:false` with a live cursor, and an event-type page may legitimately come back empty while more pages remain.                                                                      | STASH-107, STASH-108              | A       | `stash.featuretests.part2.test.ts`                               |
| The history counters compute each of their totals, and the week and month windows exclude older events while taking the lifetime counter from the projection.                                                              | STASH-110, STASH-111, STASH-113c  | A       | `stash.featuretests.part2.test.ts`                               |
| The unique-strain counter reads the verified projection, dedupes by canonical name rather than raw name, and canonicalizes exactly as the order-merge path does.                                                           | STASH-112, STASH-113a, STASH-113b | A       | `stash.featuretests.part2.test.ts`                               |
| A heavy account's history stats bound their scan of the shelf, and a realistic shelf still reports an exact unique-strain count.                                                                                           | STASH-112b, STASH-112c            | A       | `stash.featuretests.part2.test.ts`                               |
| Per-item history returns the account's own events newest-first, and recent activity honours its limit with a default of 10.                                                                                                | STASH-113, STASH-114              | A       | `stash.featuretests.part2.test.ts`                               |
| History events cannot be written by a client; ownership is derived from the referenced row, and a row that is gone takes its history with it.                                                                              | STASH-115, STASH-115b, STASH-115c | A       | `stash.featuretests.part2.test.ts`                               |
| Filter chips slice the loaded page client-side without changing the subscription, and the screen keeps auto-loading while the filtered timeline is empty and pages remain — stopping once the archive is exhausted.        | STASH-222, STASH-224              | B       | `StashHistory.test.tsx`                                          |
| Search debounces 250 ms and sends the trimmed term, and a timeline row routes to order detail or strain detail as appropriate — navigating nowhere for an event that names neither.                                        | STASH-223, STASH-225              | B       | `useStashHistoryDataStability.test.tsx`, `StashHistory.test.tsx` |
| The history screen shows loading rather than a false empty, claims an empty archive only once the query has resolved, and distinguishes a filtered miss from a genuinely empty archive.                                    | STASH-226                         | B       | `StashHistory.test.tsx`                                          |
| The summary counts open exactly one subscription, report zeroes while loading, and skip entirely when unauthenticated — and the timeline hook opens only the subscription it actually reads.                               | STASH-227                         | B       | `useStashSummaryCounts.featuretests.test.ts`                     |

### Stats cards and forecasts

| Behavior                                                                                                                                                                                           | Verified by                     | Tier(s) | Where (file basename)                                                                          |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------- | ---------------------------------------------------------------------------------------------- |
| Lifetime stats include hand-added stash grams, exclude non-weight units, and report the excluded count.                                                                                            | STASH-139, STASH-140            | A       | `analytics.test.ts`, `stash.featuretests.part2.test.ts`                                        |
| Consumption rate counts only negative consume and empty events, applies +10% / −10% velocity thresholds, and returns a safe zero shape for a brand-new account.                                    | STASH-141, STASH-142, STASH-143 | A       | `stash.featuretests.part2.test.ts`                                                             |
| A restock forecast needs at least 10 events; confidence tiers step at 10, 15 and 30; alerts are critical under 3 days and a warning under 7.                                                       | STASH-144, STASH-145, STASH-146 | A       | `stash.featuretests.part2.test.ts`                                                             |
| Restock suggestions surface favourites that are not currently on the shelf, and every stats query rejects a mismatched owner key.                                                                  | STASH-148, STASH-149            | A       | `analytics.test.ts`, `stash.featuretests.part2.test.ts`                                        |
| The velocity card renders each of its states, stays hidden below six consumption events, and labels increasing, stable and decreasing distinctly.                                                  | STASH-235                       | B       | `StashStatsCards.test.tsx`                                                                     |
| A critical restock card is promoted to the top and rendered only once; an insufficient-data state never invents a date or countdown; a confident forecast keeps its countdown.                     | STASH-236, STASH-237            | B       | `StashStatsCards.test.tsx`                                                                     |
| The cost card renders a zero-gram account without printing `NaN` or `Infinity`, and the stash hero distinguishes its four forecast states while keeping 44 pt touch targets and non-tab semantics. | STASH-238, STASH-239, STASH-240 | B       | `StashStatsCards.test.tsx`, `consumption-model.test.ts`, `StashHeroCardAccessibility.test.tsx` |

### The order-to-stash merge contract

| Behavior                                                                                                                          | Verified by | Tier(s) | Where (file basename)              |
| --------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- | ---------------------------------- |
| Confirming an order merges its resolved items into the shelf.                                                                     | STASH-130   | A       | `stash.featuretests.part2.test.ts` |
| The merge emits **one aggregate event**, never a per-item `stash_added` — per-item events would double-count every order's grams. | STASH-131   | A       | `stash.featuretests.part2.test.ts` |
| The merge syncs each acquired strain into the collection as owned.                                                                | STASH-132   | A       | `stash.featuretests.part2.test.ts` |
| The merge is idempotent — a replayed confirm does not merge twice.                                                                | STASH-133   | A       | `orderPostSave.test.ts`            |
| The merge path and the hand-add path agree on what counts as the same line.                                                       | STASH-134   | A       | `stash.featuretests.part2.test.ts` |

### Propagation to the rest of the app

| Behavior                                                                                                                                                                                                                         | Verified by                                | Tier(s) | Where (file basename)                                                                   |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ------- | --------------------------------------------------------------------------------------- |
| A first add syncs the strain into the collection as owned with its ELO seed; a repeat add does not duplicate it; a strain with no id is skipped.                                                                                 | STASH-010, STASH-011, STASH-012            | A       | `stash.featuretests.test.ts`                                                            |
| A new stash row arms achievement re-evaluation and increments the lifetime item counter; topping up an existing line arms nothing and changes no counter, because no projected metric moves.                                     | STASH-013, STASH-013b, STASH-014           | A       | `stash.featuretests.test.ts`                                                            |
| Every add records its event with the correct gram delta for its unit (count units record zero), and that activity feeds the streak metric on a date key that agrees with the heatmap's local day.                                | STASH-015, STASH-016, STASH-137, STASH-138 | A       | `stash.featuretests.test.ts`, `stash.featuretests.part2.test.ts`                        |
| Research completing backfills the strain id onto unlinked rows only, per owner, and syncs each owner's collection.                                                                                                               | STASH-135, STASH-136                       | A       | `researchCompletion.test.ts`, `stash.featuretests.part2.test.ts`                        |
| The Safari extension receives purchased and low-stock slugs for that account only, skipping rows without a slug, and its subscriptions open only while the feature is active — never when disabled, unauthenticated, or off iOS. | STASH-085, STASH-086, STASH-242            | A, B    | `extensionSync.test.ts`, `stash.featuretests.test.ts`, `useStash.featuretests.test.tsx` |
| The stash hook skips every subscription when unauthenticated and otherwise subscribes to its four queries with the documented args.                                                                                              | STASH-210                                  | B       | `useStash.featuretests.test.tsx`                                                        |

### Ownership, export, and deletion

| Behavior                                                                                                                                            | Verified by                     | Tier(s) | Where (file basename)                                                            |
| --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------- | -------------------------------------------------------------------------------- |
| Every public stash and history export rejects an unauthenticated caller, and every ownership-scoped mutation rejects a foreign id.                  | STASH-175, STASH-176            | A       | `stash.featuretests.part2.test.ts`                                               |
| A data export includes stash and history rows with the Clerk id stripped, and account deletion purges both in batches.                              | STASH-152, STASH-153            | A       | `stash.featuretests.part2.test.ts`                                               |
| The fixture reset zeroes every counter for a `+clerk_test` account, reports what remains, and rejects both non-test accounts and an email mismatch. | STASH-150, STASH-151, STASH-178 | A       | `stash.test.ts`, `testFixtureGuards.test.ts`, `stash.featuretests.part2.test.ts` |

### Scale and concurrency

| Behavior                                                                                                                                               | Verified by                     | Tier(s) | Where (file basename)              |
| ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------- | ------- | ---------------------------------- |
| Two concurrent adds of the same line do not produce two lines, and a retire racing a permanent delete leaves no orphaned history event.                | STASH-170, STASH-171            | A       | `stash.featuretests.part2.test.ts` |
| An add against a 5,000-row shelf completes, a 50-item bulk add against a 1,000-row shelf completes, and history stats over 20,000 events still return. | STASH-172, STASH-173, STASH-174 | A       | `stash.featuretests.part2.test.ts` |

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

Tier C signs in as a real Clerk identity against the deployed staging Convex
deployment and the production Hono API, then cleans up after itself.

* **An app-shaped add round-trips through the deployed backend**, and the deployed validators accept the argument shapes all five write mutations are called with from the app (STASH-300, STASH-301).
* **The real scheduler runs**: an add lands its `stash_added` history row, and syncs the strain into the collection as owned (STASH-302, STASH-303).
* **Strain enrichment resolves against the real Hono API** for both the slug path and the name path (STASH-307).
* **Every layer reports the same grams for the same shelf** — stash stats, lifetime stats and the restock forecast agree on the deployed data, including millilitres (STASH-308).
* **A large real archive pages to exhaustion without duplicates or gaps**, and a server-side search finds a match that lies beyond the first page (STASH-309, STASH-310).
* **Cross-user isolation holds on the real deployment** — one account is refused another's stash rows (STASH-313).
* **The teardown contract works**: the fixture reset returns a seeded test account to zero remaining rows, which is what makes the rest of the suite repeatable (STASH-312).

## Deliberate gaps & conditions

**Two rows are deliberately not built.**

| Case                                                                             | Why                                                                                                                                                                                                                                   |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| STASH-177 — "stash writes are unbounded"                                         | There is no `boundedWrites` surface to assert against. The property that mattered was the write **fan-out**, and that is now pinned positively by STASH-177a/b/c: a 50-item bulk add schedules one history-event mutation, not fifty. |
| STASH-311 — the research webhook links stash rows to a new strain id, on staging | Would require driving a real research completion for a strain nobody has researched, on a shared deployment. Covered deterministically at Tier A by STASH-135 and STASH-136.                                                          |

**Four Tier C rows are proven in another area's suite** and must not be rebuilt
here: achievement metrics incrementing after an add (STASH-304 → the achievements
suite), the order-confirm merge end to end (STASH-305 → the orders suite), a stash
notebook accepting `sourceStashItemIds` (STASH-306 → the notebooks suite), and
extension slugs matching the shelf (STASH-314 → the dispensaries suite). The
order-merge idempotency row is likewise owned by the order post-save suite
(STASH-133).

<Warning>
  **STASH-131 must never be inverted.** It sits two tests away from STASH-132, shares
  a fixture, and reads almost identically — but STASH-132 was correctly inverted to
  assert that the merge *does* sync the collection, while STASH-131 pins the correct
  behavior that the merge does *not* emit per-item `stash_added` events. Inverting
  both ships a gram double-count. STASH-131 carries a do-not-invert fence in the
  file.
</Warning>

**A 500-id bulk retire is a recorded ceiling, not an endorsement.** STASH-052 exists
to document that no array cap is enforced on that path today, unlike permanent
deletion which caps at 50. If a cap is added, that case is where it gets asserted.

## Where to extend

A new backend rule — a validator, an ownership check, an event write — gets a case in
`apps/mobile/convex/tests/stash.featuretests.test.ts` for the write surface, or
`stash.featuretests.part2.test.ts` for history, analytics, propagation and scale. A
new hook argument or optimistic update gets a co-located `*.featuretests.test.ts(x)`
next to its source; a new screen state gets a jest-expo suite under
`apps/mobile/test/ui/features/`. Anything that only fails against a real deployment —
a validator drift, the scheduler actually firing, pagination over a real archive —
goes in `apps/mobile/test/integration/suites/stash.integration.test.ts`, scoped to its
own test user and cleaned up with the fixture reset.

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