{
 "id": "7.6",
 "title": "Data quality",
 "department": "Data and analytics",
 "department_slug": "data-and-analytics",
 "direction": null,
 "branded": false,
 "status": "draft",
 "owner": "Param",
 "last_verified": "Mon Oct 26",
 "holds": "Facts gate, numbers checked against data, freshness",
 "urls": {
  "html": "https://handbook.joinsquareup.com/data-and-analytics/data-quality",
  "markdown": "https://handbook.joinsquareup.com/data-and-analytics/data-quality.md",
  "json": "https://handbook.joinsquareup.com/data-and-analytics/data-quality.json"
 },
 "other_directions": [],
 "tldr": {
  "headline": "Every number must trace to a fact, stay fresh, never fake",
  "points": [
   {
    "icon": "lock",
    "text": "Twelve gates stop unbacked, stale, retired and dummy numbers before a card shows them."
   },
   {
    "icon": "clock",
    "text": "Connectors are read every 3 to 12 hours; three failed reads make a number stale."
   },
   {
    "icon": "check",
    "text": "Every fix works for every brand, and honesty tests run before every deploy."
   },
   {
    "icon": "warn",
    "text": "One out of stock 15ml variant once flagged 54 of Arata's Plays by mistake."
   },
   {
    "icon": "spark",
    "text": "New: a nightly data check per brand, listing unbacked numbers and stale links."
   }
  ],
  "stat": {
   "value": "54",
   "label": "Plays wrongly flagged 16 Oct"
  },
  "decide": "Run a nightly data check, and how long may a Play stay flagged?",
  "mood": "building"
 },
 "sources": [
  "brew/fact-gate.js:1-6, 65-88",
  "brew/facts-desk.js:1-5, 34-46",
  "brew/pack-numbers.js:1-4",
  "brew/health.js:5-7",
  "brew/connectors/index.js:37, 40, 51-61",
  "brew/SCOPE.md:50",
  "brew/DUMMY.md:27-30",
  "memory brew-connected-facts, brew-stories-and-cards, brew-proof-plain-make, brew-move-freshness, brew-qa-audit-2026-10-20, brew-audit-2026-10-15, feed-for-all-brands, fix-as-product"
 ],
 "open_questions": [
  "**Run a nightly data check?** Proposal: yes, one report to hello@ listing each partner's unbacked numbers and stale connections.",
  "**How long may a Play stay flagged?** Proposal: 7 days, then it leaves the feed until it is refreshed."
 ],
 "parts": {
  "Answer": "Every number a brand sees must trace to a fact, an account or a sum, stay fresh, and never pass a dummy or stale value as true; this page sets those gates and how we check them, for every brand at once.",
  "Today": "### The gates\n\n| Gate | What it stops | Source |\n|---|---|---|\n| Fact gate | A money amount or percentage on a card that the fact sheet does not back; cards written from Plays also gate multipliers and counts of a thousand or more | [src: brew/fact-gate.js:1-6] |\n| Plan numbers | A number in a sentence that says target, estimate, assumption or to confirm never backs a card | [src: brew/fact-gate.js:65-77] |\n| Retired numbers | A number that stopped being true, a product gone or a price changed, is cut from Plays before they back a card | [src: brew/fact-gate.js:79-88] |\n| One number, one source | The headline numbers are worked out once, the same way for every brand | [src: brew/pack-numbers.js:1-4] |\n| The fact sheet | The team corrects it with choices or a pasted link, never a typed number; a free daily check keeps prices and stock in line; each edit is recorded and flags the Plays that quote the old value | [src: brew/facts-desk.js:1-5] |\n| Connected truth | For members, a connected number beats the site's; a stale or simulated one never counts | [src: brew/facts-desk.js:34-46] [src: memory brew-connected-facts] |\n| Not read is not zero | A health part or ring that could not be read is left out, never scored as zero | [src: brew/health.js:5-7] |\n| Stories | Quote only numbers in their data; a failed source says so on its ring | [src: memory brew-stories-and-cards] |\n| Proof sheet | Every card's numbers matched to a fact line, account, rival ad, quote or sum, with link and date; unmatched numbers are named; a Play is never proof | [src: memory brew-proof-plain-make] |\n| Whole Plays | A Play missing any of its six sections is not saved | [src: memory brew-qa-audit-2026-10-20] |\n| Weekly Plays | Marked verified only when every number is backed | [src: brew/SCOPE.md:50] |\n| Dummies | Simulated rows keep a demo flag and stay out of Plays, the fact sheet, briefs and stale flags | [src: brew/DUMMY.md:27-30] |\n\n### Freshness\n\n| Connector | Read every | Read again when a member opens, if older than | Source |\n|---|---|---|---|\n| Shopify | 3 h | 1 h | [src: brew/connectors/index.js:37, 40] |\n| Meta | 3 h | 1 h | [src: brew/connectors/index.js:37, 40] |\n| Google Ads, GA4 | 6 h | 2 h | [src: brew/connectors/index.js:37, 40] |\n| Judge.me | 6 h | 2 h | [src: brew/connectors/index.js:37, 40] |\n| Klaviyo | 12 h | 6 h | [src: brew/connectors/index.js:37, 40] |\n\n- Stale after 3 failed reads in a row, or no good read for four cadences and at least 24 hours; stale numbers stop being evidence. [src: brew/connectors/index.js:51-61]\n- A connected number that changes by 10% from the value a Play was written with flags that Play; flagged Plays refresh within a weekly budget. [src: memory brew-move-freshness]\n- A product out of stock or gone flags every Play naming it at once, with no AI. [src: memory brew-move-freshness]\n\n### Past failures and the lesson each left\n\n| Date | What went wrong | Lesson | Source |\n|---|---|---|---|\n| 16 Oct | One 15ml variant out of stock flagged 54 of Arata's Plays: a short name matched the 200ml | Check a matcher against real brand data, variants included | [src: memory brew-move-freshness] |\n| 16 Oct | Arata's connected ledger held simulated Shopify data with no flag | Simulated rows always carry the demo flag | [src: memory brew-audit-2026-10-15] |\n| 16 Oct | Cozy was set to India though it sells in the US, so its Plays were written for India | Country comes from the store, never a default | [src: memory brew-audit-2026-10-15] |",
  "Tomorrow": "| Item | Version | Tag |\n|---|---|---|\n| Every gate above, for every brand | v1 | Stays |\n| A nightly data check per brand: unbacked numbers, stale connections, Plays flagged too long | v1 | New, proposed |\n| A post never goes out with an unbacked number | v2 | New |\n| Results read only from fresh, non simulated account rows | v3 | New |\n| Ad rules refuse to act on stale numbers | v4 | New |",
  "How we do it": "1. Every fix works for every brand, every card and every view, and heals saved data on read. [src: memory feed-for-all-brands] [src: memory fix-as-product]\n2. Audit what the product shows against the platform, read only:\n\n```bash\ncd /Users/param/Documents/ChatGPT/brew\nnode tools/audit-cards.mjs --bucket\nFEED_VAULT_KEY=$(gcloud secrets versions access latest --secret=FEED_VAULT_KEY) node tools/meta-audit.mjs <slug>\nFEED_VAULT_KEY=$(gcloud secrets versions access latest --secret=FEED_VAULT_KEY) node tools/shopify-audit.mjs <slug>\nnode tools/audit-brand-book.mjs <dir with a copy of the brand files>\n```\n\nThe two account audits read with staging's vault key; a brand at home on production needs `FEED_VAULT_KEY_PROD`. All four write nothing. [src: brew/tools/meta-audit.mjs:1-3] [src: brew/tools/shopify-audit.mjs:1-3] [src: brew/tools/audit-cards.mjs:1-6]\n\n3. Run the honesty and number tests before every deploy:\n\n```bash\nAI_MOCK=1 node --test test/honesty.test.js test/claims.test.js\n```\n\n4. A new connector needs entries in `EVERY_H`, `OPEN_H`, `NAMES` and `PRECEDENCE`. [src: memory brew-connected-facts]",
  "Numbers": "| Metric | Definition | Where measured | Status |\n|---|---|---|---|\n| Unbacked numbers | Numbers on shown cards with no proof match, per brand | Proof sheet counts | Proposed |\n| Stale connections | Connections marked stale | `connections.json`; `db/status.mjs` | Live data, not reported |\n| Facts to confirm | Facts marked to confirm and not yet corrected | The fact sheet | Proposed |\n| Days flagged | Days a Play stays flagged before its refresh | Updates per brand | Proposed |",
  "Open": "1. **Run a nightly data check?** Proposal: yes, one report to hello@ listing each partner's unbacked numbers and stale connections.\n2. **How long may a Play stay flagged?** Proposal: 7 days, then it leaves the feed until it is refreshed."
 }
}