Skip to main content

Thames Valley Gifts Storefront — Expected Output Spec

DRAFT — not approved. Written by an agent from the research folder and the founder decisions listed in §1. The founder confirms or corrects it in a Stage-2 review. Until then, CLAUDE.md Rule #17 blocks storefront code (WP2).

Markers used in this file:

  • FOUNDER DECISION: something only the founder can decide. A recommended default is given where one exists.
  • DECIDED 2026-09-28 (default adopted by orchestrator): a FOUNDER DECISION whose recommended default was accepted on the founder's behalf so WP2 planning can proceed; the founder can still override it. See the Decision log at the end of this file.
  • [UNVERIFIED]: a fact nobody has checked yet. Check it before building on it.
  • [PLACEHOLDER]: a value that must be replaced before launch. Wherever it appears, it must be labelled as such.

0. Scope​

This spec covers everything a shopper sees on https://thamesvalleygifts.ca up to and including the screen that says a proof request was received:

  • Home
  • Shop and collections (by occasion, maker and product type)
  • The makers index and maker pages
  • The product detail page (PDP)
  • The coming-soon waitlist
  • The bulk / B2B quote page
  • Guides, policy pages and contact
  • The request-submitted confirmation
  • The server contract for submitting a request (POST /api/tvg/order-requests)

Sibling specs. Their content is assumed here, not repeated:

SpecCovers
tvg-proof-to-pay.md (written with this one)Everything after "Request a proof": the confirmation email, proof, approval, payment, fulfilment, refunds, every email, and the state machine. It is the canonical source for the confirmation screen's wording, and §4.11 here mirrors it. If the two differ, tvg-proof-to-pay.md wins.
tvg-ops-and-payouts.md (not written yet)The founder's ops pages, catalogue editing, makers, payouts.
tvg-ai-front-desk.md (not written yet)The chat widget and the voice line. No chat widget may appear on the storefront until that spec is approved.

Out of scope at launch (architecture §2.10): cart, customer accounts or logins, site search, reviews, discount codes, gift cards, SMS, international shipping, maker portal.

Screenshots: capture into knowledge/acceptance/screenshots/tvg-storefront/ from the deployed host once WP2 is live (names are given per touchpoint). None exist yet.


1. Fixed facts and founder decisions already made​

These are settled. Agents do not reopen them.

