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.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.The behavior contract
Creating a notebook from an order
Creating a notebook from stash
Who may generate, and how often
Watching a notebook build
Failure, retry, and revisions
The notebooks list and the archive
Opening, sharing, archiving, deleting
Journal and photos
Isolation, retirement, and the schema surface
Daily Stories — quarantined by design
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.siteroute, 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 reportsblocked, 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 returnsdaily-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.
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 inapps/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 for
tier selection, naming, and the Tier C isolation rules.