Imported from dtebar-10010/tq_v02 (
.github/skills/user-journey-simulation/SKILL.md). Install upstream withnpx skills add dtebar-10010/tq_v02 --skill user-journey-simulation. Copyright stays with the author.
User Journey Simulation Workflow (tqv02)
Use this skill to build and execute a bounded, high-confidence simulator for user interactions across tqv02, record the results into the persistent journey catalog, gate publish readiness, and remediate defects found during exploration.
This skill is catalog-backed: the journey inventory, the state dimensions, the run ledger, and the
publish-readiness gate all live in the database (tq_journey_* tables), not in prose. Prose here tells
you how to drive them.
Scope
End-to-end user journeys across:
- HTTP routes and function-based view handlers (
tqv02_app/views/*.py) - front-end interaction paths (jQuery
CartManagerinstatic/js/front.js, AJAX endpoints) - state transitions (auth, role, cart, wishlist, coupon, locale, currency, stock, merge, payment intent, consent, rate-limit, notification)
- correctness invariants at every transition (HTTP, DB, money units, cross-surface parity, i18n, security)
- side-effect surfaces (emails, Stripe, webhooks, sessions, cache, CSV exports, PDFs)
- defect reproduction and remediation with regression tests
- persistence of results and readiness trend (
JourneyRun,PublishReadinessSnapshot)
Out of scope unless explicitly requested: load/performance benchmarking (use performance-auditor),
translation-string authoring (use i18n-workflow), deploy execution (use pa-deploy-safety).
Persistence spine (read this before anything else)
| Layer | Location | Role |
|---|---|---|
| Models | tqv02_app/models/journey.py |
JourneyFamily, StateDimension, StateDimensionValue, JourneyPermutation, JourneyPermutationValue, JourneyRun, PublishReadinessSnapshot |
| Candidate generation | tqv02_app/services/journey_catalog.py |
build_combinations, plan_permutations, save_permutations, catalog_findings, JourneyCatalogError |
| Result recording | tqv02_app/services/journey.py |
record_journey_run, get_publish_readiness, capture_publish_readiness_snapshot |
| Seeding | seed_journey_catalog, seed_journey_permutations |
families + dimensions; bounded pairwise/3way candidates |
| Pruning | prune_journey_permutations |
delete never-executed candidates; run history always preserved |
| Recording | record_journey_run, record_journey_runs |
single result; atomic JSON batch |
| Gating | verify_journey_catalog, verify_journey_readiness |
integrity gate; live release gate |
| Reporting | generate_journey_run_report |
HTML ledger at misc/manual/reports/user-journey-run-ledger.html |
| Reference docs | misc/manual/reports/user-journey-simulator-potential-journeys.html, misc/manual/developer/journey-persistence-schema.html |
catalog narrative and schema |
| Shared test fixtures | tqv02_app/tests/journey_fixtures.py (JourneyFixtureMixin) |
make_family, make_pools, make_permutation, command, failing_command |
Hard facts about the spine that constrain what you may do:
JourneyPermutation.permutation_codeismax_length=24and unique. Generated codes aref"{journey_code}-{sha256(...)[:11]}". Never hand-write a code longer than 24 chars.- Every
StateDimensionmust have values and exactly oneis_default=True, orbuild_combinationsraisesJourneyCatalogError. get_publish_readiness()fails closed: skipped and never-run permutations block publish, and a family with zero permutations blocks publish.get_publish_readiness()uses.distinct("permutation_id")(PostgreSQL-only). Do not port these gates to SQLite.verify_journey_readinessadditionally requires: catalog integrity clean, no warnings, no latest run older than--max-age-hours(default 24), a fresh snapshot, snapshot equal to live values, and no runs recorded after the snapshot.record_journey_runsaccepts only the field set{permutation_code, status, test_module, test_command, defect_ref, notes, blocks_publish}; unknown keys, duplicate codes, or a non-boolblocks_publishabort the whole batch.
Mandatory startup preflight
- Confirm the DB is PostgreSQL (
topquarks_v01); the readiness gate needsDISTINCT ON. - Graph tools (codebase-memory-mcp):
list_projectsand confirmD-current-code-top-quarks-tqv02_project.index_status; if stale or missing,index_repositorywithrepo_path = "D:/current/code/top-quarks/tqv02_project",mode = "moderate".- Prefer
search_graph/get_architecture/trace_path/search_codeover raw grep for structural questions.
- Catalog preflight:
If the catalog is empty, seed it (see Step 2).python manage.py seed_journey_catalog --dry-run python manage.py verify_journey_catalogverify_journey_catalogwithout--strictprints warnings for families with no permutations;--strictpromotes those to failures.
Step 1: Enumerate routes, edges, and triggers
Build the candidate set from three sources, in order:
- Graph-first: architecture / routes / dependencies via
search_graphandtrace_path. - Urlconf:
tqv02_app/urls.py(namespacetqv02_app) plustqv02_settings/urls.pyfor the project-level mounts and the error handlers. - Runtime-only edges: template
<form action>targets,data-*attributes read bystatic/js/front.js, and$.ajax()call sites. These carry edges the urlconf cannot show (trigger type, method, expected JSON contract).
Authoritative route surface (regenerate whenever urls.py changes)
Project level (tqv02_settings/urls.py):
/admin/analytics/(staff),/admin/(Django admin + custom actions)/ckeditor5/(editor uploads),/webhooks/stripe/(system)/sitemap.xml,/robots.txt,/jsi18n/,/i18n/setlang/- custom auth before
include('allauth.urls'):/accounts/login/,/accounts/logout/,/accounts/register/,/accounts/register/ajax/,/accounts/password-reset/, reset confirm/done /accounts/(allauth: social login/callback/cancel/error, logged-out)/orbit/,/ai-assistant/,/mcp,/__debug__/(DEBUG only)- error handlers:
handler400handler403handler404handler500->utility_views.custom_400/403/404/500; rate-limit renders429.htmlviacommon.rate_limit_response
App level (tqv02_app/urls.py, namespace tqv02_app):
- storefront:
home,search,search_autocomplete,set_currency,categories,product_list,product_detail - reviews:
get_reviews,add_review,edit_review,delete_review - basket:
basket,basket_mini,add_to_cart,remove_from_cart,update_cart_quantity,apply_coupon,remove_coupon - checkout:
checkout,create_payment_intent,update_checkout_totals,process_order,order_confirmation,guest_order_track,download_invoice - wishlist:
wishlist,add_to_wishlist,remove_from_wishlist - account:
customer_account,footer_counts,customer_orders,order_detail,customer_addresses,delete_address,payment_methods,add_payment_method,set_default_payment,delete_payment_method,data_export,delete_account - blog + comments:
blog_index,blogs_topics_list,blog_post_detail,add_comment,add_reply,edit_comment,delete_comment,get_comments,get_comment_html,api_get_comments - chat:
customer_service_chat - static pages:
about_us,contact,faq,terms,privacy,shipping,returns - email lifecycle:
email_subscribe,verify_email,resend_verification,unsubscribe - history:
recently_viewed,remove_from_history - notifications/returns:
subscribe_stock_notification,my_returns,request_return,return_confirmation - health:
health_check,readiness_check,liveness_check - sourcing (staff):
generate_purchase_orders,send_purchase_order,fulfill_purchase_order,download_purchase_order
Normalized route inventory record
Produce one row per edge with:
- route pattern, method(s), view dotted path
- trigger type: direct URL | form submit | AJAX (
$.ajax) | JS event | email link | webhook | crawler | cron - mutation flag (read / write) and whether it must call
_invalidate_checkout_session(request) - auth requirement (
@login_required? guest-allowed by design?) and role (user/staff) - rate-limit / CAPTCHA gate (
@ratelimit, Turnstile) - response contract (HTML template name, or JSON keys, or file/CSV/PDF)
- criticality (payment / auth / order / PII = critical or high)
- edge id:
route + method + trigger + auth_guard - mapped
JourneyFamily.journey_code(orNEWif it needs a family)
Any edge with no mapped family is a catalog gap: add the family before claiming coverage.
Step 2: Seed and extend the bounded catalog
The seeded catalog currently holds 57 families and 10 state dimensions: J-001 .. J-048
(48 storefront) plus O-001, O-002, O-002b, O-003 .. O-008 (9 support/system, where O-002b
is admin CSV exports). Seeding is idempotent and never overwrites operator-edited rows.
Verify the live counts before trusting any number here:
python manage.py seed_journey_catalog --dry-run # prints Unchanged=<families+dimensions+values>
python manage.py verify_journey_catalog
python manage.py seed_journey_catalog --dry-run
python manage.py seed_journey_catalog
python manage.py seed_journey_catalog --generate-permutations --strategy pairwise
python manage.py seed_journey_catalog --generate-permutations --strategy 3way --max-permutations 110000
python manage.py seed_journey_permutations --family J-029 --strategy 3way --max-permutations 2000 --dry-run
seed_journey_catalog --generate-permutations plans the whole batch before writing and rolls back the
family/dimension seed if generation fails, so a budget overflow leaves no partial catalog.
Permutation budget (the default no longer covers 3-way)
The catalog has grown past 100k rows, so --max-permutations needs deliberate sizing. The budget check
is limit = max_permutations // len(families), compared against the per-family row count, and it
raises as soon as the running total exceeds that limit.
Measured against the live 10 dimensions (auth 3, cart 5, coupon 4, currency 4, locale 3, payment_intent 4, role 2, session_merge 3, stock 2, wishlist 4) and 57 families:
| Strategy | Rows per family | All 57 families | Default 50000 verdict |
|---|---|---|---|
pairwise |
280 | 15,960 | OK (allows 877/family) |
3way |
1,858 | 105,906 | FAILS |
Consequences:
- A full-catalog
--strategy 3wayrun at the default50000exits 1 withRESULT: FAIL reason=Permutation budget exceeded; select a family or raise the limit.Pass--max-permutations 110000for headroom. - A single-family
--strategy 3wayrun needs--max-permutations >= 1858; the per-family divisor makes a bare--family <code>call use the whole budget as its limit, so2000is a safe scoped value. - Full 3-way is the correct default only for critical packs. Prefer
--strategy pairwise(15,960 rows) as the baseline and scope 3-way with--family, per Step 4.
Re-derive these numbers whenever dimensions or values change - do not trust the table blind:
python manage.py seed_journey_permutations --strategy pairwise --max-permutations 110000 --dry-run
python manage.py seed_journey_permutations --strategy 3way --max-permutations 110000 --dry-run
Both print Candidates in bound: <n> without writing, which is the authoritative count.
Regeneration trap: these commands recreate every candidate they are allowed to. Running
seed_journey_permutations or seed_journey_catalog --generate-permutations without --risk-tier
undoes the pruning policy and re-floods the gate with not_run rows. Always pass
--risk-tier critical unless you deliberately want the full cross-product.
Scale note: catalog_findings() evaluates every permutation-level check set-wise in the database, in a
fixed 6 queries regardless of catalog size (measured: 0.55s over 106k permutations, ~5s wall for
the full verify_journey_catalog command including Django startup). Offender lists are capped at
MAX_LISTED_CODES (20) per finding, with an explicit list truncated. line when more exist, so a
broken catalog cannot flood the gate output. Both gates are safe to wire into a deploy step.
Keep it that way: never reintroduce a per-permutation Python loop into catalog_findings().
tqv02_app/tests/test_journey_catalog_findings_scale.py pins the query count with
assertNumQueries, so a regression fails the suite rather than silently costing minutes.
Adding a family (do this whenever Step 1 finds an unmapped edge)
Append a tuple to JOURNEY_FAMILIES in
tqv02_app/management/commands/seed_journey_catalog.py in the order:
(journey_code, actor_class, title, entry_trigger, route_pattern, primary_invariants, risk_tier, category).
journey_code: keep theJ-(storefront) /O-(support+system) prefix, zero-padded, unique,max_length=12. Suffix letters are allowed for insertions (O-002balready exists).risk_tier: one ofcritical,high,medium,low.category:storefrontorsupport.- No en-dashes or em-dashes anywhere in the tuple.
tqv02_app/tests/test_seed_journey_catalog.pyassertsJourneyFamily.objects.count() == len(JOURNEY_FAMILIES), so a new tuple must survive a double-seed run without duplicates.
Known catalog gaps to close for full-surface coverage
These edges exist in the codebase but have no dedicated family yet. Add them (suggested codes) before declaring the inventory complete:
| Suggested code | Actor | Journey | Route surface | Risk |
|---|---|---|---|---|
J-049 |
Guest/Auth | Newsletter subscribe + double opt-in | /subscribe/, /verify-email/<token>/, /resend-verification/ |
medium |
J-050 |
Guest/Auth | Newsletter unsubscribe from email link | /unsubscribe/<token>/ |
medium |
J-051 |
Guest/Auth | Contact form submit (Turnstile + rate limit) | /contact/ |
medium |
J-052 |
Guest/Auth | Customer-service chat session | /chat/, chat rate-limit middleware |
medium |
J-053 |
Guest/Auth | Cookie consent accept / reject / manage | _components/_cookie_consent.html, tq_cookie_consent cookie, GTM gating |
medium |
J-054 |
Guest/Auth | Error-page journeys | handler400/403/404/500, 404.html, 429.html |
medium |
J-055 |
Guest/Auth | Rate-limit and CAPTCHA rejection paths | rate_limit_response, validate_turnstile on login/register/comment/contact |
high |
J-056 |
Guest/Auth | Variant selection to cart line identity | product_detail variant selectors, "{pid}:variant:{vid}" session keys |
high |
J-057 |
Guest/Auth | Stock decrement and oversell guard | process_order stock path, check_low_stock |
critical |
J-058 |
System (cron) | Outbound lifecycle emails | send_abandoned_cart_emails, send_stock_notifications, check_low_stock, cleanup_old_emails |
high |
J-059 |
Auth | Return lifecycle beyond request (approve / reject / refund) | my_returns, return notification emails |
high |
J-060 |
Guest/Auth | Session and cookie hygiene | StaleHostCookieCleanupMiddleware, CurrentCurrencyMiddleware |
medium |
O-009 |
Staff | Admin analytics dashboard drill-down | /admin/analytics/ |
medium |
O-010 |
System (AI) | AI assistant + MCP + Orbit mounts | /ai-assistant/, /mcp, /orbit/ |
medium |
O-011 |
System | Media and static delivery, cache-busting token | /media/, /static/, static_v tag |
low |
O-012 |
Guest/Auth | Review machine-translation surface | ReviewTranslation, translation_service.get_localized_review() |
medium |
When you add a family, also add the matching regression test module to the journey-to-test map in
Step 7.3, or record its permutations as blocked with a reason instead of silently skipped.
Step 3: Build the finite state-machine model
Seeded dimensions (already in STATE_DIMENSIONS)
| Dimension | Values (default first) |
|---|---|
auth |
anonymous, authenticated, staff |
locale |
en, es, fr |
currency |
usd, eur, gbp, jpy |
cart |
empty, non_empty, variant_lines, mixed_session_db, post_merge |
coupon |
none, valid, invalidated, removed |
wishlist |
empty, non_empty, session_backed, db_backed |
stock |
in_stock, out_of_stock |
session_merge |
no_pending, pending, resolved |
payment_intent |
absent, created, confirmed, stale |
role |
customer, staff |
Recommended additional dimensions (add only with exactly one default)
shipping_tier (below_threshold default / at_threshold / above_threshold),
tax_jurisdiction (none default / us_state / intl),
address_book (empty default / single / multiple_with_default),
payment_method (none default / saved_card / new_card / declined_card),
consent (unset default / accepted / rejected),
rate_limit (under default / tripped),
captcha (disabled default / pass / fail),
device (desktop default / mobile_599).
Cost warning: build_combinations is pairwise/3-way over all dimensions, and the per-family limit
is max_permutations // len(families). Adding a dimension is not free at the current scale - the live
10 dimensions already produce 280 pairwise / 1,858 3-way rows per family (15,960 / 105,906 across 57
families). Rows are default-filled, so growth is roughly quadratic (pairwise) and cubic (3-way) in the
number of dimensions, not exponential: a measured 11th dimension with 4 values takes 3-way to 2,698 per
family (153,786 total) and pairwise to 355 per family (20,235 total).
Add dimensions one at a time, and always run seed_journey_permutations --dry-run first to read
Candidates in bound: <n> before writing anything. See the budget table in Step 2.
Represent, for each transition: states (nodes), actions/events (edges), guards (preconditions), effects (expected state delta). Keep the model finite and explicit; never expand over unbounded product/session permutations.
tqv02-specific guards you must model explicitly
- Cart duality: anonymous ->
session["basket"]dict; authenticated ->Basketrows.unique_together = ['user', 'product', 'variant']; quantity capped at 99. - Variant keys:
"{product_id}"simple,"{product_id}:variant:{variant_id}"variant. remove_from_cart/update_cart_quantitytakeAllProduct.product_id, notBasket.basket_id.remove_from_wishlisttakes the wishlist item id, not a product id.- Money units: catalogue fields are integer dollars;
Order/OrderItem/Payment/Couponare cents; conversion only throughtqv02_app/services/money.py. - Post-login redirect funnels through
TQAccountAdapter.get_login_redirect_url(); the guest cart is auto-merged into the DB ("Keep All") - there is nocart_merge_prompt. - Any cart mutation must call
_invalidate_checkout_session(request). - Session keys in play:
basket,checkout_email,checkout_login_email,checkout_redirect,checkout_snapshot,checkout_payment_intent_id,last_order_number.
Step 4: Define the bounded exhaustive strategy
Layered bounds:
- Pairwise coverage across all state dimensions (baseline,
--strategy pairwise, 15,960 rows - fits the default budget). - Targeted 3-way for critical flows (
--strategy 3way --family <code> --max-permutations 2000; full-catalog 3-way needs--max-permutations 110000and yields 105,906 rows):- auth + session_merge + cart
- coupon + shipping_tier + cart total
- locale + currency + checkout summary
- payment_intent + process_order + webhook side effects
- auth + role + admin export
- stock + variant + process_order
- Depth-limited traversal: default depth 6, depth 8 for critical packs.
- Neighbor replay budget on failure: replay the same edge with alternate guard outcomes.
- Deterministic fixtures and seeds. For parallel-only or order-sensitive regressions use Django's
built-in
--shuffle [SEED]runner before reaching for any external random-order tool. - Record the budget you used (
--max-permutations) and everything it cut.
Always report what was out of bound and why.
Step 5: Run dynamic simulation
Three execution layers:
- Django test client for deterministic backend transitions and DB assertions (primary layer).
- Browser automation (Playwright preferred, Selenium acceptable) only for UX/runtime contracts the test client cannot observe: JS-driven qty spinners, mini-cart hover, variant selector state, consent banner, 44x44 touch targets.
- Direct API checks for JSON contract consistency on the AJAX endpoints.
Fixture discipline:
- Use
tqv02_app/tests/factories.py/factories_fb.pyfor domain objects andtqv02_app/tests/journey_fixtures.py::JourneyFixtureMixinfor catalog objects. - Deterministic prices, stock, and coupon rules; no
randomwithout a fixed seed. - Mock external APIs at the service boundary:
unittest.mock.patch('tqv02_app.services.payments.stripe'). Never hit live Stripe. - Stripe test cards: success
4242 4242 4242 4242, 3DS4000 0027 6000 3184, declined4000 0000 0000 9995. - Orbit is DISABLED in
tqv02_settings/test_settings.py. Do not re-enable it for a journey run. - Any test that POSTs
set_languageMUST calltranslation.deactivate()intearDown.
Step 6: Enforce invariants at every step
Assert on each transition. A violated invariant is a failed path, not a warning.
I1 HTTP and routing
Status code, redirect chain and final URL, method guards (@require_POST), and that
remove_from_cart / update_cart_quantity reject non-POST where declared.
I2 Response contract
Template marker presence for HTML; for AJAX, success: bool always present, error responses carry an
explicit non-200 status= (400/401/403/422), and endpoint-specific keys are present:
add_to_cart -> created, quantity, product, wishlist_removed;
update_cart_quantity -> quantity, item_subtotal, removed;
basket_mini -> html, count, total, items;
add_to_wishlist -> created.
I3 Database state
Creates/updates/deletes and counters; get_or_create semantics honoured (no duplicate Basket rows);
Order / OrderItem / Payment written exactly once; stock decremented exactly once.
I4 Side effects
Emails (count + recipient + subject + locale), notifications, merge behaviour, session key deltas,
cache deltas, ProcessedStripeEvent dedupe rows, RefundAudit written before stripe.Refund.create.
I5 Security
Route protection (@login_required, @staff_member_required), CSRF on POST, webhook signature verified
before any DB write, no mark_safe on user content, rate-limit and Turnstile gates active where
declared, no secret leakage in logs or responses.
I6 i18n
Active-locale output consistency; gettext_lazy at module level and gettext at runtime; breadcrumb
labels use runtime gettext; locked terms respected (es cesta, fr panier); French NBSP before
: ; ! ? % and decimal comma; no en-dashes or em-dashes.
I7 Money and pricing
Subtotal / shipping / discount / tax / total arithmetic; unit discipline (dollars vs cents) with
conversion only in services/money.py; zero-decimal currency handling for JPY (no * 100);
stripe.PaymentIntent(amount=) is an int in cents; idempotency_key= derived from
(user.id or session_key, cart_hash, currency, amount_cents), never a per-call UUID.
I8 Cross-surface parity
The same numbers must appear in: endpoint JSON, basket_mini html, consolidated_css_context
(basket_count, basket_total), navbar badge, basket page, checkout summary, and
/account/footer-counts/. basket_total is pre-formatted, so never prepend a second $.
I9 Idempotency and replay
Repeat mutation calls do not corrupt state: duplicate process_order with the same PaymentIntent id is
a no-op; duplicate Stripe events are ignored via ProcessedStripeEvent; seed commands re-run without
duplicates.
I10 Accessibility and responsive
44x44px minimum touch targets, keyboard reachability of variant selectors and qty spinners, single
mobile breakpoint at <= 599.98px, out-of-stock affordances non-interactive.
Step 7: Defect remediation loop
For each failed invariant:
- Capture a minimal reproduction path (state vector + action sequence + permutation code).
- Patch the root cause with the smallest safe change.
- Add or extend regression tests for the failing edge and one neighbour edge.
- Replay the failing path and the impacted neighbourhood.
- Record the outcome with
record_journey_run(Step 8):fail+--defect-ref+--blocks-publishwhile open, thenpassafter the fix and replay. - Mark status: fixed, blocked, or deferred-with-risk.
Cap at 3 fix attempts per defect before escalating as blocked. Never silently relax an assertion to make
a test pass. If you find a pre-existing violation outside the current task, add a # SECURITY: or
# BUG: TODO with file:line and surface it at the end of the turn instead of drive-by fixing.
Step 7.1: Ranked change-safety checklist (hotspot-aware)
- Highest risk: checkout finalization -
tqv02_app/views/basket_views.py::process_order. Always test: guest/auth submit, missing PI, duplicate PI, non-succeeded PI, idempotent re-submit, snapshot/intent mismatch, stock decrement, oversell guard. - High risk: coupon validation and discount math -
tqv02_app/models/coupon.py::{validate_coupon,calculate_discount}. Always test: percentage / fixed / free-shipping / BOGO branches, usage caps, min-order, first-order, guest-email caps, bounds and rounding. - High risk: checkout totals and Stripe amount composition -
tqv02_app/views/basket_views.py::{create_payment_intent,update_checkout_totals,apply_coupon}. Always test: cross-surface parity, FX conversion and currency exponents, PI reuse/cancel paths, idempotency key stability. - Medium-high risk: social/auth merge handoff -
tqv02_app/allauth_adapter.py::{pre_login,get_login_redirect_url,pre_social_login}. Always test: session snapshot persistence, existing-email social linking, merge prompt vs account redirect branches,login(..., backend='django.contrib.auth.backends.ModelBackend'). - Medium risk: admin exports/actions -
tqv02_app/admin.py::{export_orders_csv,export_users_csv,export_products_csv}. Always test: CSV schema/headers, guest/auth row variants, staff-only visibility, translated labels.
If any item in a touched area fails, treat it as a hard defect and run the remediation loop.
Step 7.2: Standard one-command gate profiles
Fast gate (targeted hotspot smoke)
python manage.py test tqv02_app.tests.test_views_basket_internal_helpers tqv02_app.tests.test_allauth_regressions tqv02_app.tests.test_coupons tqv02_app.tests.test_tax_shipping tqv02_app.tests.test_multicurrency tqv02_app.tests.test_webhooks tqv02_app.tests.test_views_social_auth tqv02_app.tests.test_admin_views --settings=tqv02_settings.test_settings --keepdb --failfast
Medium gate (hotspot regression suite)
python manage.py test tqv02_app.tests.test_views_basket tqv02_app.tests.test_views_basket_pass3 tqv02_app.tests.test_coupons tqv02_app.tests.test_coupon_model_pass3 tqv02_app.tests.test_tax_shipping tqv02_app.tests.test_multicurrency tqv02_app.tests.test_ui_state_sync_matrix tqv02_app.tests.test_allauth tqv02_app.tests.test_allauth_regressions tqv02_app.tests.test_views_social_auth tqv02_app.tests.test_webhooks tqv02_app.tests.test_webhook_views_pass3 tqv02_app.tests.test_admin_views tqv02_app.tests.test_admin_pass3 --settings=tqv02_settings.test_settings --keepdb --parallel=8
Journey-catalog gate (always run when the catalog or its commands changed)
python manage.py test tqv02_app.tests.test_seed_journey_catalog tqv02_app.tests.test_seed_journey_permutations tqv02_app.tests.test_services_journey tqv02_app.tests.test_services_journey_catalog tqv02_app.tests.test_record_journey_run tqv02_app.tests.test_record_journey_runs tqv02_app.tests.test_verify_journey_catalog tqv02_app.tests.test_verify_journey_readiness tqv02_app.tests.test_command_generate_journey_run_report tqv02_app.tests.test_command_journey_report_filters tqv02_app.tests.test_journey_catalog_findings_scale tqv02_app.tests.test_prune_journey_permutations --settings=tqv02_settings.test_settings --keepdb --parallel=8
Full gate (pre-merge high confidence)
python manage.py test tqv02_app.tests --settings=tqv02_settings.test_settings --keepdb --parallel=8
Area-specific quick gates
- Checkout/payment path:
python manage.py test tqv02_app.tests.test_views_basket tqv02_app.tests.test_views_basket_pass3 tqv02_app.tests.test_webhooks tqv02_app.tests.test_webhook_views_pass3 tqv02_app.tests.test_multicurrency tqv02_app.tests.test_stock_decrement tqv02_app.tests.test_checkout_addresses --settings=tqv02_settings.test_settings --keepdb --failfast
- Coupon/tax/currency path:
python manage.py test tqv02_app.tests.test_coupons tqv02_app.tests.test_coupon_model_pass3 tqv02_app.tests.test_tax_shipping tqv02_app.tests.test_multicurrency tqv02_app.tests.test_currency_switching_effects tqv02_app.tests.test_pricing_properties tqv02_app.tests.test_money_and_composition_properties tqv02_app.tests.test_ui_state_sync_matrix --settings=tqv02_settings.test_settings --keepdb --failfast
- Auth/social merge path:
python manage.py test tqv02_app.tests.test_allauth tqv02_app.tests.test_allauth_regressions tqv02_app.tests.test_views_social_auth tqv02_app.tests.test_guest_auth_matrix --settings=tqv02_settings.test_settings --keepdb --failfast
- Admin exports/actions path:
python manage.py test tqv02_app.tests.test_admin_views tqv02_app.tests.test_admin_pass3 tqv02_app.tests.test_admin_consolidation tqv02_app.tests.test_i18n_phase9b_admin_catalog --settings=tqv02_settings.test_settings --keepdb --failfast
- Abuse/security gates path:
python manage.py test tqv02_app.tests.test_rate_limiting tqv02_app.tests.test_captcha tqv02_app.tests.test_validate_turnstile tqv02_app.tests.test_security tqv02_app.tests.test_settings_hardening tqv02_app.tests.test_sanitize_comment_html --settings=tqv02_settings.test_settings --keepdb --failfast
- i18n/locale path:
python manage.py test tqv02_app.tests.test_i18n_phase3 tqv02_app.tests.test_i18n_phase10_db_translation tqv02_app.tests.test_i18n_phase12 tqv02_app.tests.test_i18n_phase13_seo tqv02_app.tests.test_i18n_phase13_seo_fr tqv02_app.tests.test_i18n_phase15_fr_switcher tqv02_app.tests.test_translation_service_coverage tqv02_app.tests.test_o007_jsi18n_catalog --settings=tqv02_settings.test_settings --keepdb --failfast
Step 7.3: Journey family to test-module map
Use this to pick the evidence module recorded in JourneyRun.test_module. If a family has no module,
that is a coverage gap: create the module or record blocked with a reason.
| Families | Primary test modules |
|---|---|
| J-001..J-008 (browse, search, currency, locale, static) | test_views_shop, test_views_shop_extra, test_product_views_pass3, test_coverage_gaps_product_views, test_views_utility, test_views_utility_hardening, test_currency_switching_effects, test_multicurrency |
| J-009..J-017 (auth, register, reset, OAuth, logout) | test_allauth, test_allauth_regressions, test_views_social_auth, test_views_user, test_views_user_pass3, test_guest_auth_matrix, test_captcha, test_rate_limiting |
| J-018..J-025 (cart lifecycle, coupon, merge) | test_views_basket, test_views_basket_pass3, test_views_basket_internal_helpers, test_views_minicart, test_coverage_gaps_basket_views, test_coupons, test_coupon_model_pass3, test_ui_state_sync_matrix, test_cart_stale_js_and_empty_checkout |
| J-026..J-032 (checkout, order, invoice, tracking) | test_views_basket, test_checkout_addresses, test_tax_shipping, test_services_payments, test_stock_decrement, test_order_emails, test_order_timezone_helpers, test_consistency_counters_totals |
| J-033..J-037 (wishlist, history) | test_views_wishlist, test_views_wishlist_extra, test_views_wishlist_pass3, test_wishlist_edge_cases, test_wishlist_model_pass3, test_history_views, test_history_views_pass3, test_history_views_coverage_gaps |
| J-038 (stock notify) | test_stock_cart_wishlist_notifications, test_command_send_stock_notifications_gaps, test_notification_returns |
| J-039..J-045 (account, addresses, payment methods, GDPR) | test_views_user, test_views_user_extra, test_coverage_gaps_user_views, test_payment_methods, test_gdpr, test_command_delete_and_clear_user_data |
| J-046 (returns) | test_returns_workflow, test_notification_returns, test_command_seed_returns |
| J-047..J-048 (blog, comments, reviews) | test_views_blog, test_reviews, test_review_views_pass3, test_sanitize_comment_html, test_translation_service_coverage |
| O-001..O-002b (admin, analytics, exports) | test_admin_views, test_admin_views_pass3, test_admin_pass3, test_admin_consolidation, test_admin_coverage_gaps, test_i18n_phase9b_admin_catalog |
| O-003 (sourcing) | test_sourcing_workflow, test_sourcing_views, test_sourcing_views_pass3, test_command_import_sourcing |
| O-004 (webhooks) | test_webhooks, test_webhook_views_pass3 |
| O-005 (health) | test_views_health, test_views_health_extra |
| O-006 (SEO) | test_seo, test_i18n_phase13_seo, test_i18n_phase13_seo_fr |
| O-007 (jsi18n) | test_o007_jsi18n_catalog, test_locale_strings_and_makemessages |
| O-008 (CKEditor) | test_views_blog, test_security |
| Proposed J-049..J-053 | test_email_views, test_email_views_unsubscribe, test_email_views_pass3, test_email_backend, test_customer_service_chat, test_cookie_consent |
| Proposed J-054..J-055 | test_views_utility_hardening, test_rate_limiting, test_captcha, test_validate_turnstile |
| Proposed J-056..J-057 | test_product_variants, test_command_seed_product_variants, test_stock_decrement |
| Proposed J-058..J-060 | test_order_emails, test_command_send_stock_notifications_gaps, test_stale_cookie_cleanup_middleware, test_middleware_currency_hardening, test_middleware_pass3 |
| Proposed O-009..O-012 | test_admin_views, test_mcp_and_ai_assistants, test_ai_integrations, test_templatetags, test_translation_service_coverage |
Step 8: Record results and gate publish readiness
Record every executed permutation. Untested and skipped permutations fail the gate, so silence is never neutral.
Single result:
python manage.py record_journey_run J-018-a1b2c3d4e5f pass --test-module tqv02_app.tests.test_views_basket --test-command "python manage.py test tqv02_app.tests.test_views_basket --settings=tqv02_settings.test_settings --keepdb"
python manage.py record_journey_run J-029-9f8e7d6c5b4 fail --defect-ref BUG-142 --blocks-publish --test-module tqv02_app.tests.test_views_basket --notes "stale PI reuse regression"
python manage.py record_journey_run J-006-1122334455a pass --dry-run
Batch import (preferred for a full simulation pass; one snapshot for the whole batch, all-or-nothing):
python manage.py record_journey_runs tests_artifacts/journey_runs.json --dry-run
python manage.py record_journey_runs tests_artifacts/journey_runs.json
tests_artifacts/journey_runs.json shape:
[
{
"permutation_code": "J-018-a1b2c3d4e5f",
"status": "pass",
"test_module": "tqv02_app.tests.test_views_basket",
"test_command": "python manage.py test tqv02_app.tests.test_views_basket --settings=tqv02_settings.test_settings --keepdb",
"defect_ref": "",
"notes": "",
"blocks_publish": false
}
]
Gate and report:
python manage.py verify_journey_catalog --strict
python manage.py verify_journey_readiness --max-age-hours 24
python manage.py generate_journey_run_report --dry-run
python manage.py generate_journey_run_report
generate_journey_run_report writes misc/manual/reports/user-journey-run-ledger.html by default.
All of these commands print RESULT: OK or RESULT: FAIL reason=<short> and exit 0 / 1, so they
compose into a deploy gate.
Readiness interpretation:
is_publishableis True only when every permutation's latest run ispassand every family has at least one permutation.- A
fail/blockedrun withblocks_publish=Trueincrementsopen_blocking_defects. readiness_scoreis the percentage of permutations whose latest run ispass.verify_journey_readinessalso fails on stale evidence: run it in the same session as the tests, or raise--max-age-hoursdeliberately and say so in the report.
Step 8.5: Maximum-work run recipe and where the reports land
Invoke the skill with an explicit scope and bound, for example:
Run user-journey-simulation. Full storefront + checkout + account scope, pairwise baseline, 3-way on the critical packs, execute the full gate, record every result, regenerate the ledger.
The full ladder, in order
# 0. Preflight - catalog exists and is internally consistent (~5s)
python manage.py seed_journey_catalog
python manage.py verify_journey_catalog --strict
# 1. Candidates for critical families only (policy: not_run rows stay scoped)
python manage.py seed_journey_permutations --risk-tier critical --strategy pairwise --dry-run
python manage.py seed_journey_permutations --risk-tier critical --strategy pairwise
# 2. Deepen an individual critical family (1,858 rows each)
python manage.py seed_journey_permutations --risk-tier critical --family J-029 --strategy 3way --max-permutations 2000
# 3. Execute the evidence (this is the actual work - hours, not seconds)
python manage.py test tqv02_app.tests --settings=tqv02_settings.test_settings --keepdb --parallel=8
# 4. Record results as a single atomic batch + one snapshot
python manage.py record_journey_runs tests_artifacts/journey_runs.json --dry-run
python manage.py record_journey_runs tests_artifacts/journey_runs.json
# 5. Gate and publish the ledger
python manage.py verify_journey_readiness --max-age-hours 24
python manage.py generate_journey_run_report
Steps 3 and 4 are where the effort lives. Steps 0, 1, 2, and 5 are fast bookkeeping.
Reading the outputs
| Artifact | Where | How to view |
|---|---|---|
| HTML run ledger | misc/manual/reports/user-journey-run-ledger.html |
Open in a browser: start misc\manual\reports\user-journey-run-ledger.html |
| Catalog narrative | misc/manual/reports/user-journey-simulator-potential-journeys.html |
Browser; the hand-written journey inventory |
| Schema reference | misc/manual/developer/journey-persistence-schema.html |
Browser |
| Live readiness numbers | stdout of verify_journey_readiness |
RESULT: OK / RESULT: FAIL reason=..., exit 0/1 |
| Readiness trend | PublishReadinessSnapshot rows |
Django admin, or the trend section of the HTML ledger |
| Per-permutation history | JourneyRun rows |
Django admin (/admin/tqv02_app/journeyrun/) |
Serve the manual locally if you prefer HTTP over file://:
python manage.py runserver_quiet
# then browse to the report path under /media/ or open the file directly
Scoping the ledger
generate_journey_run_report renders one table row per permutation, so it caps output at
--limit 2000 by default and prints a warning when it truncates. Scope it instead of dumping
everything:
python manage.py generate_journey_run_report --family J-029 --family O-004
python manage.py generate_journey_run_report --status fail --status blocked
python manage.py generate_journey_run_report --status not_run --limit 50
python manage.py generate_journey_run_report --limit 0 # no cap; 15,960 rows = 5.5 MB, ~17s
python manage.py generate_journey_run_report --output tests_artifacts/scratch.html
--familyand--statusare repeatable;--status not_runselects never-executed permutations.- An unknown
--familyexits 1 rather than silently producing an empty table. - Readiness KPIs are always catalog-wide. Filtering changes only the ledger table, so a scoped
report can never make the catalog look healthier than it is. The HTML carries a
Scoped ledger: showing N of Mbanner whenever a filter or cap is in effect.
Right-sizing the catalog
Permutation counts are candidates, not evidence. Generating candidates is cheap; executing them is
not, and every unexecuted candidate fails the readiness gate as not_run. Keep the catalog to
what you can actually execute:
- pairwise across all 57 families = 15,960 candidates (the current, recommended baseline),
- pairwise on the 8 risk-prioritized packs = a few thousand,
- 3-way reserved for the handful of critical families.
Policy: unexecuted candidates only for critical families
An unexecuted candidate is a not_run row. It has exactly one use: failing the readiness gate closed
so an untested catalog cannot look healthy. It contributes nothing to the ledger, where it renders as
an identical "No run recorded" row.
The standing policy is therefore: carry not_run rows only for critical families. Everywhere
else the catalog holds just the permutations that have real evidence.
Generate within the policy:
python manage.py seed_journey_permutations --risk-tier critical --strategy pairwise
python manage.py seed_journey_permutations --risk-tier critical --family J-029 --strategy 3way --max-permutations 2000
--risk-tier is repeatable and filters which families gain candidates at all. Without it,
seed_journey_permutations regenerates the full cross-product and re-floods the gate.
Enforce the policy on an existing catalog:
python manage.py prune_journey_permutations --keep-unexecuted-tier critical --dry-run
python manage.py prune_journey_permutations --keep-unexecuted-tier critical
Other pruning modes:
python manage.py prune_journey_permutations --keep-per-family 280
python manage.py prune_journey_permutations --family J-029 --keep-per-family 100
Guarantees, all test-pinned:
- Only permutations with no
JourneyRunhistory are ever deleted; evidence always survives. - Executed permutations count against the per-family allowance.
- A family is never emptied. Pruning always leaves at least one candidate, because a family with zero permutations is an uncovered family and a hard readiness failure.
- One transaction: an interrupt rolls back with no partial state.
Current shape after applying the policy:
| Tier | Families | Permutations | Executed | not_run |
|---|---|---|---|---|
| critical | 12 | 3,360 | 106 | 3,254 |
| high | 26 | 48 | 48 | 0 |
| medium | 16 | 27 | 26 | 1 |
| low | 3 | 6 | 6 | 0 |
Total 3,441 permutations, 186 runs, readiness_score 5.41%, uncovered_families 0. The full ledger
renders uncapped in ~7s at 1.25 MB.
History: the catalog was generated at full 3-way (106,092 candidates against 186 runs), which no gate could ever turn green. It was pruned to the 15,960 pairwise baseline, then to 3,441 under this policy, with all 186 runs intact throughout.
Step 9: Bounded exhaustive exploration strategy
Use bounded search rather than infinite path expansion:
- Depth-limited BFS or DFS (default depth 4-6 unless overridden).
- Pairwise or reduced combinatorics across state dimensions; 3-way only for critical packs.
- Risk-weighted prioritization for high-impact edges first:
- payment / order / auth transitions
- session-to-auth merge
- coupon invalidation and shipping-tier crossings
- stock decrement and oversell
- PII export and account deletion
- Always report unvisited but in-scope edges when bounds cut them off.
Risk-prioritized packs (execute in this order when in scope)
- Checkout and payment intent pack (
J-026..J-032,O-004, proposedJ-057). - Auth and cart merge pack (
J-009..J-017,J-025). - Coupon and shipping-threshold pack (
J-022,J-023,J-027). - Cart lifecycle and cross-surface counter pack (
J-018..J-024,J-033..J-035,J-040). - Locale and currency pricing pack (
J-006,J-007,O-007). - PII and account-lifecycle pack (
J-042..J-045). - Staff and system pack (
O-001..O-003,O-005,O-006,O-008). - Abuse and resilience pack (
J-010,J-012, proposedJ-054,J-055).
Output contract
Return these artifacts:
- Route inventory and transition catalog (with an explicit unmapped-edge list).
- State-machine summary: dimensions, values, guards, bounds, and the exact
--strategy/--max-permutationsused. - Executed path matrix with pass/fail by invariant class
I1..I10. - Defects found with reproduction path, permutation code, fix status, and regression test links.
- Coverage summary: visited states, visited edges, skipped edges with reason.
- Recorded-evidence summary:
record_journey_run(s)invocations, resultingPublishReadinessSnapshotvalues, and theverify_journey_readinessoutcome. - Follow-up recommendations ordered by risk.
Report metrics (required)
- total families in catalog / families with at least one permutation
- total permutations in bound / visited permutations
- total edges in bound / visited edges
- pairwise combination coverage percentage
- critical-edge coverage percentage
- invariant failures by class (
I1..I10) - defects fixed vs blocked vs deferred
readiness_score,open_blocking_defects,is_publishable- unvisited in-scope edges, each with the bound that cut it
Hard constraints
- Never claim full-path exhaustion over an unbounded state space.
- Always disclose coverage bounds and skipped edges.
- Treat invariant failures as hard defects, never warnings.
- Prefer deterministic fixtures and reproducible seeds.
- Do not skip remediation for an in-scope, fixable defect.
- Do not silently relax assertions to make tests pass.
- Do not mark a permutation
passwithout an executed command recorded intest_command. - Do not re-enable Orbit in test settings to make a journey run.
- Never hit live Stripe, live email, or production data from a simulation run.
- Commit with
.\gc.cmd -m "msg", never rawgit commit.
Definition of done
- Route candidates are enumerated, normalized, and every edge maps to a
JourneyFamily. - A finite state machine was built, documented, and seeded into the catalog.
- Dynamic simulations executed with an explicit bounded strategy.
- Invariants
I1..I10enforced on each executed step. - In-scope defects were fixed with regression tests or documented as blocked.
- Every executed permutation has a
JourneyRunrow;verify_journey_catalog --strictandverify_journey_readinesswere run and their output reported. - The HTML run ledger was regenerated when results changed.
- Results include coverage gaps and risk-ranked next actions.
Troubleshooting (observed failure modes)
| Symptom | Cause | Fix |
|---|---|---|
FAIL: Permutation <code> has no dimension values. |
A hand-curated permutation row exists with an empty dimension_values M2M. catalog_findings() treats this as a hard failure. |
Attach values via the admin or permutation.dimension_values.set([...]), or delete the orphan row. Generated rows always get values from save_permutations. |
WARNING: Family <code> has no permutations. |
A family was seeded but never had candidates generated. Warning under verify_journey_catalog, failure under --strict and always a failure in verify_journey_readiness. |
Run seed_journey_permutations --family <code> (try --dry-run first). |
RESULT: FAIL reason=Permutation budget exceeded; select a family or raise the limit. |
max_permutations // len(families) is smaller than the per-family row count. Most often a full-catalog --strategy 3way at the default 50000, which needs 1,858/family (105,906 total). |
Scope with --family (plus --max-permutations 2000), or raise the full-catalog budget to 110000. The whole seed rolls back, so nothing is left half-written. See the budget table in Step 2. |
RESULT: FAIL reason=Dimension '<name>' must have values and exactly one default. |
A dimension has zero values or not exactly one is_default=True. |
Fix the dimension before generating anything. |
RESULT: FAIL reason=Generated code conflicts with existing state: <code> |
A regenerated code maps to a different family or value set than the stored row. | Do not force it; investigate whether a dimension value was renamed. Renaming changes the hash. |
Concurrent catalog generation detected; retry the command. |
Two generation runs overlapped. | Re-run once, serially. |
RESULT: FAIL reason=journey_readiness with not_run=<n> |
Permutations exist with no JourneyRun at all. The gate fails closed. |
Execute and record them, or record blocked with a defect_ref. Never leave them silent. |
Latest snapshot no longer matches live results. / Runs were recorded after the latest snapshot. |
Runs were recorded with --no-snapshot, or the batch path skipped the final snapshot. |
Call capture_publish_readiness_snapshot() or re-record one run without --no-snapshot. |
No fresh readiness snapshot found. |
Snapshot older than --max-age-hours (default 24). |
Re-run the simulation in-session, or raise the threshold deliberately and disclose it in the report. |
distinct("permutation_id") raises NotSupportedError |
Running the readiness gate on SQLite. | Use PostgreSQL (topquarks_v01); DISTINCT ON is PostgreSQL-only. |
Related skills and agents
.github/agents/user-journey-simulator.agent.md- the agent persona that drives this skill..github/prompts/simulate-user-journeys.prompt.md- one-shot invocation prompt.ui-state-sync-tests- cross-surface counter/total parity (invariant classI8).stripe-payments-expert- PaymentIntent, webhook, and refund specifics (I7,I4).currency-types-ecommerce-expert- money units and zero-decimal currencies (I7).i18n-workflow- locale invariants and.poregeneration (I6).pa-deploy-safety- deploy gating once readiness is green.performance-auditor- when a journey exposes an N+1 or slow path rather than a correctness defect.
