Open-Source Cold Email & Warmup Platform
Warmbly is the open-source platform for cold email: warm your mailboxes, send campaigns, and triage every reply. Run it on our cloud or self-host the whole thing.
ทุกเช้าได้สามงานที่ควรทำ พร้อมกลุ่มลูกค้า ข้อความ และวิธีวัดว่าคุ้มจริงไหม
Translation pending
MarketingTypeScript · HTMLGitHub @ratthakansOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗เว็บไซต์เต็มรูปแบบของ PARALLAX (Agentic Lead Generation & CRM Platform) เนื้อหาทั้งหมดเป็นภาษาไทย ตามทะเบียนเสียงที่กำหนดไว้ใน Master Blueprint v2
Next.js 16 (App Router · Turbopack) · React 19 · Tailwind CSS 4 · TypeScript
npm run dev
เปิด http://localhost:3040 (พอร์ตกำหนดไว้ใน .claude/launch.json ของโฟลเดอร์แม่)
npm run build && npm start
| เส้นทาง | เนื้อหา | อ้างอิง blueprint |
|---|---|---|
/ | หน้าแรก — ตำแหน่ง เครื่องยนต์ บรีฟเช้า และหลักฐาน | §0, 3, 6–10 |
/problem | ปัญหา สถิติ 2569 แรงที่เปลี่ยนตลาด และตำแหน่ง | §1, 2, 3 |
/platform | สถาปัตยกรรมห้าชั้น สามแกน การกำกับ มาร์จิ้น | §6, 11, 12, 22 |
/platform/keep | Keep — ห้าขั้นตอนและกลุ่มที่ระบบคืนมา | §7 |
/platform/reach | Reach — ห้าขั้นตอนและท่าทีต่อข้อมูล | §8 |
/platform/proof | Proof — กติกาวัดตามขนาดกลุ่ม และ ROI Tracker | §10 |
/platform/learning | การเรียนรู้ข้ามลูกค้า — คูเมืองที่ซื้อทางลัดไม่ได้ | §9 |
/product | ระบบชื่อสินค้า บรีฟเช้า ฟีเจอร์ทั้งหมด การเริ่มใช้ | §4, 13 |
/demo | เดโมสาธารณะของบรีฟเช้า เปิดได้ทันทีโดยไม่ต้องเข้าสู่ระบบ | — |
/customers | สี่รูปทรงวงจร เกณฑ์คัด และวิธีเจาะตลาด | §5 |
/playbook | MST Golf — ต้นแบบที่ต่อข้อมูลแล้ว | §15 |
/pricing | สี่ชั้นราคาและสิ่งที่ตั้งใจไม่ทำ | §17 |
/company | ชื่อ แบรนด์ สองทะเบียนเสียง ทีม แผนเดินทาง ภูมิศาสตร์ | §4, 20, 22, 23, 24 |
/investors | GTM ขนาดตลาด เศรษฐศาสตร์ต่อบัญชี BMC ความเสี่ยง | §14, 16, 18, 19, 21, ภาคผนวก |
/trust | PDPA ความยินยอม เพดานการส่ง | §11 |
/contact | แบบฟอร์มและช่องทางอีเมล | — |
/console | ประตูทางเข้าของ webapp — จะถูกแทนที่ในเฟสถัดไป | — |
/sitemap.xml · /robots.txt · /opengraph-image · /icon.svg สร้างอัตโนมัติ
/console ถูกตั้ง noindex ไว้แล้ว
POST /api/contact ตรวจสอบข้อมูลครบถ้วน แต่ ยังไม่มีปลายทาง
ถ้าไม่ตั้งค่า CONTACT_WEBHOOK_URL ระบบจะเขียน log บนเซิร์ฟเวอร์อย่างเดียว
และผู้ส่งจะเห็นหน้าขอบคุณ — แปลว่า lead จะหายไป
# .env.local
CONTACT_WEBHOOK_URL="https://hooks.zapier.com/..." # หรือ Make / Slack / n8n / API ของคุณเอง
ANTHROPIC_API_KEY="sk-ant-..." # ไม่ตั้งก็ใช้ได้ แต่ AI จะถอยไปใช้สูตรสำเร็จ
ANTHROPIC_API_KEY ไม่ใช่ของบังคับ — ถ้าไม่ตั้ง Console ยังทำงานครบทุกหน้า
โดยสลับไปใช้สูตรที่ deterministic และติดป้ายบอกไว้ทุกจุดที่ใช้งาน
รายละเอียดอยู่ที่หัวข้อ AI ใช้ตรงไหน
payload ที่ส่งไปยัง webhook:
{
"name": "", "company": "", "email": "", "phone": "",
"cycle": "", "baseSize": "", "topic": "", "message": "",
"receivedAt": "ISO-8601"
}
ถ้า webhook ตอบไม่ใช่ 2xx ระบบจะคืน 502 และหน้าเว็บจะแนะนำให้อีเมลหา
[email protected] แทน
ทั้งหมดอยู่ใน app/globals.css เป็น token และ utility ไม่ใช่คลาสกระจัดกระจาย
ทิศทาง — light-first พื้นหลักคือขาวและฟ้าอ่อน ตัวหนังสือเป็น navy เข้ม พื้นเข้มใช้เป็น accent เท่านั้น (ปกติ 1–2 บล็อกต่อหน้า และ footer) พาเลตต์เย็นล้วน ไม่มีโทนอุ่นเลย
สี — ink / ink-2 / ink-3 / ink-4 (navy สำหรับตัวหนังสือ) ·
paper / paper-2 / line (พื้นสว่างและเส้นไรผม) ·
signal #0047ff · azure · sky · cyan (แกนน้ำเงินอิ่ม) ·
abyss #00113d (พื้น accent) · frost (ตัวหนังสือบนพื้น accent)
ตัวอักษร — sans ล้วน ไม่มี serif
IBM Plex Sans Thai (เนื้อหาและตัวเลข) · IBM Plex Mono (ป้ายกำกับ)
ใช้ผ่าน utility t-display t-h1 t-h2 t-h3 t-lead t-body t-small
t-label t-numeral (น้ำหนัก 300 + tabular figures) t-thai
สนามไล่สี (CI) — <Field variant="aurora" | "wash" | "deep" />
aurora คือพื้นสว่างสำหรับ hero — ขาวฝั่งที่วางตัวหนังสือ ไล่ไปฟ้าอิ่มและ navy
ที่มุมขวาล่าง · wash คือเวอร์ชันเบาสำหรับ section ทั่วไป · deep คือ accent เข้ม
.field__blob เป็นตัวครอบที่รับ animation ส่วน <i> ข้างในรับ gradient และ
blur แยกกันเพื่อให้ compositor ใช้ blur ที่ raster ไว้แล้วซ้ำ แทนที่จะเบลอ
ก้อนขนาดพันพิกเซลใหม่ทุกเฟรม ขนาดคิดเป็น vmax เพื่อให้สีน้ำเงินยังมีน้ำหนัก
บนจอมือถือ ทุก animation ปิดเองเมื่อผู้ใช้เปิด prefers-reduced-motion
เกรน — .grain--light ใช้ soft-light จึงเกาะเฉพาะบริเวณที่มีสี
และปล่อยพื้นขาวไว้สะอาด · .grain--dark ใช้ overlay บนพื้น accent
แถบตัวเลขแบบ Stripe — <StatBand items={[{value,label,sub}]} />
เส้นไรผมคั่นทุกคอลัมน์ ตัวเลขใหญ่น้ำหนักบาง วางบนพื้นขาวเสมอ ไม่วางทับ gradient
เลย์เอาต์ — shell (คอนเทนเนอร์) · band / band-sm (ระยะห่างแนวตั้ง) ·
surface-dark / surface-paper / surface-mist
ทุกตัวอยู่ใน components/reveal.tsx (client) และ app/globals.css
เข้ามาเมื่อเลื่อนถึงครั้งเดียว แล้วเลิกสังเกต — การเคลื่อนไหวที่วนซ้ำ
ทุกครั้งที่เลื่อนผ่านเป็นสิ่งกวนใจตั้งแต่ครั้งที่สอง
| ตัว | ใช้ตอน |
|---|---|
Reveal + dir | up · left · right · scale · fade — คอลัมน์ซ้ายเข้าจากซ้าย กราฟิกเข้าแบบ scale จึงรู้สึกเป็นการจัดวาง ไม่ใช่เอฟเฟกต์ที่แปะทับ |
Stagger | ลูกในกริดเข้าไล่กัน แทน delay={i * 70} ที่เคยเขียนมือกระจายสิบกว่าจุดด้วยค่าไม่ตรงกัน |
Numeral | ตัวเลขไล่ขึ้นเมื่อเลื่อนถึง อยู่ใน Stat/StatBand จึงได้ทั้งเว็บ · แยก prefix/suffix เอง (฿786,800 · 12.49% · 6×) และค่าที่ไล่อยู่ถูกซ่อนจากโปรแกรมอ่านหน้าจอ |
LineReveal | หัวเรื่องเข้าทีละบรรทัด แบ่งด้วย \n ที่ส่งเข้ามา ไม่ใช่เดาจากความกว้าง (ซึ่งจะแตกคนละที่ทุกขนาดจอ) |
ReadingProgress | แถบบางบนสุด · rAF + listener แบบ passive |
More | รายละเอียดที่พับไว้ — details/summary จริง จึงค้นด้วย ⌘F ได้ |
ตัวกันเนื้อหาหายถาวร — สถานะตั้งต้นของทุกตัวคือ opacity: 0
ถ้า IntersectionObserver ไม่เคยรายงาน ผลไม่ใช่ "ไม่มีอนิเมชัน" แต่คือหน้าว่าง
และเกิดขึ้นจริง: แท็บที่อยู่เบื้องหลังมี visibilityState = hidden
จน observer ไม่รายงานการตัดกัน (เจอตอนทดสอบ ทั้งหน้าค้างที่ opacity 0)
จึงตั้งเวลาปลดล็อกไว้ 1.5 วินาที และเพิ่ม @media print ที่บังคับให้
ทุกอย่างมองเห็นและคลี่ตัวพับออก — การเคลื่อนไหวเป็นของเสริม เนื้อหาไม่ใช่
ทุกตัวเคารพ prefers-reduced-motion ทั้งทาง CSS และทาง JS —
ตัวเลขที่ไล่ขึ้นต้องขึ้นค่าสุดท้ายทันที ไม่ใช่ไล่เร็วขึ้น
ข้อความบนเว็บถูกตัดลงราว 11% โดยเฉลี่ย และ 14–25% ในหน้าที่หนักที่สุด (หน้าแรก · ราคา · platform · investors) ด้วยสามวิธี เรียงตามลำดับที่ควรลอง:
Moreกติกาของข้อ 3: พับได้เมื่อบรรทัดที่ยังเห็นอยู่สมบูรณ์ในตัว คำถาม FAQ พับคำตอบได้ · ห้าชั้นของ platform พับได้เพราะมี one-liner อยู่แล้ว แต่การ์ดที่มีแค่หัวเรื่องพับไม่ได้ — ครั้งแรกที่ลองพับคำอธิบายการ์ดทุกใบ ได้กริดที่มีแต่ชื่อกับปุ่ม "เพราะ" ห้อยอยู่ ซึ่งอ่านเหมือนหน้าโหลดไม่เสร็จ จึงคืนคำอธิบายเหล่านั้นให้เห็น แล้วไปตัดความยาวมันแทน
components/ui.tsx รวม primitive ทั้งหมด — Field Mark Wordmark Label
Section PageHero StatBand Stat Quote Note Card Steps ArrowLink
NextUp CTA
| ไฟล์ | หน้าที่ |
|---|---|
convergence.tsx | กราฟิกลายเซ็น — เส้นสายตาสองชุดจากสองตำแหน่ง ทอดไปยังกลุ่มลูกค้าเดียวกัน จุดที่บรรจบคือจุดที่รู้ระยะจริง สุ่มแบบ deterministic (hashed sine) เพื่อให้ server กับ client วาดตรงกัน |
site-header.tsx | เมนูหลัก dropdown และ drawer มือถือ (client) |
site-footer.tsx | ท้ายเว็บ — บล็อก accent เข้ม |
nav.ts | โครงเมนู — แก้ที่เดียว มีผลทั้ง header |
reveal.tsx | fade-up ตอนเลื่อนถึง ผ่าน IntersectionObserver (client) |
brief-mock.tsx | ภาพจำลองบรีฟเช้า ใช้ซ้ำ 4 หน้า อ่านได้ทั้งบนพื้นสว่างและพื้น accent |
engine-loop.tsx | ไดอะแกรม Keep → Reach → Proof |
contact-form.tsx | ฟอร์มติดต่อพร้อม validation (client) |
/appแอปจริงที่ใช้งานได้ ไม่ใช่ภาพ mockup โทนเข้ม แยก token ออกจากเว็บการตลาด
ทั้งหมด (app/(app)/console.css) เว็บอยู่ใน route group (site)
Console อยู่ใน (app) จึงไม่มี header/footer ของเว็บมาปน
/console เดิม redirect มาที่ /app
| หน้า | ทำอะไร |
|---|---|
/app | บรีฟเช้า — สามอย่างที่ควรทำวันนี้ พร้อมปุ่มอนุมัติและโหมดซ้อม |
/app/plays | คลัง play 23 ตัว เปิดปิดรายตัว ปรับ guard ดูนิยามเป็น YAML |
/app/campaigns | แคมเปญทั้งหมด และหน้ารายละเอียดที่ตรวจ arm ซ้ำได้ |
/app/proof | ส่วนต่าง verdict สามค่า และต้นทุนต่อลูกค้าที่กลับมาซื้อซ้ำ |
/app/customers | ตาราง feature ที่คำนวณล่วงหน้า |
/app/import | ลากไฟล์ CSV จาก POS วาง แม็ปคอลัมน์ด้วย AI แล้วนำเข้า |
/app/settings | เพดานที่บังคับใช้ · สถานะ AI และแคช · เครื่องมือเดโม · บันทึกการใช้งาน |
งานหลักคือวงจรเดียว และตั้งใจให้จบใน สองคลิก
บรีฟเช้า ──คลิก "อนุมัติ — ส่งราว N คน"──▶ หน้าแคมเปญนั้น
(แถบ "เหลืออีกขั้นเดียว")
──คลิก "ส่งข้อความ — N คน"────▶ "ส่งแล้ว รออ่านผล"
+ วันที่จะอ่านผลได้
สิ่งที่แก้จากรอบก่อน เพราะ flow เดิมตันกลางทาง:
hash(customer_id + campaign_id) และ campaign_id ยังไม่เกิดจนกดอนุมัติ
(เคยเห็น 921 บนปุ่มแล้วส่งจริง 943)status เป็น measuring ตั้งแต่ตอนอนุมัติ ตอนนี้ขึ้น
"ยังไม่ได้ส่ง" พร้อมแถบสรุปด้านบนว่ามีกี่แคมเปญ กี่คนที่ยังไม่ได้รับ฿551,155 ต้องการ 108px จึงถูกตัดใช้ node:sqlite ที่มากับ Node 22+ จึงไม่มี native dependency ไฟล์อยู่ที่
.data/parallax.db (อยู่ใน .gitignore แล้ว) ทุกการเข้าถึงผ่าน
lib/db.ts เท่านั้น — เปลี่ยนไป Postgres ตอนขึ้น production แก้ไฟล์เดียว
schema ตรงตาม Play Engine §5 · §6 · §7
เปิด /app ครั้งแรกระบบจะ seed ทั้งสี่บัญชีและคำนวณ feature ให้เอง
(ใช้เวลาราว 4–5 วินาที ครั้งเดียว) ชุดข้อมูลสุ่มแบบ deterministic —
reseed แล้วได้ตัวเลขเดิมทุกครั้ง และ MST Golf ตรงกับตัวเลขใน Playbook
ทั้งหก: ฐาน 1,240 · เคยซื้อ 999 · ซื้อซ้ำ 707 · เข้ามาใน 30 วัน 248 ·
เงียบเกิน 90 วัน 585 · บนสุด 2% ทำรายได้ 41%
เลือกให้ครบทั้งสี่ "รูปทรงวงจรรายได้" เพราะข้อความหลักของ PARALLAX คือ engine ทำนายจากวงจร ไม่ใช่จากอุตสาหกรรม — ถ้าเดโมมีแต่ร้านค้าปลีก ข้อความนั้นพิสูจน์ไม่ได้ สลับบัญชีได้จากปุ่ม “สลับ” ในแถบซ้าย
| บัญชี | ธุรกิจ | วงจร | ฐาน | play ที่ใช้ได้ |
|---|---|---|---|---|
| MST Golf | ค้าปลีกอุปกรณ์กอล์ฟ | ตัดสินใจนาน | 1,240 | 19/23 |
| HONG MOVE | แท็กซี่ EV สนามบินหาดใหญ่ | ถึงรอบ | 3,180 | 16/23 |
| Bangpakong Riverside CC | สนามกอล์ฟ 18 หลุม | เติมของ | 2,460 | 19/23 |
| พรรคประชาธิปัตย์ | งานทะเบียนสมาชิกพรรค | หมดอายุ | 26,000 | 11/23 |
โปรไฟล์ทั้งหมดอยู่ใน lib/tenants.ts เป็น ข้อมูล เพิ่มบัญชีใหม่คือ
เพิ่มแถว ไม่ต้องแก้ตัวสร้างข้อมูล เส้นแบ่งเดียวกับคลัง play
play ถูกกรองด้วยรูปทรงวงจรของบัญชี ไม่ใช่รันทุกตัวให้ทุกคน — บริษัทแท็กซี่จึงไม่ได้รับข้อเสนอ “ล้างสต๊อกค้าง” และพรรคการเมืองไม่ได้รับ “ขายพ่วงหลังซื้อของหลัก” ตัวเลข posterior ที่สืบทอดข้ามบัญชีก็อ่านเฉพาะ วงจรเดียวกัน ไม่ใช่แถวไหนก็ได้
คำเรียกเปลี่ยนตามบัญชี — ลูกค้า · ผู้โดยสาร · นักกอล์ฟ · สมาชิก ทั้งในหน้าจอและใน prompt ที่ส่งให้ AI เขียนข้อความ ถ้าเขียน “ลูกค้า” ตายตัว ข้อความที่พรรคส่งหาสมาชิกจะอ่านเหมือนโปรโมชันร้านค้า
ขอบเขตของบัญชีพรรคการเมืองถูกจำกัดโดยเจตนา — เป็นงานธุรการสมาชิก เท่านั้น (ต่ออายุค่าบำรุง · สมาชิกที่ขาดต่อ · ชวนร่วมกิจกรรม · ดูแลผู้บริจาค) ไม่ใช่เครื่องมือหาเสียงหรือโน้มน้าวผู้ออกเสียง และไม่มี play ใดที่แบ่งกลุ่ม ตามความคิดเห็นทางการเมือง คอนโซลขึ้นเตือนไว้บนหน้าบรีฟว่าความคิดเห็น ทางการเมืองเป็นข้อมูลอ่อนไหวตาม PDPA มาตรา 26 บัญชีนี้จึงตั้งเพดานไว้ที่ สัปดาห์ละหนึ่งข้อความและห้ามส่วนลดทุกกรณี
| ไฟล์ | ขั้น |
|---|---|
lib/plays.ts | คลัง play เป็นข้อมูล 15 KEEP + 8 REACH |
lib/seed.ts | INGEST — ชุดข้อมูลตัวอย่าง |
lib/derive.ts | DERIVE — คำนวณ feature รายคนล่วงหน้า |
lib/match.ts | MATCH — รัน selector ทุก play แล้วจัดอันดับ ไม่มี LLM |
lib/dispatch.ts | DISPATCH — แช่แข็ง audience แบ่ง arm ส่งแบบ idempotent |
lib/proof.ts | PROOF — attribution T+7/30/90 · verdict · ROI 90 วัน |
lib/ai.ts | ชั้น LLM ทั้งหมด มีทางถอยเป็นสูตรสำเร็จเมื่อไม่มีคีย์ |
lib/ingest.ts | อ่าน CSV จริง · normalise เบอร์/วันที่/เงิน · เขียนเข้าฐาน |
lib/demo.ts | สร้างประวัติแคมเปญ · เดินเวลา · ใส่ผลของแคมเปญลงข้อมูลจริง |
lib/tenants.ts | ทะเบียนสี่บัญชีตัวอย่าง — โปรไฟล์เป็นข้อมูล |
lib/active-tenant.ts | บัญชีที่เปิดอยู่ (cookie) — ของเดโม ไม่ใช่การแยกผู้เช่า |
verify/ | ชุดตรวจ invariant รันด้วย npm run verify |
ต่อกับ Claude ผ่าน @anthropic-ai/sdk โมเดล claude-opus-5 ใช้ structured
output (output_config.format) ทุกจุด ตั้ง ANTHROPIC_API_KEY ใน .env.local
แล้วรีสตาร์ท ถ้าไม่ตั้ง ระบบยังทำงานครบทุกหน้าจอ โดยสลับไปใช้สูตรสำเร็จ
ที่ deterministic แทน และติดป้าย "ข้อความสำเร็จรูป" ให้เห็นตรงที่ใช้งาน
ผลลัพธ์ทุกครั้งถูกแคชด้วย hash ของ input ในตาราง ai_cache เรียกซ้ำไม่เสียเงินซ้ำ
ดูสถานะและล้างแคชได้ที่ /app/settings
| ใช้ AI | เพราะ |
|---|---|
| แม็ปคอลัมน์ CSV | หัวคอลัมน์ของ POS แต่ละยี่ห้อไม่เหมือนกัน เขียนกฎตายตัวไม่ได้ |
| เดาบทบาทสินค้า | ต้องรู้ว่าอะไรคือของหลัก อะไรคือของที่ซื้อตาม |
| เขียนข้อความสามโทน | เรียกครั้งเดียวต่อแคมเปญ ไม่ใช่ต่อคน (F3) — ตัวกำหนดมาร์จิ้น |
| สรุปบรีฟเช้า | เจ้าของร้านไม่ต้องรู้ว่า RFM คืออะไร |
| อธิบายว่าทำไมแนะนำ | ต้องตอบได้ทุกครั้งว่าทำไมส่งหาคนนี้ (D7) |
ห้ามใช้ AI: การเลือกคนเข้ากลุ่ม · การให้คะแนนและจัดอันดับ play · การคำนวณส่วนต่างและช่วงความเชื่อมั่น · การตัดสินส่งหรือไม่ส่ง · การบังคับ cooldown และเพดานความถี่ ทั้งหมดเป็นคณิตศาสตร์ล้วน เพราะต้องรันซ้ำได้ผลเดิม อธิบายได้ทุกบรรทัด และไม่มีต้นทุนต่อการรัน
npm run typecheck # tsc --noEmit
npm run verify # H1 แช่แข็งกลุ่ม · H2 hash arm · F12 idempotency · B8 ความยินยอม · verdict สามค่า
npm run verify:caps # เพดานความถี่ · ช่วงห้ามส่ง · ล็อกกลุ่มควบคุม · เครดิตไม่ติดลบ
npm run verify:tenants # ทั้งสี่บัญชี: ขนาดฐานตรงเป้า · play ตรงวงจร · ความยินยอม · ฐานไม่ปนกัน
npm run verify:pricing # เพดานแผน · รอบบิล · บัญชีเครดิตกระทบยอด · ตัวหาร ROI ครอบชุดเดียวกับตัวตั้ง
ทั้งสี่ชุดรันบนฐานข้อมูลจริง (ลบ .data/ ก่อนถ้าต้องการเริ่มใหม่)
และตรวจพฤติกรรมจริงไม่ใช่ mock — ชุดนี้เป็นตัวที่จับบั๊กความยินยอมรั่ว
เข้าไปใน audience ที่ export และเพดานความถี่ที่ไม่เคยทำงานได้จริง
verify:pricing มีหลักข้อเดียว: เพดานทุกข้อที่หน้า /pricing ประกาศ
ต้องมีบรรทัดที่พิสูจน์ว่าระบบปฏิเสธจริงเมื่อถูกละเมิด ไม่ใช่แค่มี
ฟังก์ชันที่คืนข้อความไว้ — เช่น เรียก approveCampaign บนแผน Free
แล้วต้อง throw และต้องไม่มีแถวแคมเปญหลุดลงฐาน
ถ้าสี่ตัวนี้ไม่ได้ทำตั้งแต่วันแรก อีกร้อยฟีเจอร์จะพังทีหลังพร้อมกัน ทั้งสี่อยู่ในระบบแล้วและมีชุดตรวจยืนยัน
(campaign_id, customer_id) — กดส่งซ้ำไม่เพิ่มข้อความhash(customer_id + campaign_id) — หน้ารายละเอียดแคมเปญคำนวณซ้ำให้ดูว่าตรงกันความยินยอมบังคับกับทุก play รวมรายชื่อตัดออก — spec §10 บอกว่าส่งออก
audience โดยไม่กรองความยินยอมผิด PDPA แต่ §3 ให้ R6 เลือกคนที่ถอนความยินยอม
สองข้อนี้ขัดกัน จึงตีความว่า paid_only = ยินยอมอยู่แต่ไม่มีช่องทางตรง
(ไม่มี LINE) และคนที่ถอนความยินยอมเป็น none ห้ามทั้งส่งและส่งออก
ผลข้างเคียงที่ยอมรับ: คนที่ถอนจะไม่อยู่ในรายชื่อตัดออก จึงอาจยังเห็นโฆษณา
แยกล็อกกลุ่มควบคุมออกจากล็อกหน้าต่างตอบสนอง — spec ให้กรอง "คนที่ติดอยู่ใน แคมเปญที่ยังไม่จบ" ออก แต่ถ้าล็อกยาวเท่าหน้าต่างวัดผล 90 วัน จะทักลูกค้าได้ ปีละสี่ครั้ง จึงล็อกกลุ่ม treated แค่ 14 วันแล้วปล่อยให้เพดานรายสัปดาห์คุม ส่วนกลุ่ม holdout ล็อกตลอดหน้าต่างวัดผล เพราะถ้าถูกแตะกลุ่มควบคุมจะปนเปื้อน และส่วนต่างที่วัดได้จะไม่มีความหมาย
ตัวชี้วัดที่ตัดสินคือสัดส่วนคนที่กลับมาซื้อ ไม่ใช่ยอดต่อหัว — เดิมทดสอบ ส่วนต่างของยอดต่อหัว ซึ่งในค้าปลีกมีส่วนเบี่ยงเบนสูงกว่าค่าเฉลี่ยหลายเท่า (คนส่วนใหญ่ซื้อศูนย์บาท ไม่กี่คนซื้อหลายหมื่น) ผลคือช่วงความเชื่อมั่น คร่อมศูนย์ทุกครั้งจนกลุ่มถึงระดับหลายแสนคน — verdict เป็น "ยังไม่พอสรุป" ตลอดกาลทุกขนาดกลุ่ม ซึ่งไม่ใช่ความระมัดระวังแต่คือเครื่องวัดที่วัดอะไรไม่ได้ ตอนนี้ทดสอบสัดส่วนซื้อ/ไม่ซื้อ (ความแปรปรวนถูกจำกัดด้วย p(1−p)) จึงสรุปได้ ที่กลุ่มระดับพันคน ส่วนตัวเลขบาทคือส่วนต่างสัดส่วน × ค่ากลาง (median) ของยอดต่อคนที่ซื้อ — ใช้ median ไม่ใช่ค่าเฉลี่ย เพราะลูกค้ารายใหญ่ไม่กี่คน ลากค่าเฉลี่ยขึ้นได้หลายเท่า และคนที่แคมเปญเปลี่ยนใจได้ไม่ใช่คนกลุ่มนั้น
โหมด pooled_90d_holdout รวมผลจริง — โหมดนี้มีอยู่เพราะกลุ่ม 300–1,000 คน
เล็กเกินกว่าจะสรุปรายแคมเปญ แต่เดิมโค้ดวัดแยกรายแคมเปญแล้วประทับ
"ยังไม่พอสรุป" ให้ทุกช่วงที่ไม่ใช่ T+90 คือสัญญากำลังทางสถิติที่ไม่เคยส่งมอบ
ตอนนี้ที่ T+90 รวมกลุ่มทดสอบและกลุ่มควบคุมของทุกแคมเปญโหมดนี้ที่ครบกำหนด
เข้าด้วยกันแล้วสรุปครั้งเดียว แถวที่แชร์ผลกันมีคำกำกับไว้บนหน้า Proof
หน้าต่าง ROI 90 วันนับจากวันที่ผลออก ไม่ใช่วันอนุมัติ — เดิมกรอง
approved_at >= now-90d แต่ verdict ที่ T+90 ต้องรอให้แคมเปญอายุครบ 90 วัน
สองเงื่อนไขแทบไม่มีจุดตัดกัน ตัวเลขเรือธงจึงว่างเปล่าโดยโครงสร้าง
และเลือก "ช่วงที่สุกที่สุดที่สรุปได้" ต่อแคมเปญ (90 → 30 → 7)
ไม่ทิ้งผล T+30 ที่ใช้ได้จริงเพราะยังไม่ครบ 90 วัน
ตัวเลขเรือธงต้องเฉลี่ยทุกแคมเปญที่วัดได้ ไม่ใช่เฉพาะที่ผ่านนัยสำคัญ —
เดิมเฉลี่ยเฉพาะแคมเปญที่ช่วงความเชื่อมั่นพ้นศูนย์ ซึ่งสูงเกินจริงอย่างเป็นระบบ
เพราะแคมเปญที่พ้นศูนย์คือแคมเปญที่ค่าประเมินสูงพอดี (winner's curse)
บั๊กนี้มองไม่เห็นตอนมีบัญชีเดียวเพราะทั้งสี่แคมเปญของ MST Golf สรุปได้หมด
พอเพิ่มบัญชีพรรคที่วงจรเป็นรายปีจึงเห็น: สี่แคมเปญวัดได้ +15% · +96% ·
−3% · +34% เฉลี่ยแบบเลือกเฉพาะที่ผ่าน = +65% เฉลี่ยทั้งสี่ = +35%
ตอนนี้เพิ่มคอลัมน์ matured แยก "วัดได้แล้ว" ออกจาก "สรุปได้"
และเฉลี่ยจากทุกแถวที่วัดได้
หน้าต่างวัดผลต้องยาวพอเทียบกับวงจรของกลุ่ม — วัดการกลับมาจ่ายซ้ำ ในหน้าต่างที่สั้นกว่าวงจรของคนกลุ่มนั้นมาก ๆ ไม่มีความหมาย อัตราฐานเข้าใกล้ ศูนย์ ทำให้ไม่กี่รายการกลายเป็นส่วนต่างหลายร้อยเปอร์เซ็นต์ ตัวอย่างจริง: บัญชีพรรค (วงจรราว 360 วัน) รายงาน T+7 ว่า "ได้ผล +342%" ช่วงความเชื่อมั่น 78–687% ผ่านนัยสำคัญตามสูตรจริงแต่ไร้ความหมาย ตอนนี้บังคับว่าหน้าต่าง ต้องยาวอย่างน้อยหนึ่งในสี่ของวงจรมัธยฐานของกลุ่มนั้นจึงจะสรุปได้
เครดิตข้อความห้ามติดลบ — sendCampaign หักเครดิตโดยไม่ตรวจยอดคงเหลือ
ร้านจึงส่งเกินที่จ่ายไว้ได้ไม่จำกัดและยอดกลายเป็นลบเงียบ ๆ (พบตอนเพิ่มบัญชี
ที่ฐานใหญ่กว่าเครดิตที่ให้มา: −538 และ −674) เพดานที่ไม่บังคับใช้ไม่ใช่เพดาน
ตอนนี้ส่งเท่าที่เครดิตพอแล้วรายงาน skippedNoCredit ออกมาตรง ๆ
posterior ต้องอ่านของวงจรเดียวกัน — LIMIT 1 โดยไม่ระบุ cycle_shape
ทำให้สนามกอล์ฟหยิบสถิติของร้านค้าปลีกมาใช้ ขัดกับข้อความที่ว่าเรียนรู้ข้ามร้าน
"ในวงจรเดียวกัน" — มองไม่เห็นตอนมีบัญชีเดียว
ตัวหารของ ROI รวมส่วนลดที่ให้ไปจริง — เดิมนับแต่ค่าส่งข้อความ ค่าส่ง LINE ราว 0.75 บาท แต่ส่วนลด 10% ของบิล 2,500 บาทคือ 250 บาท มากกว่าสามร้อยเท่า ถ้าไม่นับ "ต้นทุนต่อบาทที่เพิ่มขึ้น" จะดูดีเกินจริง จนไม่มีความหมาย และนั่นคือตัวเลขที่ลูกค้าใช้ตัดสินใจต่อสัญญา
lib/demo.ts สร้างประวัติแคมเปญสี่ตัวกระจายในปฏิทินย้อนหลังกว่าปี
โดยเดินผ่านเส้นทางเดิมทั้งหมด — แช่แข็งกลุ่ม แบ่ง arm ด้วย hash ส่งจริง
แล้วเลื่อนวันย้อนหลังและวัดผล ไม่มีการเขียนผลลัพธ์ลงไปตรง ๆ
ชุดข้อมูลตัวอย่างถูกสร้างก่อนที่จะมีแคมเปญ จึงไม่มีผลของแคมเปญอยู่ในนั้นเลย
ถ้าวัดตรง ๆ ส่วนต่างจะเป็นศูนย์โดยโครงสร้าง injectCampaignEffect จึงทำให้
โลกจำลอง "มีผลจริง" — แทรกธุรกรรมเพิ่มให้ 15% ของคนในกลุ่มที่ได้รับข้อความ
ที่ไม่มีการซื้อในหน้าต่างนั้นอยู่แล้ว (ตัดสินด้วย hash จึงทำซ้ำได้)
แล้วปล่อยให้ estimator ตัวจริงไปค้นหาผลนั้นเองผ่าน holdout ตามปกติ
ผลที่ออกมาราว +20% ถึง +40% พร้อมช่วงความเชื่อมั่นที่พ้นศูนย์มาไม่มาก บางครั้งได้ "ยังไม่พอสรุป" ซึ่งถูกต้อง ไม่ใช่บั๊ก — กลุ่มควบคุมรวมกัน มีแค่ราวสองร้อยคน ที่ขนาดนั้นผลขนาดกลางไม่ผ่าน 95% ไม่ว่าจะจริงหรือไม่ นี่เป็นข้อจำกัดของฐานตัวอย่าง 1,240 คน ไม่ใช่ของเครื่องวัด: ร้านจริงที่ส่ง สัปดาห์ละครั้งตลอดปีจะมีกลุ่มควบคุมสะสมหลายพันคน
ปุ่ม "เดินเวลาไปข้างหน้า" ที่ /app/settings เลื่อนวันอนุมัติย้อนหลัง
แล้ววัดผลใหม่ ใช้ตอนเดโมเพื่อให้เห็นว่าชั้น Proof ทำอะไรได้
ทุกอย่างที่เหลือเป็นของจริง — holdout · การแบ่ง arm · สูตรคำนวณ
lib/plans.ts เป็นข้อมูลล้วน ห้าม import ฐานข้อมูล — ทั้งหน้า /pricing
บนเว็บหลักและหน้า /app/billing ในคอนโซลอ่านจากไฟล์นี้ไฟล์เดียว
lib/billing.ts ต่อข้อมูลนั้นเข้ากับฐานข้อมูลและเป็นที่เดียวที่ตอบว่า
"แผนนี้ทำอะไรได้"
| แผน | ค่าสมาชิก/เดือน | คนที่ระบุตัวตนได้ | เครดิตแรกเข้า | Reach |
|---|---|---|---|---|
| Free | 0 ฿ | ถึง 500 | — | ✗ |
| Growth | 1,900 ฿ | ถึง 5,000 | 1,000 | ✓ |
| Multi | 5,900 ฿ | ถึง 25,000 | 3,000 | ✓ |
| Chain | 15,000 ฿ ขึ้นไป | ตกลงกัน | 10,000 | ✓ |
เครดิตข้อความแยกจากค่าสมาชิก จ่ายล่วงหน้า ไม่หมดอายุ —
1,000 ข้อความ 1,500 ฿ · 5,000 ข้อความ 6,500 ฿ · 20,000 ข้อความ 22,000 ฿
ต้นทุนจริงราว 0.75 ฿/ข้อความ อยู่ที่ MESSAGE_COST_BAHT ที่เดียว
รายปีจ่ายสิบเดือนใช้สิบสองเดือน
เพดานสองข้อที่บังคับใช้จริง — จำนวนคนตรวจใน commitImport
ก่อนแตะฐานข้อมูล (แทนที่ทั้งฐานนับจากศูนย์ ไม่ใช่บวกทับ ไม่งั้นร้านที่
ใกล้เพดานแก้ไขข้อมูลตัวเองไม่ได้) · Reach ตรวจใน approveCampaign
ไม่ใช่ที่หน้าจอ เพราะปุ่มที่ซ่อนไว้ไม่ได้ป้องกันการยิง Server Action ตรง ๆ
รอบบิลคำนวณจากปฏิทินกับ tenants.billing_day ทุกครั้ง ไม่เก็บสถานะ
"รอบปัจจุบัน" ในฐานข้อมูล — สถานะแบบนั้นต้องมีงานตามเวลามาเลื่อนให้
และถ้างานนั้นไม่ทำงานคืนหนึ่ง ยอดใช้งานจะค้างในรอบเก่าอย่างเงียบ ๆ
วันตัดรอบมาจากวันเปิดบัญชี บีบไว้ที่ 28 เพื่อไม่ให้ข้ามเดือนกุมภาพันธ์
credit_purchases บันทึกทุกครั้งที่ยอดเครดิตเพิ่ม จึงกระทบยอดได้:
คงเหลือ + ที่ส่งไปแล้ว = ที่ซื้อมาทั้งหมด ทุกบัญชี (verify:pricing
ตรวจข้อนี้) ยอดคงเหลือลอย ๆ ตอบคำถาม "จ่ายไปเท่าไร" ไม่ได้
ตารางราคาขัดกับตัวเองในหน้าเดียว — แถว "ข้อความรวมในแพ็ก 2,000" อยู่ห่างจากการ์ด "ไม่ซ่อนค่าข้อความในค่าสมาชิก" สองย่อหน้า และขัดกับ ต้นทุนจริง: 2,000 ข้อความมีต้นทุน 1,500 ฿ จากค่าสมาชิก 1,900 ฿ เหลือ 400 ฿ เป็นค่าซอฟต์แวร์ ร้านที่ส่งหนักที่สุดจะเป็นร้านที่ทำให้ ขาดทุนมากที่สุด — ซึ่งเป็นสิ่งที่หน้าเดียวกันบอกว่าจะไม่ทำ ตอนนี้ค่าสมาชิกเป็นค่าซอฟต์แวร์ล้วน เครดิตแยก ตรงกับกลไกที่โค้ดบังคับ อยู่แล้ว (จ่ายล่วงหน้าและหยุดเมื่อหมด ไม่ใช่เก็บส่วนเกินย้อนหลัง ซึ่งไม่มีระบบรองรับ)
เพดานที่ประกาศแต่ไม่มีใครอ่าน — tenants.tier มีอยู่ในฐานข้อมูล
ตั้งแต่ต้นแต่ไม่มีบรรทัดใดอ่าน และหน้าราคาประกาศเพดาน "ถึง 500 คน"
โดยไม่มีการตรวจที่ไหนเลย เพดานที่ไม่บังคับใช้ไม่ใช่เพดาน แต่เป็นคำโฆษณา
ขายสิ่งที่ยังไม่มีสามอย่าง — ตารางเดิมให้ Multi/Chain ได้ "Native API" และ "โหมดส่ง autopilot" ทั้งที่ทั้งสองไม่มีในระบบ และแบ่ง Proof เป็น "รวมผล 90 วัน" กับ "เต็มรูปแบบ" ตามชั้นราคา ทั้งที่โหมดวัดผลเลือกจาก ขนาดกลุ่ม ไม่ใช่จากแผน — การขายความแม่นทางสถิติเป็นชั้นราคาคือการ โกหกเรื่องวิธีการ ตอนนี้ Proof เท่ากันทุกแผนที่ส่งได้ ส่วน API และ autopilot มีคำว่า "กำลังพัฒนา" กำกับ และมีหัวข้อบนหน้าราคาที่บอกตรง ๆ ว่ายังไม่พร้อมและไม่คิดเงินสำหรับมัน
ตัวหารของ ROI ครอบแคมเปญคนละชุดกับตัวตั้ง — ความเห็นในโค้ดเขียนว่า
"ต้นทุนนับจากแคมเปญชุดเดียวกับที่เข้าสมการ" แต่ query ส่วนลดกรอง
horizon_days = 90 AND verdict != 'insufficient_data' ซึ่งแคบกว่าตัวตั้ง
ที่รวมทุกแคมเปญ matured = 1 ที่ขอบเขตสุกที่สุดของมัน (90 หรือ 30 หรือ 7)
แคมเปญที่วัดได้ที่ T+30 หรือวัดได้ที่ T+90 แต่คร่อมศูนย์ จึงเอายอดเพิ่ม
เข้าตัวตั้งโดยไม่เอาส่วนลดเข้าตัวหารเลย ผลคือหน้าค่าใช้จ่ายรายงานว่า
"จ่าย 1 บาท ได้กลับ 455 บาท" ซึ่งไม่มีใครควรเชื่อ หลังแก้ HONG MOVE
(ส่วนลด 15%) เปลี่ยนจากตัวเลขที่พองเป็น 1.1 ฿ ต่อ 1 ฿ ตอนนี้อ่านส่วนลด
จากแถวที่ picked เลือกไว้แล้ว จึงเป็นชุดเดียวกันโดยโครงสร้าง
และค่าส่งก็นับจาก measuredIds ไม่ใช่ทุกแคมเปญที่อยู่ในหน้าต่าง
นำเข้าแบบแทนที่ลบข้อมูลของทุกบัญชี — commitImport ลบตารางที่ไม่มี
คอลัมน์ tenant_id ด้วย DELETE FROM <table> เปล่า ๆ ร้านหนึ่งติ๊ก
"แทนที่ข้อมูลเดิม" แล้วธุรกรรม ความยินยอม แคมเปญ และผลการวัดของอีก
สามบัญชีหายไปด้วยโดยไม่มีข้อความใดบอก ตอนนี้ลบจากลูกไปหาแม่ผ่าน
subquery ของบัญชีนั้นเท่านั้น ตามแบบเดียวกับ wipeTenant
Server Action รับ tenantId จากฟอร์ม — ทุก action อ่าน tenantId
จาก FormData แล้วตรวจแค่ว่ามีบัญชีนั้นในทะเบียน แปลว่าใครก็ยิง POST
ตรงมาพร้อม tenantId ของบัญชีอื่นแล้วอนุมัติแคมเปญ · ส่งข้อความ ·
ล้างข้อมูลของบัญชีนั้นได้ ช่องนี้มองไม่เห็นจากหน้าจอเพราะทุกฟอร์มส่ง
ค่าที่ถูกอยู่แล้ว — แต่ hidden input ไม่ใช่การควบคุมสิทธิ์ ตอนนี้อ่าน
จาก cookie ฝั่งเซิร์ฟเวอร์ ค่าในฟอร์มใช้ตรวจว่าหน้าจอที่กดมากับบัญชี
เดียวกันจริงเท่านั้น ยังไม่ใช่ auth — cookie แก้ได้ แต่จุดที่ต้อง
เปลี่ยนตอนต่อ session จริงเหลือฟังก์ชันเดียว (currentTenant)
ใบแจ้งค่าใช้จ่ายมีรายการที่ไม่ได้เกิดในรอบนั้น — ตอนแรกลงวันที่
ยอดเครดิตของชุดข้อมูลตัวอย่างเป็น "สองวันก่อน" เพื่อให้หน้ามีตัวเลข
ให้ดู ได้หน้าจอที่ขัดกันเอง: ใบแจ้งเดือนนี้ขึ้นค่าเครดิต 55,000 ฿
ขณะที่ข้อความที่เครดิตนั้นจ่ายไปถูกส่งเมื่อปีที่แล้ว (ประวัติเดโม
เลื่อน sent_at ย้อนหลังพร้อม approved_at ตามที่ควรเป็น)
ตอนนี้ลงวันที่ตอนเปิดบัญชี และตารางค่าส่งรายแคมเปญแสดงรายการล่าสุด
พร้อมธงว่าอยู่ในรอบนี้ไหม แทนที่จะกรองเฉพาะในรอบแล้วว่างเปล่าตลอด
เรียงตามลำดับที่ต้องแก้ ไม่ใช่ตามความยาก
ไม่มีระบบล็อกอิน — บัญชีมาจาก cookie และผู้กระทำถูกบันทึกเป็น
owner ทั้งหมด ต้องมี session จริงและตรวจว่าผู้ใช้เข้าถึงบัญชีนี้ได้
แก้ที่ currentTenant ใน app/(app)/actions.ts และ
lib/active-tenant.ts เป็นข้อเดียวที่กันคนนอกไม่ได้เลยตอนนี้
การส่ง LINE ยังไม่ยิง API — ชั้นส่งบันทึกแถวครบ มีคีย์กันซ้ำ หักเครดิตถูก แต่ไม่มีใครได้รับข้อความจริง ต้องต่อ Messaging API พร้อม retry ที่ไม่ทำลาย idempotency และเก็บสถานะส่งไม่สำเร็จ
injectCampaignEffect ต้องถอดออก — เป็นของเดโมเท่านั้น
ถ้าเหลืออยู่ตอนต่อข้อมูลลูกค้าจริง จะเขียนธุรกรรมปลอมลงฐานของลูกค้า
CONTACT_WEBHOOK_URL ไม่ตั้ง = lead หาย — ผู้ส่งเห็นหน้าขอบคุณ
แต่ข้อมูลอยู่แค่ใน log ของเซิร์ฟเวอร์ และยังไม่มี rate limit
บน POST /api/contact
SQLite อยู่บนดิสก์ของเครื่องที่รัน — บน Vercel ระบบไฟล์อ่านได้
อย่างเดียวยกเว้น /tmp ซึ่งเป็นของ instance ใครของมัน lib/db.ts
จึงย้ายไป /tmp เองเมื่อเจอ process.env.VERCEL ผลคือคอนโซลที่นั่น
เป็น เดโมที่รีเซ็ตตัวเอง ไม่ใช่ฐานข้อมูล — วัดได้ว่าคอนเทนเนอร์เย็น
ใช้เวลา ~11 วินาที (seed 4 วินาที · ไฟล์ 88 MB) ส่วนที่อุ่นแล้ว 0.4–0.8 วินาที
ข้อจำกัดที่แท้จริงไม่ใช่ภาษา SQL แต่เป็น API แบบ synchronous:
มี .prepare() 113 จุดใน 18 ไฟล์ และไดรเวอร์ที่โฮสต์ได้ทุกตัวเป็น async
การย้ายจึงไม่ใช่แค่เปลี่ยน connection string แต่ต้องเปลี่ยนทุก call site
ให้เป็น async ซึ่งจะไล่ขึ้นไปถึง runMatch · deriveFeatures · roiSummary
ต้องทำก่อนมีลูกค้าจริงคนแรก และต้องมีการสำรองข้อมูล
สิทธิของเจ้าของข้อมูลตาม PDPA ยังไม่มีหน้าจอ — หน้าราคาเขียนว่า ขอไฟล์กลับและขอให้ลบได้ แต่ยังไม่มีปุ่มไหนทำสองอย่างนั้น ต้องมีทั้งระดับบัญชีและระดับบุคคล พร้อมบันทึกว่าใครขอเมื่อไร
ไม่มีการชำระเงิน — ปุ่มซื้อเครดิตและย้ายแผนเปลี่ยนฐานข้อมูลตรง ๆ ระบบจริงต้องผ่าน payment gateway และออกใบกำกับภาษี
ยังไม่มี observability — error.tsx มีแล้วทั้งสองฝั่ง และความล้มเหลว
ที่คาดไว้ถูกส่งกลับเป็นข้อความให้ผู้ใช้อ่านแล้ว (ดู lib/action-state.ts)
แต่ยังไม่มีใครเก็บ digest ไปไว้ที่ไหน — ยังไม่รู้เลยว่ามีอะไรพังบ้าง
รายการหลักอยู่ที่ เหลืออะไรก่อนเป็น 1.0 ด้านบน ที่เหลือเป็นข้อเล็ก: แคมเปญที่ยังไม่มีธุรกรรมในหน้าต่างเวลาเลย จะใช้ ค่าจำลองจาก hash ของแคมเปญแทนการวัดจากข้อมูลจริง (เกิดเฉพาะแคมเปญที่ เพิ่งอนุมัติ และหายไปเองเมื่อมีธุรกรรมเข้ามา)
ตัวเลขในเว็บทุกจุดที่ยังไม่ได้วัดจริง มีข้อความกำกับไว้ว่าเป็นชุดข้อมูลตัวอย่าง
หรือเป็นเป้า ไม่ใช่ผลลัพธ์ — ตามที่ blueprint กำหนด ก่อนขึ้นเวทีจริงต้องหา
แหล่งอ้างอิงหรือถอดตัวเลขในภาคผนวกของ /investors ออก