#Fact / decisionSource
F1Platform: Option D. TVG is built inside churchwiseai-web, served at /tvg/*, and rewritten from thamesvalleygifts.ca. There is no cart at launch.Founder, 2026-09-28
F2Payments stay on the existing Stripe account.Founder
F3Design: "Main Street". Drinkware, totes and the general shop use a bright small-town shop register. Memorial, baptism, ordination and wedding pages use a quieter keepsake register.Founder
F4Every page footer carries exactly: "Thames Valley Gifts is a division of ChurchWiseAI LTD."Business Names Act; BRAND.md
F5Ontario business-name registration BIN 1001760694. This is not the GST/HST number and must never be labelled as one (see §3.6).BRAND.md
F6Personalized items are final sale unless defective or different from the approved proof. This is disclosed before the request is submitted, together with an "I have checked the spelling" confirmation.Founder
F7The delivery window runs from proof approval.Founder
F8Pickup is by appointment in Ingersoll and is free.Founder
F9Shipping is Canada only at a flat rate: $14.50 [PLACEHOLDER] until the founder sets it.Founder
F10Scripture products offer KJV or WEB. NET comes later and is not offered at launch.Founder
F11Product statuses are example, coming_soon and available, with a server-side gate: an example product accepts no request; a coming_soon product accepts a waitlist signup only; a product can be available only when its maker has a signed agreement.Founder
F12The founder's own products (the ChurchWise Gifts label) are not available before mid-November 2026.Founder
F13No invented reviews and no "bestseller" claims. Example listings are visibly labelled as examples.Founder; BRAND.md positioning

2. User states and product statuses​

2.1 User states​

StateWhoHow they arriveWhat is different for them
U1 Anonymous shopper, phone (375 px baseline)A gift buyer on a phone, often from Facebook or a Google resultHome, a collection or a PDP linkSingle column. The live preview stays in view while they type (§7).
U2 Anonymous shopper, desktop (≥1024 px)Same buyer on a laptopSameTwo columns on the PDP: gallery and preview on the left, form on the right.
U3 B2B buyerA church office, sports team, realtor or business ordering 10 or more pieces, or different names per pieceHome "Bulk orders", the bulk link on a PDP, or a direct link to /bulkUses /bulk. Sees quantity tiers on PDPs. Is told organizations can pay by card or Interac e-Transfer after approving the proof.
U4 Returning customer with a request in flightSomeone who has already submitted a requestThe link in their email, or back to the siteThe storefront has no accounts and no memory of them. They reach their request only through the emailed link or "Find my request" (§4.10). The storefront shows them nothing personal.

There is no logged-in state on the storefront. The ops pages are on churchwiseai.com/founder/[token]/tvg/* and are out of scope here.

2.2 Product statuses: what each one shows, everywhere​

StatusListed in /shop, home, maker page?Tile showsPDP showsActions allowedServer gate on POST /api/tvg/order-requests
availableYesThe "Available" label, the price ("$29.00" or "from $29.00"), and the maker nameThe full personalization form, the live preview, the price, the final-sale disclosure and "Request a proof"Request a proofkind='retail' or 'bulk_quote' accepted
coming_soonYesThe "Coming soon" label, the status_note (for example "Orders open mid-November 2026") and the maker name. No price.Gallery, description, a preview-only personalization panel (§4.6), and the waitlist form. No "Request a proof".Join the waitlistOnly kind='waitlist' accepted. retail and bulk_quote get 409
exampleYes, always carrying the labelThe "Example" label, the text "Example listing — not for sale" and the maker name. No price.Gallery (each image labelled), description, and a plain list of the personalization options. No form inputs, no preview interaction, no request button, no waitlist.Browse onlyEvery kind gets 409
pausedNo—If reached by direct URL: title, gallery, and "This item isn't taking requests right now." No form.None409
draft, retiredNo—404None409 (or 404 if the id is unknown)

The three status labels are a fixed set. Each one is text, never colour alone:

  • "Available": solid fill.
  • "Coming soon": outline.
  • "Example": striped or dashed, like a sample tag.

Price rule. A price appears only on available items. Example items never show a price. Nothing J & B Creations prices appears anywhere (jb_creations_reference rules).


3. Global rules (every page)​

3.1 Header​

Should see:

  • The "Thames Valley Gifts" wordmark, linking to /.
  • Nav: Shop · Occasions · Makers · Bulk orders · Help (Help goes to /contact).
  • On phones, the nav collapses into a menu button labelled "Menu", which is a real <button> with aria-expanded.
  • A thin announcement bar: "You approve a proof before we make it · Free pickup in Ingersoll · Ships across Canada".

Should NOT see:

  • "Cart", a basket icon, or a "(0)" counter. The design mock shows "Cart (0)"; that is out of scope at launch.
  • "Sign in", "Account" or "My orders".
  • A site search box (deferred).
  • Any ChurchWiseAI AI-product promotion, PewSearch links, or the words "AI-powered" in marketing copy.

Should see:

  • Links to Shipping, Returns & final sale, Privacy, Terms, About, Contact, and "Find my request".
  • The exact line "Thames Valley Gifts is a division of ChurchWiseAI LTD." on every page, including the confirmation screen and 404.
  • "Prices in Canadian dollars."
  • "Pickup in Ingersoll by appointment."

Should NOT see:

  • A newsletter signup.
  • Social "follow us" counts.
  • Payment badges for methods TVG does not accept. Card, Apple Pay and Google Pay may be shown. No "Interac" badge, because Interac is not available at online checkout. E-transfer is a B2B-only manual option.
  • A GST/HST number labelled with the BIN.

3.3 Registers (F3)​

  • Every page root exposes data-register="main_street" or data-register="keepsake" so QA can assert it.
  • Keepsake register applies to:
    • the collection pages /shop?occasion=memorial|baptism|ordination|wedding;
    • any PDP whose product register is keepsake;
    • /guides/verses.
  • DECIDED 2026-09-28 (default adopted by orchestrator): add an explicit register field to tvg_products (not in architecture §2.2). The default comes from the product's first occasion: memorial, baptism, ordination or wedding gives keepsake, anything else gives main_street, and the operator can override it. Every occasion not in that list is Main Street.
Main StreetKeepsake
PaletteBright tiles (Oxford green, sweet corn, Tillsonburg sky per design doc)Quiet ground, one accent colour. No sweet-corn yellow fills, no coloured product tiles.
Copy toneFriendly and direct ("Put their name on it.")Calm and plain. No exclamation marks, no puns, no emoji in page copy.
Cross-sell"More from this maker" allowedOnly items from the same occasion, or none. Never drinkware or novelty items next to a memorial piece.
Bulk tier promo bannerAllowedNot shown, but the tier table stays if the product has tiers.

Contrast. Sweet corn (#F2C230) fails 4.5:1 as text on white. It may be a fill behind dark text, but never text or a thin outline on white.

3.4 Honesty rules (F13, BRAND.md)​

Should NOT appear anywhere on the storefront:

  • Star ratings, review counts, testimonials, "Top rated", "Bestseller", "Popular", "Trending", "Customer favourite", or "X sold".
  • "N people are viewing this", countdown timers, "Only N left", or "Sale" / strike-through prices without a real prior price.
  • "As seen on" logos, media mentions, or "years in business" claims.
  • Stock photos, or any other maker's photos, presented as TVG work. No J & B Creations photos are copied.
  • Any claim not true on launch day ("hand-inspected by every maker", "made within 100 km") unless the founder confirms it.

Should appear:

  • Every image that is not a photo of a real finished piece is labelled on the image: "Illustration", or "Sample image" for example listings.
  • Every value marked [PLACEHOLDER] in this spec, while it remains unconfirmed, keeps the site in "Opening soon" mode (§3.7). Placeholder values are never presented as final to a paying customer.
  • Allowed true claims:
    • named towns (Woodstock, Ingersoll, Tillsonburg, Dorchester, London);
    • "You approve a proof before we make it";
    • "Made nearby";
    • "A person looks at every photo before it is engraved" (true by process: the operator reviews uploads before proofing).

3.5 Fixed copy strings​

These strings are exact. Any change is a spec change first.

IdWhereExact text
C1Every footerThames Valley Gifts is a division of ChurchWiseAI LTD.
C2Next to every shown priceHST added at checkout
C3Example tile and example PDPExample listing — not for sale
C4PDP final-sale disclosure (above the checkbox)Personalized items are final sale. If your item arrives defective or different from the proof you approved, we will remake it or refund you.
C5PDP spelling checkbox labelI have checked the spelling of every name, date and word, and I understand personalized items are final sale unless defective or different from the approved proof.
C6PDP primary buttonRequest a proof
C7Under the buttonNothing is charged now. We email you a proof to approve first.
C8Preview disclaimerPreview is approximate. Your proof shows exactly what will be made.
C9Photo fallback (no live preview)We can't show a live preview of photo engraving. A person checks your photo and your proof shows exactly how it will look.
C10Low-resolution acknowledgement checkboxI understand this photo may lose detail when it is engraved.
C11Pickup linePickup: free in Ingersoll, by appointment
C12Shipping lineShipping: $14.50 flat anywhere in Canada [PLACEHOLDER value]
C13Canada-only noteWe ship within Canada only.
C14Timing line templateProof by email within 1 business day. Ready {min}–{max} business days after you approve your proof.
C15Same-personalization note (quantity > 1)Every item in this request gets the same personalization. Need a different name on each? Request a bulk order.
C16Translation note (verse fields)The World English Bible (WEB) uses the name "Yahweh" where the King James Version (KJV) says "the LORD". Example, Psalm 23:1 — KJV: "The LORD is my shepherd; I shall not want." WEB: "Yahweh is my shepherd; I shall lack nothing."
C17Coming-soon PDP notePreview only — orders open {status_note}. What you type here isn't saved.
C18Paused PDPThis item isn't taking requests right now.

FOUNDER DECISION: C14's "within 1 business day" proof promise, given 1–2 operator hours a day. Recommended: keep it. If it can't be met, change the string, never miss it silently.

3.6 Money and tax wording​

  • Prices are CAD, tax-exclusive, and formatted $84.00. The currency appears once on each PDP ("$84.00 CAD").
  • C2 ("HST added at checkout") sits next to every price shown. In this shop "checkout" means the proof and pay step. The proof page shows the exact tax line before any payment (tvg-proof-to-pay.md).
  • The PDP never shows a computed tax amount. Tax depends on the destination province (architecture §2.5, accountant sign-off pending), and the design mock's "HST (13%) $10.92" line is not carried into the build.
  • [UNVERIFIED] The label "HST" is wrong for provinces that charge GST only. C2 stays as fixed by the founder. The proof shows the correct jurisdiction label.
  • The GST/HST registration number that goes on receipts is ChurchWiseAI LTD's CRA business number plus RT0001. BRAND.md confirms the RT0001 account exists; the 9-digit BN is not recorded there. DECIDED 2026-09-28 (default adopted by orchestrator): the number is supplied at runtime via an environment variable (never committed to git); the 9-digit BN itself remains [UNVERIFIED] until the founder supplies it. [UNVERIFIED] The BIN 1001760694 (F5) is the Ontario business-name number. It may appear on the About/Terms page as "Ontario business name registration 1001760694" but must never be labelled GST/HST.
  • DECIDED 2026-09-28 (default adopted by orchestrator): show the BIN publicly, on the About page only.

3.7 Pre-launch "Opening soon" mode​

Conditions. The site stays in this mode until all of these are true, and the founder switches it off:

  • the shipping rate is confirmed (F9);
  • the accountant has ruled on the tax table (architecture §2.5);
  • at least one product is available with a signed maker agreement.

While in this mode:

  • A banner on every page reads "Opening soon — you can look around, but we aren't taking orders yet."
  • Every page is noindex.
  • POST /api/tvg/order-requests accepts kind='waitlist' only. Everything else gets 409.

3.8 Chat widget​

No chat widget, "Ask us" bubble or AI assistant appears until tvg-ai-front-desk.md is approved.


4. Touchpoints​

4.1 Home /​

States: U1–U4. Screenshots: home-desktop.png, home-375.png.

Should see, top to bottom:

  1. The announcement bar (§3.1), and the "Opening soon" banner while §3.7 applies.
  2. A hero with a plain headline and a sub-line naming the real area: "Personalized gifts from makers in Oxford County, Middlesex and London." It has one primary link, "Shop gifts" (to /shop), and one secondary link, "Bulk orders for churches, teams and businesses" (to /bulk).
  3. A product-family strip linking to filtered /shop: Tumblers & mugs · Slate & wood · Totes & towels · Faith gifts · Realtor closing gifts · Church & team orders. Only families with at least one listed product appear.
  4. The featured grid: available products first, then coming_soon. Every tile shows its status label, maker name, and a price only if available.
  5. "How it works", in exactly three steps:
    1. "Choose and personalize"
    2. "We email you a proof — nothing is charged"
    3. "Approve, pay, and we make it"
  6. "Made nearby", a makers strip. Each maker shows photo or initials, name, town and specialty, linking to the maker page. Example makers carry the "Example" label.
  7. A trust strip with true statements only (§3.4): proof before production · free pickup in Ingersoll · $14.50 flat shipping in Canada [PLACEHOLDER] · prices in CAD.
  8. The footer (§3.2).

Should NOT see:

  • Reviews or ratings, "bestseller", a cart, sign-in, or a chat bubble (§3.8).
  • Example products in the featured grid. They appear only via the makers strip and maker pages.
  • A price on any tile that isn't available.

Conditional:

  • IF zero products are available (likely before mid-November, F12): the featured grid is headed "Coming soon" and shows coming_soon items with their status_note. It never shows an empty grid and never implies anything can be ordered.

Success criteria: within 8 seconds a shopper can tell what is sold, that it's made nearby, that they approve a proof before paying, and where to start.

4.2 Shop and collections /shop​

Query parameters: ?type=, ?occasion=, ?maker=, ?status=. Screenshots: shop-all.png, shop-occasion-memorial.png, shop-maker-jb.png.

Should see:

  • A page heading that reflects the filter, for example "Memorial gifts" or "From J & B Creations".
  • Filter chips:
    • Occasion: memorial · baptism · ordination · wedding · anniversary · new home / realtor closing · birthday · Christmas · church & team.
    • Maker.
    • Product type.
    • Status: Available / Coming soon / Examples.
  • Active filters show as removable chips with a text "×" and the label "Remove filter: Memorial".
  • A grid of tiles. Each tile has:
    • the image (with "Illustration" or "Sample image" label where §3.4 applies);
    • the title;
    • "by {Maker}" (maker name always visible, as on NOTHS);
    • the status label;
    • the price only if available (the "from" price when options change it);
    • status_note if coming_soon, or C3 if example.
  • Sort order: available, then coming_soon, then example, then sort_order.
  • The keepsake register (§3.3) on memorial, baptism, ordination and wedding occasion pages.

Should NOT see:

  • Out-of-stock language (items are made to order).
  • Draft, paused or retired products.
  • Items outside the filter.
  • Infinite scroll without a way to reach the footer. Use pagination or "Show more" at 24 per page.

Conditional:

  • IF a filter returns 0 items: show "Nothing here yet." plus a link to the unfiltered shop. Never show a blank grid.
  • IF ?status=available returns 0: show "Nothing is open for orders yet. Here's what's coming." plus the coming_soon items.

Links: a tile goes to /makers/{maker}/{product}. The maker name on a tile goes to /makers/{maker}.

4.3 Makers index /makers ("Made nearby")​

Should see:

  • The heading "Made nearby".
  • One sentence on the model, with no claim about principal or agent. For example: "Thames Valley Gifts sells personalized gifts made by local makers. Each piece is made by the maker named on it."
  • One card per maker whose status is onboarding or active and who has at least one listed product. The card shows photo or initials, display name, town, specialties, and "Example" if the maker has no signed agreement.

Should NOT see:

  • Maker home addresses, personal phone numbers or personal emails.
  • A commission rate.
  • "Sell with us" forms. DECIDED 2026-09-28 (default adopted by orchestrator): no maker-recruitment link at launch. Use the contact page.

4.4 Maker page /makers/[maker]​

Screenshots: maker-churchwise-gifts.png, maker-jb-example.png.

Should see for every maker:

  • Name, town, specialties, story_md, years making (only if the maker supplied it), and a photo (only a real photo supplied by the maker).
  • A grid of their listed products with status labels.
  • The line: "Every order from {Maker} goes through Thames Valley Gifts: you approve a proof, then pay, then {Maker} makes it."

Maker-specific:

ChurchWise Gifts (the founder's own label):

  • Town is Ingersoll.
  • Products are coming_soon, with status_note "Orders open mid-November 2026", until the founder flips them (F12).
  • Its agreement gate is satisfied by an internal record. DECIDED 2026-09-28 (default adopted by orchestrator): a self-agreement row is acceptable, agreement_version='internal'.
  • The story is written or approved by the founder. No "pastor" credential claim appears unless the founder writes it.

J & B Creations:

  • The maker card and page carry the "Example" label and the sentence "Sample maker profile. Details to be confirmed with J & B Creations before anything is for sale."
  • Their products are example only.
  • Should NOT see:
    • J & B photos, prices (including the $20 / $16 chalkboard prices), reviews or the Alignable recommendation quote;
    • the Forest Grove street address, either phone number, or the Gmail address;
    • claims about their turnaround.
  • Town "Dorchester" appears only after Jackie Rayner confirms it.
  • [UNVERIFIED] and FOUNDER DECISION (blocking): has Jackie Rayner agreed, in writing, to her business name appearing on a public example profile? If not, the J & B page stays unpublished (status='onboarding' and hidden) and no J & B example product is listed. Interim rule until consent is obtained: any placeholder maker card shown in mockups or examples uses the neutral name "Example maker: a Dorchester sublimation studio" instead of J & B Creations or Jackie Rayner's name.

Links: product tiles go to the PDP. Contact goes to /contact. There is no direct maker contact.

4.5 Product detail page /makers/[maker]/[product] (status available)​

States: U1–U4. Screenshots: pdp-text-desktop.png, pdp-text-375.png, pdp-verse.png, pdp-monogram.png, pdp-photo.png, pdp-final-sale.png.

The PDP is a single form that submits one request. Sections appear in this order. On desktop, (a) is on the left and (b)–(p) on the right. On phones they stack, with the preview pinned (§7).

Should see:

  • Hero image, detail images, a scale image (a hand, cup or ruler) where one exists, and colour swatches.
  • Each image has meaningful alt text.
  • The live preview. When the product's preview.mode is text_overlay, the first gallery slot is the preview: the customer's text drawn as SVG over preview.base_image, in the production font files (src/fonts/tvg.ts, OFL-licensed).
  • Preview update behaviour:
    • It updates on every input change: text, verse, translation, monogram, font or colour.
    • It updates within 150 ms of the last keystroke. No animation when prefers-reduced-motion: reduce; the preview swaps instantly.
    • Its alt text restates the content, for example: "Preview: slate coaster engraved 'The Andersons · Est. 2019' in Classic serif".
    • A polite live region announces "Preview updated" at most once per pause in typing (debounced to 1 second or more), not on every keystroke.
    • Text that doesn't fit the box shrinks (fit: shrink) down to a minimum size. Below that minimum, an inline note says "This is getting small. Shorter text will engrave more clearly."
  • Empty fields show the field's label in grey placeholder style inside the preview, for example "Name here", so the layout is visible before typing.
  • C8 appears directly under the preview.

Photo-engraving fallback (preview.mode: "none" with before/after examples):

  • There is no live render. C9 appears where the preview would be.
  • Before/after pairs appear only if they are real TVG work, labelled "Example of a photo engraved on slate. Not your photo." If there are no real pairs yet, show nothing rather than stock images.
  • The uploaded photo's thumbnail may be shown, labelled "Your photo (not a preview of the engraving)".

Should NOT see:

  • A preview claiming to be exact.
  • A preview for photo engraving.
  • Autoplay carousels.
  • Zoom that traps keyboard focus.

(b) Title, maker line and status​

  • Title (H1). The maker line: "Made nearby by {Maker}, {Town}", linking to the maker page. The "Available" label.

(c) Price​

Should see:

  • The base price as $84.00 CAD, with C2 ("HST added at checkout") right next to it.
  • When options or quantity change the price, a live line reads "Estimated subtotal: $X before HST and shipping. Your proof shows the final total." It is recalculated in the browser with the same pricing.ts module the server uses.

Should NOT see: a tax amount, a "total", a strike-through price, or "Free shipping".

(d) Personalization panel: general rules for every field​

Should see:

  • Overall instructions before the form fields, per W3C: "Fields marked Required must be filled in. Spaces and punctuation count toward character limits."
  • Every field has a visible <label>. The limit and required/optional status are inside the label text, for example "Name (required, up to 28 characters)". Placeholder text is never the only label.
  • Errors appear inline next to the field, as an icon plus text, never colour alone. The error names the field, says how to fix it, and is linked with aria-describedby.
  • Field order follows personalization_schema.fields.

Should NOT see: tooltip-only instructions, or a validation summary as the only error display.

(e) Text field (type: text), for example line1 with a 24-character limit​

Should see:

  • The label, for example "Engraving line 1 (required, up to 24 characters)".
  • A counter below the field, "12 / 24", updated on every input.
    • At 80% of the limit or more, the counter changes colour (Tillsonburg sky) and adds the text "(4 left)".
    • At the limit it reads "24 / 24 (limit reached)".
  • maxlength equal to the limit, so the 25th character cannot be typed.
  • Paste handling: when pasted text would exceed the limit, the field keeps the first N characters and an inline message appears: "Only the first 24 characters fit. Check the end of your text." This design answers the Etsy-truncation failure: truncation is never silent.
  • Character set: Latin letters including accents (é, è, ç, ñ, ö and similar), digits, spaces, and & . , ' - · ! ? ( ) / #. DECIDED 2026-09-28 (default adopted by orchestrator): this set is confirmed as the launch character set, pending a final check against the production font files' actual glyph coverage.
    • An unsupported character (emoji, CJK, and so on) is refused with an inline message naming it: "This character can't be engraved: ☺. Please remove it."
    • Submit is blocked until it is removed. The server enforces the same rule.
  • Leading and trailing spaces are trimmed on submit. Double spaces inside the text are kept and shown as-is in the read-back (§4.5 (m)).

Should NOT see: a counter that disagrees with the server's count. Both count Unicode code points after NFC normalization.

DECIDED 2026-09-28 (default adopted by orchestrator): NFC-normalize first, then count code points (not grapheme clusters), for accented names typed with combining marks.

(f) Textarea (type: textarea)​

The same rules as (e), with a multi-line counter. For engraved text, line breaks count as 1 character each. The gift note is covered in (l).

(g) Verse picker (type: verse)​

Should see:

  • The label "Scripture verse", plus "(required)" or "(optional)".
  • Translation choice: a radio group, KJV ("King James Version") | WEB ("World English Bible"). The default is the schema's default_translation (KJV).
  • C16, the WEB "Yahweh" note, placed next to the translation choice and visible without expanding anything.
  • Verse list:
    • A select or list of curated references from tvg_verses. It only offers references that have human-verified text in both translations, where the field offers both.
    • Each option shows the reference and a fit hint from word count against max_words: "fits well", "longer, smaller letters", or "too long for this item". Options marked "too long for this item" are disabled, and the reason is shown in text.
  • The full verse text exactly as it will be engraved appears below the selection, with reference and translation ("Psalm 23:1 · KJV").
  • Switching translation updates the shown text and the preview immediately.
  • A link to /guides/verses, and "Want a verse that isn't listed? Ask us" (to /contact). Custom references are not allowed at launch (allow_custom_reference: false).

Should NOT see:

  • NET or any other translation (F10).
  • A free-text verse box.
  • Verse text fetched live from an API.
  • A verse with only one verified translation offered in both.

(h) Monogram (type: monogram, 3 letters)​

Should see:

  • The label "Monogram (required, 3 letters)".
  • Three separately labelled one-letter inputs: "First name initial", "Middle name initial" and "Last name initial".
    • Each accepts A–Z only, auto-uppercases, and moves focus forward after one letter.
    • Each is still editable with Backspace and Shift+Tab.
  • Arrangement choice (radio group with a visual example for each):
    • "Traditional — first, LAST, middle (last initial larger in the centre)". This is the default first_LAST_middle.
    • "In name order".
    • "Couple — first person's initial, shared LAST initial, second person's initial". DECIDED 2026-09-28 (default adopted by orchestrator): this gender-neutral wording replaces "hers … his", keeping the same convention.
  • A plain sentence that reads the result back: "Your monogram will read M J A, with J larger in the centre." It updates live.
  • A link to /guides/monogram.
  • The preview renders the arrangement. For first=M, middle=A, last=J with Traditional, the preview reads "MJA", with J larger and centred.

Should NOT see: a single free-text "monogram" box. That is the letter-order failure named in ux_research §2 and jb_creations §3.4.

(i) Photo upload (type: image_upload)​

Should see:

  • The label "Your photo (required, JPEG or PNG, up to 15 MB)". The formats and size come from the schema.
  • Resolution guidance computed from the product's print area. For a 6 × 4 in area: "Best: at least 1800 × 1200 pixels. Works: at least 900 × 600 pixels. Clear subject, even light, good contrast."
  • A choose-file control that works from phone camera rolls. accept="image/jpeg,image/png,image/webp". [UNVERIFIED] iOS converts HEIC to JPEG with this accept value. Test it on a real iPhone before the copy mentions iPhone photos.
  • After a file is chosen, the browser reads the pixel size and shows the effective DPI rating as text and icon:
    • Good (≥300 DPI): "Good — this photo is sharp enough."
    • OK (≥150): "OK — this should engrave well."
    • Low (<150): "Low resolution — this photo may lose detail when engraved." The unticked checkbox C10 appears and is required to submit.
  • A low-resolution photo is never silently rejected. The only hard rejections are the wrong file type or a file over the size limit. Each shows a message naming the problem: "This file is 22 MB. The limit is 15 MB."
  • The option "I'll send my photo later", a checkbox. It creates the request with status='needs_info', and the confirmation screen says the photo is still needed.
  • The sentence: "A person looks at every photo before it is engraved and will write to you if it won't engrave well."
  • A privacy line: "We remove location data from your photo and delete it 90 days after your order is complete." It links to /privacy.

Should NOT see:

  • A live engraving preview.
  • Crop or filter tools claiming to show the final result.
  • Upload to a public bucket (architecture §2.6: private bucket, signed URLs).

(j) Choice fields: colour, size, font (type: choice)​

Should see:

  • A radio group per field, with the label containing the field name and "(required)" where needed.
  • Colour swatches always carry a text label ("White"). The selected state is shown with a border and a check icon.
  • Font swatches render a sample word in that font, plus its text label ("Classic serif").
  • Size options show their dimensions and any price change ("8 × 10 in (+$10.00)").
  • Choosing an option updates the preview (font, colour) and the estimated subtotal.

Should NOT see: colour-only swatches, or font names shown without a sample.

(k) Quantity and tiers​

Should see:

  • A number stepper labelled "Quantity", with min and max from the schema, inputmode="numeric", and − and + buttons of at least 44 × 44 px.
  • A tier table when tiers exist, for example "1–9: $28.00 each · 10–24: $22.00 each · 25+: $19.00 each". The row that applies to the current quantity is highlighted in text ("Your price") as well as colour.
  • C15 whenever quantity is more than 1.
  • When the requested quantity is above max: the stepper stops at max, and the inline message "For more than {max}, request a bulk quote" links to /bulk?product={slug}&qty={n}.

(l) Gift note​

Should see:

  • The label "Gift note (optional, not engraved, up to 250 characters)", with a counter.
  • Visual separation from the engraved fields (its own group heading, "Gift note").

Should NOT see: gift-note text in the preview.

(m) Read-back, final sale and spelling confirmation​

Should see, in this order, directly above the button:

  1. "Check your personalization", a read-back box listing every produced value exactly as it will be sent:
    • each text field in a monospaced style, with visible markers for double spaces;
    • the verse reference, translation and full text;
    • the monogram letters and arrangement sentence;
    • font, colour and size;
    • quantity;
    • photo filename and rating, or "Photo: sending later". Each row has an "Edit" link that focuses the field.
  2. C4, the final-sale disclosure, as body text (not small print).
  3. C5, the spelling and final-sale checkbox: unticked by default, a real checkbox with a label.
  4. The contact and fulfilment fields (n).
  5. C6, "Request a proof", with C7 under it.

Behaviour:

  • The button is always enabled. Clicking it with problems moves focus to the first problem and shows its inline message.
  • If C5 is unticked, the checkbox gets focus and the message "Please confirm you've checked the spelling."
  • A disabled button with no reason given is never used.

Should NOT see:

  • The final-sale terms only on a policy page.
  • A pre-ticked checkbox.
  • The final-sale sentence merged into Terms acceptance.

(n) Contact and fulfilment (part of the same form)​

Should see:

  • "Your name (required)".
  • "Email (required — your proof is sent here)", type="email".
  • "Phone (optional)", type="tel".
  • Fulfilment, a radio group:
    • C11 ("Pickup: free in Ingersoll, by appointment").
    • "Ship to me: $14.50 flat anywhere in Canada [PLACEHOLDER]".
  • If Ship is chosen, address fields appear: line 1, line 2 (optional), city, a province select listing only the 13 provinces and territories, and postal code validated A1A 1A1 (case-insensitive, space optional). Country is fixed as "Canada" and shown as text, not a field. C13 appears.
  • "Needed by (optional)", a date input. If the date is earlier than today plus 1 business day plus lead_time_max_bd plus shipping transit, an inline note reads: "That's sooner than we can usually make it. We'll tell you honestly when we send your proof." Submission is not blocked.
  • A hidden honeypot field.

Should NOT see:

  • A country selector.
  • A pre-ticked or required marketing-email checkbox. DECIDED 2026-09-28 (default adopted by orchestrator): no optional news opt-in box at launch, because no promotional program exists.
  • A password or account field.

(o) Shipping, pickup and timing lines​

Should see (near the price on desktop, and after the button on mobile):

  • C11.
  • C12.
  • C14, filled from lead_time_min_bd/lead_time_max_bd.
  • "Shipping adds travel time after that."

[UNVERIFIED] The carrier is Canada Post tracked. Do not name a carrier until the founder confirms.

(p) Below the form​

  • A description (description_md) and specifications: materials, size, care ("Hand wash" etc. only if the maker supplied it).
  • Links to the guides and to "Returns & final sale".
  • "More from {Maker}" in the Main Street register only (§3.3).

Should NOT see on the PDP at all:

  • "Add to cart", "Buy now", "Pay", or wallet buttons (Apple Pay / Google Pay).
  • A second primary button. "Request a proof" is the only primary action.
  • Any price or tax total presented as final.

Success criteria: a shopper can personalize, see their text on the product, get a clear read-back, understand it's final sale and that nothing is charged now, and submit. And they cannot submit anything the maker cannot make.

4.6 Coming-soon PDP (status coming_soon)​

Should see:

  • Gallery, title, maker line, the "Coming soon" label and status_note.
  • The personalization panel with live preview, headed by C17. There is no read-back box, no C4/C5, and no fulfilment fields.
  • The waitlist form:
    • "Your name".
    • "Email (required)".
    • An unticked consent checkbox: "Email me once when this item can be ordered." It is required to submit, and it is CASL express consent for that one message.
    • The button "Notify me".
  • After submit: "Thanks — we'll email you once when {product} opens for orders. That's the only email we'll send about it."
  • The submit creates exactly one tvg_order_requests row with kind='waitlist', no items, and consent text and timestamp stored.

Should NOT see: a price, C6 ("Request a proof"), or "Pre-order".

DECIDED 2026-09-28 (default adopted by orchestrator): no full personalized pre-request for coming_soon items at launch — waitlist only.

The "now available" email is founder-pressed only. It is never cron-sent. It is sent once per signup, and its content is defined in tvg-proof-to-pay.md §9, E13.

4.7 Example PDP (status example)​

Should see:

  • The "Example" label and C3, both near the title and on the hero image.
  • The gallery, with every image labelled "Sample image" or "Illustration".
  • Title, maker line and description.
  • A section headed "Options this item would have" as a plain list, for example: "Name, up to 28 characters · Verse, KJV or WEB · Photo".
  • A link: "See gifts you can order now", to /shop?status=available (or to coming_soon items if none are available).

Should NOT see:

  • Any price.
  • Form inputs, live preview interaction, "Request a proof", "Notify me", or a quantity stepper.
  • DECIDED 2026-09-28 (default adopted by orchestrator): no live-preview demo on example pages.

Server: POST /api/tvg/order-requests with this product's id, of any kind, returns 409 {"error":"not_orderable","status":"example"} and creates zero rows.

4.8 Bulk and B2B quote /bulk​

State U3. Screenshot: bulk.png.

Should see:

  • The heading "Bulk orders for churches, teams and businesses".
  • Three plain examples: church gifts (baptism, ordination, volunteer thank-yous); team tumblers with a different name on each; realtor closing gifts with a logo.
  • How it works:
    1. "Tell us what you need"
    2. "We send a quote and a proof"
    3. "You approve, pay by card or Interac e-Transfer, and we make it"
  • The form:
    • "Organization name (required)".
    • "Your name (required)".
    • "Email (required)".
    • "Phone (optional)".
    • "Which product?" — a select of available products plus "Something else". It is pre-filled from ?product=.
    • "How many? (required)" — pre-filled from ?qty=.
    • "Different names or text on each piece?" (yes/no). If yes: "Paste the list, one per line (up to 500 lines)", with the line count shown. DECIDED 2026-09-28 (default adopted by orchestrator): paste only at launch; no CSV upload.
    • "Needed by (optional)".
    • Fulfilment, with the same rules as §4.5 (n).
    • "Anything else we should know (up to 1,000 characters)".
    • C5 adapted as: "I have checked the spelling of every name in my list, and I understand personalized items are final sale unless defective or different from the approved proof." It is required.
    • The button "Request a quote", with C7 adapted: "Nothing is charged now. We email you a quote and proof to approve first."
  • A direct phone number for B2B buyers who want to talk first. [UNVERIFIED] The front-desk line is not provisioned yet. Show the number only once it rings somewhere real.

Should NOT see:

  • "Contact us for pricing" as a dead end: the tier prices for listed products are visible on their PDPs.
  • Net-30 invoicing promises (deferred).
  • Example or coming-soon products in the product select.

Server: creates one tvg_order_requests row with kind='bulk_quote', organization_name set, one item per selected product (none if "Something else"), and the list stored verbatim in the item's personalization under the key name_list.

DECIDED 2026-09-28 (default adopted by orchestrator): the bulk schema key name is name_list (as used above); quantity need not equal the list length — warn, don't block.

4.9 Guides and policy pages​

Guides: /guides/monogram, /guides/photo-engraving, /guides/verses.

  • Should see:
    • Monogram: the three arrangements with the Mary Ann Jones → M J A example, and the note that conventions vary so the customer chooses.
    • Photo: what engraves well, the DPI table in plain words, and real before/after pairs only.
    • Verses: KJV vs WEB, the C16 example, and why custom verses need a human check.
  • Should NOT see: borrowed Mark & Graham or other stores' text.

Policies: /policies/shipping, /policies/returns, /privacy, /terms, /about. The text comes from a new tvg: section of knowledge/data/policies.yaml (architecture §4 Phase 0), which must exist before these pages ship.

PageMust stateMust NOT state
ShippingCanada only · $14.50 flat [PLACEHOLDER] · pickup free in Ingersoll by appointment · the delivery window starts at proof approval (F7) · tracked shipments get a tracking emailInternational shipping, free-shipping thresholds, guaranteed delivery dates
Returns & final saleF6 verbatim, plus the three cases from tvg-proof-to-pay.md §8: maker error → remake or refund; proof differs from your request without being pointed out → remake; your own approved text → final sale. Also how to report a problem, the photo-evidence window, and that non-personalized items (if any are ever sold) follow a separate rule."No refunds, ever"; any promise broader than §8
PrivacyWhat is collected (contact, address, personalization, photos) · photos stored privately, location data removed, deleted 90 days after completion · proof approvals record time, IP address and browser for dispute evidence · no selling of data · the contact for privacy requestsMarketing-list language (there is none)
TermsDivision line C1 · BIN (if the founder approves, §3.6) · governing law Ontario [UNVERIFIED — counsel] · pricing in CAD, tax-exclusive · proof-then-pay sequenceGST/HST number labelled with the BIN
AboutThe hub model in plain words, the towns, and the makers linkHeadcount, revenue or tenure claims

4.10 Contact /contact and "Find my request"​

Should see:

  • Email: DECIDED 2026-09-28 (default adopted by orchestrator): hello@thamesvalleygifts.ca (the mailbox must exist and be monitored). Phone [UNVERIFIED — front-desk line]. "Pickup in Ingersoll by appointment".
  • A simple message form: name, email, message (up to 2,000 characters), honeypot. On success: "Thanks — a real person will reply, usually within 1 business day."
  • "Find my request": fields for order number (TVG-######) and email.
    • On submit, the same message always appears, whether or not they match: "If that order number and email match a request, we've emailed you its link."
    • A match sends the request link to the address on file.
    • Rate limit: 5 attempts per IP per hour.

Should NOT see:

  • A street address for pickup. DECIDED 2026-09-28 (default adopted by orchestrator): do not publish the pickup address; give it in the "ready for pickup" email only.
  • Request status shown on this page.
  • Any sign of whether an order number exists.

4.11 Request-submitted confirmation​

The canonical wording is in tvg-proof-to-pay.md §2.1. This section mirrors it; if the two differ, that spec wins.

Shown immediately after a successful POST (retail or bulk). Screenshot: confirmation.png.

Should see:

  • The heading "Request TVG-000123 received". The order number always matches ^TVG-\d{6}$.
  • "Nothing has been charged."
  • "We'll email your proof to {email} within 1 business day. Nothing is made until you approve it."
  • The read-back of what they asked for (the same content as §4.5 (m)), with the product, maker, quantity and fulfilment choice.
  • IF a photo is still needed: "We still need your photo." with a button "Add your photo" linking to the request page.
  • A link: "View your request", to /request/{token}.
  • "Didn't get the email in 10 minutes? Check spam, or contact us with your order number."

Should NOT see:

  • "Order placed", "Order confirmed", "Thank you for your purchase", "Paid", or a receipt.
  • A tax amount or a "total".
  • A payment form or pay link.
  • Cross-sell products or "Share on Facebook".

Refresh and back:

  • Reloading the confirmation does not resubmit.
  • Pressing Back returns to the PDP with the form cleared. Values were stored per tab in sessionStorage (§4.12) and are cleared on success.

4.12 Returning customer and draft persistence (U4)​

  • Form values on a PDP persist in the same browser tab (sessionStorage) so Back/Forward doesn't lose typing. The photo file does not persist. Values are cleared after a successful submit.
  • A customer with a request in flight sees the storefront exactly as U1/U2 do. They manage the request only through /request/{token}, which is defined in tvg-proof-to-pay.md.
  • Submitting a second request for the same product creates a second, separate request. The storefront never merges requests.

5. Server contract: POST /api/tvg/order-requests​

This is written as behaviour, so QA can test it without reading the code.

CaseResponseRows created
Valid available product, valid personalization, C5 confirmed201 {order_number, request_url}exactly 1 tvg_order_requests + N tvg_order_items + ≥1 tvg_order_events
Any text field over its schema limit (for example 25 characters where the limit is 24), sent directly without the browser400 {"error":"validation","fields":{"line1":"max_length"}}0
Unsupported character, a 2-letter monogram, a verse translation not in the field's list, or a verse id not in tvg_verses400 with the field key0
C5 not confirmed (spelling_confirmed false or missing) on retail/bulk400 {"fields":{"spelling_confirmed":"required"}}0
Low-DPI photo without customer_acknowledged_low_res: true4000
Product example, any kind409 not_orderable0
Product coming_soon, kind retail or bulk_quote4090
Product coming_soon, kind waitlist, consent ticked2011 request (no items)
"Opening soon" mode (§3.7), any kind except waitlist409 not_open0
A client-sent price or total (tampered)Ignored; the server prices from the schemaPrice stored = server price
Honeypot filled201-shaped response (the bot sees success)0
Same form submission twice (double-click, retry) using the same Idempotency-KeySecond call returns the first responseexactly 1
More than 10 submissions per IP per 10 minutes4290
Shipping address with a non-Canadian province or bad postal code4000

Stored on success:

  • personalization values after NFC normalization and trimming, schema_version, a product_snapshot, and spelling_confirmed_at.
  • The final-sale disclosure text version shown (final_sale_ack_text_version). DECIDED 2026-09-28 (default adopted by orchestrator): the pre-request acknowledgement is stored on the request, as well as at proof approval. Both timestamps feed the dispute evidence in tvg-proof-to-pay.md §7.

QA fixtures. The QA checklist creates requests in production. DECIDED 2026-09-28 (default adopted by orchestrator):

  • Approve an unlisted QA product (is_demo_seed=true, status available, a hidden TVG QA maker, excluded from /shop, sitemap, catalogue API and payouts). It is reachable by direct URL only.
  • Add a request flag is_test (schema addition to architecture §2.2) so test rows never reach payouts or reports.

This is required before the §12 checks that create rows can run on the real host.


6. Accessibility requirements​

WCAG 2.2 AA is the bar. Each item is testable.

  1. Every input has a programmatic <label>. The limit and required status are inside the label text (§4.5 (d)).
  2. Counters have a visible text form. A polite aria-live region announces the remaining count after the user pauses (debounced to 1 second or more), and at the 80% and 100% thresholds. It does not announce every keystroke.
  3. Errors: inline, icon plus text, linked with aria-describedby, and naming the field and the fix (WCAG 3.3.1, 3.3.3). On a failed submit, focus moves to the first invalid field.
  4. The preview has alt text restating the content, updated on change. A polite "Preview updated" announcement is debounced. For photo products, C9 is readable text, not an image.
  5. Keyboard:
    • Every control is reachable in visual order.
    • Radio groups (translation, arrangement, colour, font, size, fulfilment) use arrow keys.
    • The monogram inputs auto-advance but allow Shift+Tab and Backspace across boxes.
    • The mobile menu opens and closes with Enter/Space/Escape and returns focus.
    • Focus is always visible, with at least a 3:1 outline.
  6. Reduced motion: with prefers-reduced-motion: reduce, there are no preview transitions, no smooth scrolling and no animated counters. Content is otherwise identical.
  7. Contrast: text is 4.5:1 or more and UI components 3:1 or more. Status labels are readable without colour (text always present). Sweet-corn yellow is never used as text on white (§3.3).
  8. Target size: at least 44 × 44 px for steppers, swatches, checkboxes (the clickable label counts) and the primary button.
  9. Language: <html lang="en-CA">.
  10. No time limits on the PDP form.
  11. Zoom to 200% causes no loss of content or function.

7. Mobile rules at 375 px (U1)​

  1. No horizontal page scroll at 375 × 667. Checked with document.documentElement.scrollWidth <= 375.
  2. Single column, 16 px side gutter.
  3. The pinned preview: once the shopper scrolls past the gallery into the personalization panel, a compact preview (at most 40% of viewport height) stays pinned at the top, so typing is visible without scrolling back up. It has a "Hide preview" toggle. It is not pinned for photo products (nothing to preview). [INFERENCE, per ux_research §3.1] Validate on a real phone before approval.
  4. Inputs use a font size of at least 16 px, so iOS doesn't zoom on focus. Email uses type="email", phone type="tel", quantity inputmode="numeric", and postal code autocapitalize="characters".
  5. The "Request a proof" button sits after the read-back and checkbox. It is not a sticky bar that hides the checkbox or the final-sale text. DECIDED 2026-09-28 (default adopted by orchestrator): no sticky bottom bar.
  6. Filter chips on /shop scroll horizontally inside their own row (not the page) or collapse into a "Filters" button that opens a sheet.
  7. The confirmation screen puts the order number and "Nothing has been charged." above the fold.

8. Honesty rules​

The consolidated list is in §3.4. Additional testable rules:

  • Every example item, everywhere it appears (tile, PDP, maker page, /shop?status= results), shows the "Example" label and C3. There are no exceptions for thumbnails.
  • No J & B Creations price, photo, review, address or phone appears in HTML, image files, alt text, JSON-LD or the catalogue API response.
  • JSON-LD:
    • Product markup has no aggregateRating or review fields.
    • offers appears only for available products.
    • Example products carry no offers.
  • A placeholder value (C12's $14.50, and any "Illustration" image) is labelled, and the site can't leave "Opening soon" mode while a [PLACEHOLDER] remains in customer-facing prices.

9. What the storefront shows per user state​

This is a quick matrix. The detail lives in §4.

SurfaceU1 phoneU2 desktopU3 B2BU4 in-flight
HeaderMenu button, no cartFull nav, no cartSame as U1/U2, and "Bulk orders" is visibleSame as U1/U2; nothing personal
PDP formStacked, pinned previewTwo columnsTier table and bulk link prominent when qty ≥ 10Blank form (no prefill from earlier requests)
Bulk pageStackedTwo columnsPrimary surface—
Find my requestFooter linkFooter linkFooter linkPrimary way back besides the email

10. FOUNDER DECISIONS in this spec​

Status: DECIDED = the orchestrator adopted the recommended default on 2026-09-28 (the founder can still override); OPEN = no default existed, or this item is explicitly reserved for the founder, the accountant or counsel.

#DecisionRecommended defaultStatus
D1Add an explicit register field to products; are unlisted occasions Main Street?Yes; yesDECIDED 2026-09-28
D2Keep "Proof by email within 1 business day" (C14)Keep, or change the stringOPEN — founder
D3Show BIN 1001760694 publiclyAbout page only, never labelled GST/HSTDECIDED 2026-09-28
D4Confirm the storefront character set against the production fontsLatin + accents + the listed punctuationDECIDED 2026-09-28
D5Character counting methodNFC, then code pointsDECIDED 2026-09-28
D6Wording of the couple monogram optionGender-neutral wording in §4.5 (h)DECIDED 2026-09-28
D7Optional news opt-inNone at launchDECIDED 2026-09-28
D8Full pre-request for coming_soonNo; waitlist onlyDECIDED 2026-09-28
D9Live-preview demo on example pagesNoDECIDED 2026-09-28
D10Bulk list: CSV upload; must quantity equal list lengthPaste only; warn, don't blockDECIDED 2026-09-28
D11Publish the pickup addressNo; send it in the ready-for-pickup emailDECIDED 2026-09-28
D12Contact email addresshello@thamesvalleygifts.ca, monitoredDECIDED 2026-09-28
D13ChurchWise Gifts self-agreement recordagreement_version='internal'DECIDED 2026-09-28
D14Blocking: Jackie Rayner's written OK for an example profileWithout it, J & B is not shownOPEN — founder/consent. Interim: placeholder maker cards use "Example maker: a Dorchester sublimation studio"
D15QA product + is_test flag in productionYes, needed for §12DECIDED 2026-09-28
D16Store the pre-request final-sale acknowledgementYesDECIDED 2026-09-28
D17Sticky mobile action barNoDECIDED 2026-09-28
D18Maker-recruitment linkNo at launchDECIDED 2026-09-28
D19Shipping rate (replaces the $14.50 placeholder)— (founder sets)OPEN — founder (no default exists)

11. [UNVERIFIED] items in this spec​

  • iOS HEIC → JPEG conversion with accept="image/jpeg,image/png,image/webp" (§4.5 (i)).
  • Carrier (Canada Post tracked) (§4.5 (o)).
  • The front-desk phone line exists (§4.8, §4.10).
  • Jackie Rayner's consent to an example profile, and her town (§4.4).
  • ChurchWiseAI LTD's 9-digit BN for the GST/HST number (§3.6).
  • Tax jurisdiction labels outside Ontario pending the accountant (§3.6).
  • Mobile pinned-preview behaviour is inference, not observed (§7).
  • Ontario as governing law in Terms (counsel) (§4.9).
  • thamesvalleygifts.ca registered and attached to the Vercel project (architecture R12).

12. QA checklist (run on https://thamesvalleygifts.ca, never on a preview host)​

Rules for running it:

  • Use Playwright against the real host (host-rewrite middleware is skipped on preview hosts).
  • Save artifacts to knowledge/acceptance/screenshots/tvg-storefront/ and the run zip.
  • "Behaviour" checks sample state at two timepoints and assert a change.
  • Every negative check is paired with a positive control, so the command has shown it can find what it looks for.
  • Row-creating checks use the D15 QA product only.

12.1 Host and global​

  • curl -I https://thamesvalleygifts.ca/ returns 200. www. returns a 308 to the apex. /tvg/shop on the brand host returns a 301 to /shop. (Owned by WP5; re-checked here.)
  • Every page in §4 has the exact footer text C1. Positive control: the assertion fails when run against a page fixture with the text altered.
  • Header has no element matching /cart|basket|sign in|account/i. Positive control: the same locator finds "Bulk orders".
  • Every page root has data-register. The memorial collection and a memorial PDP give keepsake; the drinkware collection gives main_street.
  • While in "Opening soon" mode: the banner is present and <meta name="robots" content="noindex"> is on every page.

12.2 Status gate​

  • Example product:
    • The PDP shows the "Example" label and the text C3.
    • getByRole('button', {name: 'Request a proof'}) has count 0. Positive control: count is 1 on the available QA product.
    • There is no price text matching /\$\d/ in the main region.
    • There are no input/select/textarea elements in the product form region.
  • Direct POST /api/tvg/order-requests for the example product id returns 409. The DB count of tvg_order_requests for that product is unchanged (read before and after).
  • Coming-soon product:
    • The waitlist form and C17 are present. "Request a proof" count is 0. There is no price.
    • POST with kind:'retail' returns 409.
    • The waitlist submit with consent creates exactly 1 row with kind='waitlist'.
  • A draft product URL returns 404. A paused product shows C18 and has no form.
  • Every /shop tile for an example item contains C3. The count of tiles with C3 equals the count of example products listed.

12.3 PDP behaviour (two timepoints)​

  • The preview changes when text changes.
    1. Type "Anna" into line 1 and read the preview SVG text: T1 = "Anna".
    2. Type " Smith" and read again: T2 = "Anna Smith".
    3. Assert T1 ≠ T2 and T2 contains "Anna Smith".
    4. Assert the preview alt or aria-label also contains "Anna Smith".
  • The 25th character is blocked (on a 24-character field):
    1. Type 30 characters and assert inputValue().length === 24. The counter reads "24 / 24 (limit reached)".
    2. Paste a 30-character string into the cleared field and assert the length is 24 and the message "Only the first 24 characters fit" is visible.
  • Tampered over-limit POST returns 400. Send POST /api/tvg/order-requests with a 25-character line1 straight from the test, bypassing the UI. Assert 400 with fields.line1, and the row count is unchanged.
  • Tampered price: POST with unit_price_cents: 1. The created row's price equals the server price. (Uses the QA product.)
  • Translation switch:
    1. Pick Psalm 23:1 with KJV; the preview text contains "The LORD is my shepherd".
    2. Switch to WEB; it now contains "Yahweh is my shepherd".
    3. C16 is visible without interaction.
    4. There is no option containing "NET".
  • Monogram: first=M, middle=A, last=J, Traditional. The preview reads "MJA", with the J glyph box larger than the M's. The read-back sentence contains "M J A".
  • Photo:
    • Upload a 800 × 600 px image on a 6 × 4 in product. The rating text is "Low resolution" and C10 is visible. Submit without ticking C10 shows an error and creates no row.
    • Upload a 2400 × 1600 px image: the rating is "Good" and C10 is absent.
    • Upload a 20 MB file: the size message appears.
  • Final-sale disclosure:
    • C4 is visible before the button.
    • C5 is unchecked on load.
    • Clicking "Request a proof" with C5 unchecked focuses the checkbox, shows "Please confirm you've checked the spelling.", and creates no row.
  • Estimated subtotal: read it at qty 1, then set qty 10 and read again. The values differ, and the qty-10 value equals the tier price × 10.
  • Quantity over max: set qty to max + 1. The stepper stays at max and the bulk link carries qty= in its href.
  • Reduced motion: with reducedMotion: 'reduce', type text. The preview updates, with no CSS transition or animation on the preview element (computed transition-duration is 0s).

12.4 Submission​

  • A submitted request creates exactly one row.
    1. Read the count of tvg_order_requests where is_test (QA product) = N.
    2. Fill the form validly and double-click "Request a proof".
    3. Assert the count is N+1, not N+2.
    4. Assert exactly one tvg_order_items row for that request.
    5. Assert the confirmation shows ^TVG-\d{6}$, the text "Nothing has been charged.", and no text matching /order placed|paid|purchase/i.
  • Reload the confirmation. The count is still N+1.
  • The honeypot filled: the response looks successful and the count is unchanged.

12.5 Accessibility and mobile​

  • axe-core finds zero serious or critical violations on home, shop, PDP (text, verse, monogram, photo), bulk and confirmation.
  • Keyboard only: tab from the page top to "Request a proof" on the text-product PDP. Every control receives visible focus. Arrow keys switch the translation radio.
  • The counter live region exists (aria-live="polite") and contains the remaining count after typing stops.
  • At 375 × 667: scrollWidth <= 375 on every §4 page. After scrolling into the form, the preview is still in the viewport (bounding box top ≥ 0 and bottom ≤ 0.4 × viewport height). Input font-size is at least 16 px.

12.6 Honesty sweep​

  • Full-site crawl text search for /review|rated|stars|bestseller|best seller|popular|trending|only \d+ left|people are viewing/i finds 0 matches. Positive control: the same search finds "Example listing" on an example page.
  • The JSON-LD on every PDP has no aggregateRating/review. Example PDPs have no offers.
  • The catalogue API and HTML contain no "Forest Grove", "601-3449", "377-1723" or "jbcreations883". Positive control: the search finds "J & B Creations".

13. Decision log (2026-09-28 editorial pass)​

Defaulted (DECIDED 2026-09-28, default adopted by orchestrator; the founder can still override): D1, D3, D4, D5, D6, D7, D8, D9, D10, D11, D12, D13, D15, D16, D17, D18 (§10); the GST/HST-number-via-env-var default (§3.6).

Left open:

  • D2 — the C14 "within 1 business day" proof promise: explicitly reserved for the founder.
  • D14 — Jackie Rayner's written consent to a public example profile: blocking, reserved for the founder/consent process. Interim rule added: until consent, the J & B page and all J & B example products stay unpublished (status='onboarding', hidden), and any placeholder maker card in mockups uses the neutral name "Example maker: a Dorchester sublimation studio".
  • D19 — the actual shipping rate (replacing the $14.50 placeholder): no default exists; the founder must set the real number.
  • Governing law (Ontario) in Terms, and the front-desk phone line's existence: unresolved [UNVERIFIED] items, not decision markers with defaults — left as-is.