"งานกลุ่มไม่ได้ขาดข้อมูล มันขาดคนกล้าพูด" ระบบวัดแรงงานจริงในงานกลุ่มจาก revision history แล้วยิงกลับเป็นมีม Track: Applied Data Engineering · 67HACK THE MEME Engineering for Next Generation Learners
AURA FARM อ่าน ร่องรอยการทำงานที่มีชื่อผู้ทำและเวลากำกับ คำนวณสัดส่วนแรงงานจริงของสมาชิกแต่ละคน วัดความเหลื่อมล้ำด้วย Gini coefficient แล้วแปลงตัวเลขเป็นมีม PNG ที่กลุ่มดูพร้อมกันได้
เส้นแบ่งของระบบนี้ไม่ใช่ "ออนไลน์ vs ออฟไลน์" และไม่ใช่ "Google vs Microsoft" เส้นแบ่งจริงคือ เครื่องมือนั้นบันทึกไหมว่าใครแก้อะไรตอนไหน
| เครื่องมือ | มีบันทึกผู้แก้ไข? | สถานะ |
|---|---|---|
| Google Docs / Slides / Sheets | Drive Activity + Revisions API | ✅ ใช้ได้ |
| Word ที่เปิด Track Changes | w:ins / w:del ในไฟล์เอง |
✅ ใช้ได้ ลากไฟล์มาวาง |
| Word Online / OneDrive / SharePoint | Microsoft Graph driveItem/versions |
✅ ใช้ได้ ต้องมี token |
| GitHub | commits | ✅ ใช้ได้ |
| Word ในเครื่อง ไม่เปิด Track Changes | รู้แค่คนที่เซฟล่าสุด | ❌ ทำไม่ได้ |
| Canva / Figma | มี version history แต่ API ไม่เปิดรายละเอียด | ❌ ยังไม่รองรับ |
| ส่งไฟล์กันทาง LINE | ไม่มีบันทึกอะไรเลย | ❌ ทำไม่ได้ |
AURA FARM ไม่ได้ผูกกับ Google มันผูกกับ "ร่องรอยที่มีชื่อและเวลา" เครื่องมือไหนบันทึกไว้ เราอ่านได้หมด เครื่องมือไหนไม่บันทึก ไม่มีใครอ่านได้ รวมทั้งเรา
คนที่ทำพอดีส่วนของตัวเอง = 0 ออร่า · ทำเกิน = บวก · ทำน้อย = ลบ
npm install && npm run devเปิด http://localhost:3000 ใช้ได้ทันที ไม่ต้องตั้งค่า env ใดๆ และไม่ต้อง login ระบบทำงานใน mock mode เป็นค่าเริ่มต้น
| คำสั่ง | ผล |
|---|---|
npm run dev |
รัน dev server |
npm test |
รัน unit test 57 เคส |
npm run build |
build production |
npm run generate:mock |
สร้าง data/mock-projects.json ใหม่จาก seeded PRNG + parse ไฟล์ .docx ตัวอย่าง (ปกติไม่ต้องรัน) |
npm run generate:fixtures |
สร้างไฟล์ .docx สำหรับเทสต์และไฟล์ตัวอย่าง (ปกติไม่ต้องรัน) |
| URL | เรื่อง | ผลที่ได้ |
|---|---|---|
/dashboard?demo=chaos |
5 คน คนเดียวเขียน 78% อีก 2 คนแตะนิดเดียว 2 คนไม่แตะเลย | Gini 0.69 → ONE_MAN_ARMY + ยิงเตือนตอน 68% ของ deadline |
/dashboard?demo=dumper |
4 คน ปริมาณงานใกล้เคียงกัน แต่ 2 คนพิมพ์ทั้งหมดในชั่วโมงท้าย | Gini 0.10 แต่ติดธง "โยนทีเดียว" 2 คน |
/dashboard?demo=legend |
4 คน แบ่งงานกันจริง | Gini 0.13 → LEGENDARY |
/dashboard?demo=word |
4 คน จากไฟล์ Word จริงที่เปิด Track Changes ไม่ผ่าน Google เลย | Gini 0.49 → TILTING |
ชุด word ไม่ได้เขียนมือ — มันถูก parse จากไฟล์ .docx จริง ที่ public/samples/รายงานกลุ่ม-tracked.docx
ตอนรัน npm run generate:mock แล้วอบผลลง JSON ทำให้ข้อมูลเดโม่มาจาก code path เดียวกับที่ผู้ใช้จริงใช้
.docx คือไฟล์ zip ข้างในมี word/document.xml ถ้าเปิด Track Changes ทุกการพิมพ์จะถูกห่อด้วย <w:ins>
ทุกการลบห่อด้วย <w:del> โดยมี w:author และ w:date ติดมาด้วย — นี่คือ Edit[] ครบทุกฟิลด์
อยู่ในไฟล์เอง ไม่ต้องใช้ API ไม่ต้อง OAuth ไม่ต้องต่อเน็ต
กับดักที่กันไว้แล้ว (มี test ทุกข้อ):
| เรื่อง | ระบบทำอะไร |
|---|---|
ข้อความที่ถูกลบอยู่ใน w:delText ไม่ใช่ w:t |
นับทั้งสอง tag — test ยืนยันว่า charsRemoved ไม่เป็น 0 |
w:ins ซ้อนใน w:del |
ข้าม node ที่ซ้อน ไม่นับซ้ำ แล้วรายงานจำนวนที่ข้าม |
Word ลบข้อมูลส่วนบุคคล ชื่อกลายเป็น Author |
ปฏิเสธไฟล์ พร้อมบอกวิธีปิดค่านั้น |
w:date หาย |
ข้าม node นั้น แล้วรายงานจำนวนให้ผู้ใช้เห็น |
| ชื่อใน Word เป็นชื่อโปรไฟล์ Office | ให้ผู้ใช้ map ชื่อ → สมาชิกบนหน้าจอก่อนวิเคราะห์ |
| ไฟล์ไม่ได้เปิด Track Changes | บอกวิธีเปิด (รีวิว › ติดตามการเปลี่ยนแปลง) และบอกตรงๆ ว่าเปิดย้อนหลังไม่ได้ |
ระบบวัดที่วัดซ้ำแล้วได้ไม่เท่าเดิม ไม่ใช่ระบบวัด โปรเจกต์นี้จึงบังคับสามข้อ:
- ไม่มี
Date.now()และMath.random()ในlib/และcomponents/เลย — มี test ที่ grep ไฟล์จริงเพื่อกันการเผลอใส่กลับเข้ามา elapsedRatioคิดจากevaluatedAtที่ฝังอยู่ในชุดข้อมูล ไม่ใช่นาฬิกาของเครื่อง- mock data เป็นไฟล์นิ่งที่ commit ไว้ สร้างครั้งเดียวจาก
scripts/generate-mock.tsด้วย mulberry32 (seed: chaos 6767, dumper 1212, legend 4949) ตอน runtime อ่าน JSON อย่างเดียว
ผลคือรีเฟรชกี่ครั้ง เปิดวันไหน เครื่องใคร ก็ได้เลขชุดเดิมเป๊ะ
| เวลา | ทำอะไร | พูดอะไร |
|---|---|---|
| 0–5 วิ | เปิด / — AuraCard สามใบนับเลขขึ้นลงอยู่ |
"มีมนี้ทุกคนรู้จัก งานกลุ่ม 5 คน ทำจริงคนเดียว" |
| 5–20 วิ | กด ดู mock project → ?demo=chaos ดู sequence พลิกการ์ด |
"เราวัดมันได้ Lorenz วาดตัวเองก่อน แล้วการ์ดพลิกทีละใบ คนแบกพลิกเป็นใบสุดท้าย" |
| 20–40 วิ | ชี้ Gini 0.63 + Lorenz curve | "นี่ไม่ใช่ความรู้สึก นี่คือตัวเลข พื้นที่แรเงาคือความเหลื่อมล้ำ" |
| 40–60 วิ | สลับไป ?demo=dumper |
"ระบบจับคนที่ปริมาณผ่าน แต่โยนทีเดียวคืนสุดท้ายได้ ทุกคน volume 25% เท่ากัน แต่สองคนติดธง" |
| 60–80 วิ | กด สร้างมีมส่งเข้ากลุ่ม → โหลด PNG | "นี่คือสิ่งที่ส่งเข้ากลุ่ม ไม่ใช่รายงาน" |
| 80–90 วิ | เปิด /methodology |
"และเราเปิดสูตรให้ตรวจทั้งหมด รวมถึงข้อจำกัดของตัวเอง" |
ทั้งหมดเป็น pure function ใน lib/scoring.ts และ lib/gini.ts (ดูคำอธิบายฉบับเต็มที่หน้า /methodology)
netChars_i = max(0, Σ charsAdded − Σ charsRemoved)
volume_i = netChars_i / Σ_j netChars_j
p_ik = สัดส่วน netChars ของคน i ในช่วงเวลาที่ k (แบ่ง startedAt→dueAt เป็น 12 ช่วง)
consistency_i = (− Σ_k p_ik · log p_ik) / log 12
taskShare_i = task ที่ closedBy = i / task ที่ปิดแล้วทั้งหมด (ถ้าไม่มี task ปิดเลย = 1/n)
raw_i = volume_i × (0.6 + 0.4 × consistency_i) + 0.2 × taskShare_i
แล้ว normalize ให้ Σ raw = 1
fairShare = 1 / n
aura_i = round((raw_i − fairShare) × 10000)
gini = Σ_i (2i − n − 1)·raw_i / (n · Σ_i raw_i) เรียง raw น้อยไปมาก, i เริ่มที่ 1
elapsedRatio = clamp01( (evaluatedAt − startedAt) / (dueAt − startedAt) )
panicIndex = gini × elapsedRatio
alertFired = gini > 0.45 && elapsedRatio > 0.5
dumpedAtEnd_i = netChars ของคน i ใน 15% ท้ายของ timeline / netChars ทั้งหมดของคน i > 0.6
consistency เป็นตัวคูณ ไม่ใช่ตัวบวก — ถ้าบวกตรงๆ คนที่เปิดเอกสารพิมพ์วันละคำจะได้ consistency
เกือบเต็มโดยแทบไม่มีงานจริง การทำสม่ำเสมอควรขยายมูลค่าของแรงงานที่มีอยู่ ไม่ใช่เสกแรงงานจากศูนย์
พอเป็นตัวคูณ ใครที่ volume = 0 จะได้ศูนย์จากพจน์นี้เสมอ ไม่ว่าจะเข้าเอกสารบ่อยแค่ไหน (มี test บังคับข้อนี้)
tier ตัดสินจาก share ไม่ใช่จากเลขออร่าดิบ — share_i = raw_i / (1/n) โดย share = 1.00
คือทำพอดีส่วนของตัวเอง ไม่ว่าทีมจะกี่คน เลขออร่ายังโชว์เหมือนเดิมเพราะมันคือตัวที่คนอ่าน
| share | tier | gini | verdict | |
|---|---|---|---|---|
| ≥ 2.00 | 🔥 CARRY |
< 0.25 | LEGENDARY |
|
| 1.25 – 1.99 | ✅ SOLID |
0.25 – 0.45 | NORMAL |
|
| 0.75 – 1.24 | ⚖️ FAIR |
0.45 – 0.6 | TILTING |
|
| 0.25 – 0.74 | 🚌 PASSENGER |
≥ 0.6 | ONE_MAN_ARMY |
|
| < 0.25 | 👻 GHOST |
เหตุผล: ช่วงของ aura ขึ้นกับขนาดทีม คนที่ไม่แตะงานเลยได้ −10000/n ซึ่งคือ −3,333 ในทีม 3 คน
แต่เป็นแค่ −500 ในทีม 20 คน ถ้าใช้ threshold คงที่ คนที่ไม่ทำอะไรในทีมใหญ่จะได้ป้าย "พอดีเป๊ะ"
มี test บังคับไว้ที่ทีม 3 / 5 / 6 / 8 / 10 / 20 คน
revision history จับ "งานที่พิมพ์" ได้ แต่จับ "งานที่คิด" ไม่ได้ คนที่ออกแบบโครงรายงานทั้งฉบับแล้วให้เพื่อนพิมพ์ตาม จะได้ออร่าติดลบ คนที่อ่านเปเปอร์สามสิบหน้าแล้วสรุปเหลือสองบรรทัด ระบบเห็นแค่สองบรรทัดนั้น
ตัวเลขออร่าจึงเป็น ข้อมูลตั้งต้นของบทสนทนา ไม่ใช่คำตัดสิน — ข้อจำกัดทั้ง 5 ข้อเขียนไว้เต็มที่ /methodology
- ไม่มี public leaderboard ข้ามกลุ่ม ทุกผลลัพธ์เห็นเฉพาะภายในกลุ่มนั้น
- ระบบเตือนตอนยัง แก้ทัน (
elapsedRatio > 0.5คือผ่านครึ่งทางของ deadline) ไม่ใช่ประจานตอนจบ - ทุกหน้าที่แสดงผลมีปุ่มเข้า
/methodology— ระบบที่ตัดสินคนต้องอธิบายตัวเองได้ - ข้อความมีมทุกอันล้อ สถานการณ์ ไม่ใช่ล้อ คน ไม่มีคำหยาบหรือคำด่า
- ไม่มี database ทั้งโปรเจกต์ ไม่มีการเก็บผลลัพธ์ของใครไว้
โหมดปกติไม่ต้องต่ออะไรเลย และการลากไฟล์ .docx ที่เปิด Track Changes ก็ใช้ได้เต็มรูปแบบ
โดยไม่ต้องมี token ส่วนการวางลิงก์ต้องมี token เพราะเป็นการเข้าถึงบัญชีของผู้ใช้เอง
cp .env.example .env.local| ลิงก์ | env ที่ต้องมี | scope / permission |
|---|---|---|
| Google Docs | GOOGLE_OAUTH_ACCESS_TOKEN |
drive.readonly, drive.activity.readonly |
| SharePoint / OneDrive | MICROSOFT_GRAPH_ACCESS_TOKEN |
Files.Read.All, Sites.Read.All |
วิธีขอ token ทีละขั้นอยู่ใน .env.example และในหน้า /how-it-works#token
lib/adapters/googleDocs.ts—revisions.list→ exporttext/plain→ Myers diff คู่ติดกัน → ยกส่วนต่างให้lastModifyingUserแล้วถ่วงด้วย Drive Activity API เมื่อมีหลายคนแก้ในช่วงเดียวกันlib/adapters/microsoftGraph.ts— encode sharing URL เป็นu!<base64url>→/shares/{id}/driveItem/versions→ โหลดเนื้อแต่ละเวอร์ชันเป็น.docx→ ดึงw:tเป็น plain text → diff คู่ติดกัน → ยกส่วนต่างให้lastModifiedByของเวอร์ชันนั้น
ถ้าไม่มี token ระบบจะไม่แสดงข้อมูลตัวอย่างแทน — มันจะบอกตรงๆ ว่าอ่านไม่ได้เพราะอะไร
และต้องทำอะไรต่อ เพราะตัวเลขปลอมที่ดูเหมือนของจริงอันตรายกว่าไม่มีตัวเลข
(ปุ่ม "ดู mock project" กับ ?demo= ยังเป็นทางเข้าข้อมูลตัวอย่างที่ผู้ใช้ตั้งใจกดเอง)
Next.js 15 (App Router) · TypeScript strict · Tailwind CSS v4 · Vitest กราฟทุกตัวเขียน SVG เอง (ไม่ใช้ chart library) · มีม render ด้วย Canvas API ฝั่ง client
Visual direction: เอกสารราชการโรงเรียน (Sarabun, พื้นกระดาษ, ปากกาแดง) ปะทะ คลิปตัดต่อออร่า (Anton, พื้นดำ, glow ม่วง–cyan) การพลิกจาก register หนึ่งไปอีก register คือ thesis ที่มองเห็นได้ — ข้อมูลน่าเบื่อ กลายเป็น มีม
app/
page.tsx Landing — hero คือ live demo
dashboard/page.tsx ผลวิเคราะห์ (server component)
methodology/page.tsx สูตรทั้งหมด + ตารางแหล่งข้อมูล + ข้อจำกัด
api/analyze/route.ts วิเคราะห์จากลิงก์เอกสาร หรือคืน mock
lib/
types.ts scoring.ts gini.ts memeTemplates.ts mockLoader.ts
adapters/googleDocs.ts revision history + Drive Activity → Edit[]
adapters/wordDocx.ts w:ins / w:del ใน .docx → Edit[]
adapters/wordProject.ts ตัวสร้าง Project (แยกไว้ให้ client ไม่ต้องโหลด jszip)
adapters/github.ts commits → Edit[]
components/
AuraCard.tsx PanicMeter.tsx LorenzCurve.tsx ContributionTimeline.tsx
MemeCanvas.tsx DashboardView.tsx LiveHero.tsx DocumentLinkForm.tsx
CapabilityMatrix.tsx SourceTable.tsx DocxDropZone.tsx
scripts/
generate-mock.mts สร้าง mock JSON (mulberry32 + parse .docx จริง)
generate-docx-fixtures.mts สร้างไฟล์ .docx สำหรับเทสต์และตัวอย่าง
data/
mock-projects.json 4 scenario (committed)
github-fixture.json ตัวอย่าง commit สำหรับ GitHub adapter
gdocs-fixture.json ตัวอย่าง revision + activity สำหรับ Google Docs adapter
public/samples/
รายงานกลุ่ม-tracked.docx ไฟล์ Word จริงที่เปิด Track Changes ให้โหลดไปลองเองได้
__tests__/
scoring.test.ts 52 เคส
adapters.test.ts 20 เคส
wordDocx.test.ts 16 เคส
fixtures/*.docx ไฟล์ .docx จริง 4 แบบ (tracked / ไม่ tracked / anonymized / คนเดียว)
ทุก adapter มีหน้าตาเดียวกันคือ (source) => Promise<Edit[]> เพิ่มแหล่งข้อมูลใหม่จึงไม่ต้องแตะสูตรคำนวณ