Mục lục · 9 mục
Audit 4-tier (Drive + Sheet + workspace + knowledge) dọn 20 mismatch + 9 duplicate về 0 sau 4 tháng naming drift. Tool ship OK 5/6 case. Case fail: AI delete Sheet row by code col A only → bắn nhầm row real vì Sheet có duplicate mã. Plus 1 discovery quan trọng: Google Sheets File → Version history KHÔNG track row delete granular — restore = overwrite. Lesson 4 rule cho future API mutation.
TL;DR
Tôi build tool audit so sánh 4 tier metadata cho hệ project AIC: Drive folder ↔ CRM Sheet row ↔ workspace folder ↔ knowledge folder. Output đầu: 20 mismatch, 9 workspace duplicate, 13 knowledge duplicate sau 4 tháng naming drift.
Cleanup 7 category đưa về 0 mismatch — sync 100% (56-56-56-56 folder). 5 case ship OK.
Case 6 ship sai: AI delete Sheet row by code col A only → bắn nhầm row thật của project ACTIVE. Sheet có 2 row mã trùng (real + test slug). Recovery cần dữ liệu paste tay vì Drive/Sheet API không có row-level version history granular.
4 lesson rule cho future API mutation ở cuối bài.
Bối cảnh
Hệ project AIC chạy trên 4 nguồn metadata:
| Tier | Source-of-truth cho ai | Owner |
|---|---|---|
| Drive folder (lifecycle PRESALE/ACTIVE/COMPLETED/FAIL) | Team sales + tổng giám đốc click hằng ngày | Người |
| CRM Sheet “Dự án” (14 cols schema) | Index/dashboard tracking pipeline | Nhân sự sales |
| workspace/projects/ (local AI ops) | AI agent ghi BOQ JSON, drawing render, pricing log | AI |
| knowledge/companies/AIC/projects/ (knowledge plane) | AI agent đọc context dự án ở session future | AI |
4 tier nên cùng mã (AIC-2026-NNN) + cùng slug ({snake_case_short_name}). Reality sau 4 tháng:
- 67 workspace folder vs 56 Drive folder vs 59 Sheet row → drift
- 9 workspace có 2-3 folder cùng mã (vd
aic_YYYY_NNN_short_slug+aic_YYYY_NNN_long_slug+aic_YYYY_NNN_test_variant) - 13 knowledge tương tự
- 1 CRM mã refresh → workspace folder vẫn tên cũ (Drive folder name update, workspace local có folder cùng mã nhưng content lại là project khác do template copy nhầm — content drift vs name)
Không có cron weekly enforce → drift compound qua thời gian.
4-tier audit tool
Build .agents/skills/aic-gdrive-automation/lib/audit_4tier.js — read-only diff:
async function listDriveProjects(drive) {
const all = [];
for (const [lifecycle, folderId] of Object.entries(LIFECYCLE_FOLDERS)) {
// ... pagination Drive API
for (const f of res.data.files || []) {
all.push({ lifecycle, name: f.name, parsedCode: parseProjectCode(f.name) });
}
}
return all;
}
// Build matrix: code → {drive, sheet, workspace[], knowledge[]}
const matrix = {};
for (const code of allCodes) {
matrix[code] = {
drive: driveProjects.find(p => p.parsedCode === code),
sheet: sheetRows.find(p => p.code === code),
workspace: workspaceProjects.filter(p => p.parsedCode === code), // array — duplicate detect
knowledge: knowledgeProjects.filter(p => p.parsedCode === code),
};
}
Sau đó loop matrix detect:
NO_DRIVE/NO_SHEET/NO_WORKSPACE/NO_KNOWLEDGE— tier missingWORKSPACE_DUPLICATE_N(N > 1) — multi folder cùng mãLIFECYCLE_DRIFT(drive=X, sheet="Y")— Drive lifecycle ≠ Sheet status
Output markdown report với mismatch table + actionable per case.
Dry-run first — output ở workspace/audits/4tier_audit_YYYYMMDD.md. KHÔNG mutate. Đây là principle: tách audit-detector khỏi cleanup-executor.
Cleanup 7 category
Sau dry-run, tôi phân 20 mismatch thành 7 category với recommend per case:
- Workspace duplicate merge (7 case) — canonical = match Drive folder name. Vd Drive
[AIC-YYYY-NNN] {GENERAL_CONTRACTOR_NAME} {SITE_NAME}→ workspace canonicalaic_YYYY_NNN_{long_full_slug}(delete short variantaic_YYYY_NNN_{short_slug}). - Knowledge sync — theo workspace canonical sau bước 1.
- Lifecycle drift (Drive ≠ Sheet status) — per case người dùng quyết bên nào canonical.
- Content nhầm folder — 1 case workspace folder name match Drive nhưng content
boq_data.jsonghi project khác (template copy nhầm). Archive vào_pending_register/. - Workspace tên không match Drive — rename suffix theo Drive.
- Skeleton missing — tạo workspace + knowledge skeleton cho 1 project mới register CRM.
- Test/duplicate cleanup — delete row Sheet test, rename misnomer.
Per case execute sequential với git mv preserve history (workspace + knowledge), Drive API files.update({addParents, removeParents}) move folder lifecycle, Sheet API batchUpdate cho row delete + col F status update.
Verify sau mỗi category: re-run audit → mismatch count giảm.
5 commit shipped sạch:
1f0eaf47 — adopt audit pattern (BMad-METHOD QA gate)
0587e907 — Phase 1 dry-run + design doc
dd35f410 — Phase 2A.2-7 workspace + knowledge consolidate (mismatch 20→6)
2fba728d — final 6 case Drive lifecycle + Sheet ops (mismatch 6→3)
126225d9 — final audit + recovery (mismatch 3→0 ✅)
Mistake — script delete by code-only
Sheet “Dự án” có 2 row mã 038 sau 4 tháng:
- Row real: code col A =
AIC-YYYY-NNN, name = “[REDACTED — client residential project name]” (project ACTIVE đã ký HĐ) - Row test: code col A =
aic_YYYY_NNN_test_variant_slug(lowercase slug), name = “[TEST] [REDACTED — same client] — variant suffix”
Cả 2 row code col A khi parse normalize (regex aic[_-]?(\d{4})[_-]?(\d{3})) đều ra AIC-YYYY-NNN.
Script tôi viết:
const targets = ['AIC-YYYY-NNN', 'AIC-YYYY-MMM'];
for (let i = 0; i < rows.length; i++) {
const code = (rows[i][0] || '').trim();
if (targets.includes(code)) { // ❌ match code col A literal
toDelete.push({ index: i, code, name });
}
}
Filter targets.includes(code) match AIC-YYYY-NNN exact → match row REAL. Row test có code col A là aic_YYYY_NNN_test_variant_slug (KHÔNG literal AIC-YYYY-NNN) → không match.
Script in Found 2 row to delete — đúng số expected (2 mã trong targets list). Tôi không verify name từng row trước batchUpdate.
Delete row real “[REDACTED — client residential project name]” — project ACTIVE đã ký HĐ thi công.
Phát hiện sau khi re-run audit: thấy mismatch mới 038 NO_SHEET lộ ra → trace lại stdout script → vỡ lẽ.
Honest reflection: AI làm việc khá ẩu. Thấy code col A là AIC-YYYY-NNN, match targets list → xoá luôn. Không đọc col B (name). Không expand context để hỏi “tại sao có 2 row trùng mã 038?”. Con người nhìn vào sẽ pause: “ủa sao 2 row cùng mã?” — AI không có phản xạ đó nếu không được rule cứng.
Recovery — và 1 discovery quan trọng về Google Sheets
Sau khi phát hiện sai, plan đầu của tôi: anh chủ workspace mở File → Version history → restore phiên bản trước thời điểm script chạy. Tôi nghĩ nhanh 5 phút, chính xác 100%.
Plan thất bại. Anh check Version history thực tế — không track row delete granular. Sheet revision UI hiển thị các snapshot lớn, nhưng giữa snapshot không có per-cell change log dạng “row 38 deleted at 10:48 ICT”. Restore version mất phiên bản gần nhất, ghi đè work khác trong cùng workbook (nhân sự khác có thể đã edit row khác sau snapshot đó).
Đây là điểm khác biệt critical với git filesystem:
| Git filesystem rename/delete | Google Sheets row delete | |
|---|---|---|
| History granular | Per-line, per-file, per-commit | Snapshot toàn workbook, không per-row |
| Restore selective | git checkout <file> từ commit cụ thể | Restore overwrite toàn sheet |
| Audit trail | git log + git blame | Activity log có nhưng không hiện row content trước-sau |
| Recovery without overwrite | YES (git mv revert) | Phải copy data từ memory/backup external + insert tay |
→ Sheet API delete row = destructive operation thật sự. Không trust UI Version history làm safety net.
Khả thi nhất: anh paste data row tay từ memory hoặc backup external trước cleanup pass.
Tôi báo người dùng problem honestly, kèm 2 option recovery. Người dùng paste 14 cols data từ phiên bản hôm trước:
AIC-YYYY-NNN | [REDACTED — client residential project] |
Thiết kế & Thi công Nội Thất | [REDACTED — customer name] | Khối VP |
🟢 ACTIVE | ✅ Đã ký HĐ TC. ⚠️ Margin metric corporate |
<Drive Folder ID> | <Drive Link> | YYYY-MM-DD HH:MM
Re-insert đúng position (sau row 037, trước row 039) qua Sheet API insertDimension + values.update:
await sheets.spreadsheets.batchUpdate({
spreadsheetId, requestBody: {
requests: [{
insertDimension: {
range: { sheetId, dimension: 'ROWS', startIndex: 37, endIndex: 38 },
inheritFromBefore: true,
},
}],
},
});
await sheets.spreadsheets.values.update({
spreadsheetId, range: \`'Dự án'!A38:N38\`,
valueInputOption: 'USER_ENTERED',
requestBody: { values: [newRow14Cols] },
});
Re-run audit final: 0 mismatch sync 56-56-56-56.
4 lesson rule cho future API mutation
Tôi ghi vào memory feedback_sheet_row_delete_verify_by_name.md:
1. Match code + name combine, không filter code only
// ❌ Pattern cũ (vừa bắn vào chân)
if (targets.includes(rows[i][0])) toDelete.push(...);
// ✅ Pattern mới
if (rows[i][0] === expectedCode && rows[i][1].includes(expectedNameKeyword)) {
toDelete.push(...);
}
Sheet có thể có duplicate mã (real + test, current + deprecated). Verify name keyword đảm bảo target right row.
2. Dry-run preview list FULL row trước delete batch
Print từng row sẽ delete với cột A-G visible. Người dùng đọc preview confirm trước khi script chạy mutation.
Will delete 2 row:
row 38: AIC-YYYY-NNN | [REDACTED — client A residential] | ACTIVE | ...
row 40: AIC-YYYY-MMM | [REDACTED — duplicate row referencing client B] | PRESALE | ...
Confirm? [y/N]
3. Snapshot backup TRƯỚC batch destructive
sheets.spreadsheets.copyTo({destinationSpreadsheetId: BACKUP_ID}) hoặc export CSV qua Drive API. Pre-write snapshot mỗi cleanup pass — undo trivial nếu sai.
4. Match >1 cùng mã → HARD-STOP escalate
Nếu loop detect cùng targets[i] match nhiều row → red flag, KHÔNG proceed:
const grouped = {};
for (const row of rows) {
const code = parseProjectCode(row[0]);
(grouped[code] ||= []).push(row);
}
for (const [code, list] of Object.entries(grouped)) {
if (list.length > 1 && targets.includes(code)) {
throw new Error(\`Multi-row collision for \${code} — escalate to user\`);
}
}
User quyết row nào delete bằng cách paste row index cụ thể, KHÔNG để script tự đoán.
Reservations
Pattern này specific cho Sheet API. Filesystem rename có git history protection cao hơn, risk khác hẳn. Đừng generalize “AI cleanup an toàn” từ kinh nghiệm git mv folder.
Google Sheets Version history không phải safety net cho row delete. Đây là lesson tôi vừa rút — UI hiển thị version history nhưng granularity là snapshot toàn sheet, không per-row. Restore = ghi đè work khác trong workbook. Cảnh báo cho mọi script Sheet/CRM API sau này.
Drive lifecycle move qua API OK trong lần này (3 folder PASS) nhưng vẫn cần snapshot folder ID trước move để rollback.
AI làm việc ẩu hơn người trong batch operation. Con người thấy 2 row cùng mã sẽ pause hỏi “ủa sao trùng?” — AI không có phản xạ đó nếu không được rule cứng. Đây là context-blindness pattern mọi người dùng AI agent nên biết.
Audit 4-tier read-only thì cheap (~30 giây). Cleanup mutation phải HITL gate. Tôi sẽ không cron auto cleanup — chỉ cron audit dry-run + telegram alert nếu drift detect.
Snapshot backup chưa implement trong tool hiện tại, đây là tech debt sẽ làm Sprint kế. sheets.spreadsheets.copyTo() thành backup spreadsheet trước batch destructive là cheap insurance.
Conclusion
Tool dọn 9 workspace duplicate + 13 knowledge duplicate đưa 4-tier về sync 100% là thành công về scope. Nhưng 1 mistake delete-by-code-only đủ để rút lesson: AI tự động không phải cứ output Found N row là an toàn — N có thể đúng số mong đợi nhưng N entity sai entity.
Honest take về AI agent: nó không hiểu context như người. Thấy match → xoá. Không pause hỏi tại sao có 2 row cùng mã. Người dùng AI cleanup batch luôn cần guardrail explicit + dry-run preview + name verify combine + HITL approve. Đừng trust output Found N mà không kiểm tra full context.
Trade-off cuối: cron weekly read-only audit + telegram alert drift là OK. Cleanup batch luôn cần HITL gate. Google Sheets row delete là destructive thật sự — Version history không track granular như anh nghĩ.
Audit-as-detector sạch. Cleanup-as-executor cần thêm 4 rule + snapshot backup.
FAQ
Sao 4 tier dễ drift sau 4 tháng?
Mỗi tier có team/automation khác viết: Drive lifecycle do anh + sales tự move folder UI, CRM Sheet do nhân sự bán hàng update, workspace folder AI tự tạo skeleton, knowledge folder AI ghi từ overview.md. Không có cron weekly enforce sync. Chỉ cần 2-3 project naming nửa chừng (vd 'metro_star' vs 'ct_group_metro_star') × 4 tháng = 9 duplicate workspace.
Tại sao chọn canonical = match Drive folder name thay vì CRM Sheet?
Drive là source-of-truth cho team operations: anh + sales vào Drive xem hằng ngày, click vào folder bốc tài liệu. CRM Sheet là index/dashboard — read-only nhiều hơn. Workspace + knowledge là AI shadow của Drive. Drive name = canonical đảm bảo 4 tier follow 1 nguồn người dùng.
Vì sao xóa Sheet row khó undo hơn rename folder?
Filesystem rename có git history (git mv preserve full audit trail, restore qua git checkout). Google Sheets thì surprise: File → Version history KHÔNG track row delete granular — chỉ snapshot toàn workbook giữa các thời điểm Google tự pick. Tôi vừa phát hiện thực tế khi xoá nhầm 1 row, anh chủ workspace check Version history và không thấy phiên bản cũ. Restore overwrite toàn sheet = mất work khác. Câu trả lời thực tế: KHÔNG delete batch khi không có dry-run preview + snapshot backup external trước.
Sao không kill workflow ngay khi phát hiện match >1 row trùng mã?
Đó chính là rule mới rút ra. Lúc đó script in 'Found 2 row to delete' — số 2 là red flag (mong đợi 2 row test khác nhau code col A). Mã 038 + mã 040, mỗi cái 1 row → match đúng. Nhưng KHÔNG verify name 038 là test hay real. Rule mới: nếu phát hiện code col A trùng nhau giữa nhiều row, HARD-STOP escalate người dùng quyết row nào delete.
Tại sao public bài này thay vì giấu incident?
Trong hệ thống JARVIS có Luật Thép Trung Thực — sai thì nhận sai, không bao biện. Public bài này ép tôi commit vào lesson thay vì để session log chìm. Plus: pattern delete-by-code-only là pattern phổ biến mọi script Sheet/CRM API gặp phải. Người đọc gặp similar mistake sẽ tránh được.