#!/usr/bin/env node /** * 「靜態租戶字串不得用於資料面過濾」機械閘的**執行殼**(Arcrun#108)。 * * 規則本體(純函式、零 node 相依)在 `tenant-source-rules.mjs`——拆開的理由是 * **這道閘自己要能被測試**:Workers runtime 的 vitest 沒有 node:fs,規則若和走檔案系統的 * 程式碼綁在一起就 import 不動,測試也就寫不出來(當晚有一道閘連讀自己的原始碼都擋, * 結果沒人驗得了它會不會誤攔)。現在 tests/tenant-gate.test.ts 直接餵字串驗規則。 * * 用法: * node scripts/check-tenant-source.mjs [projectRoot] # 掃 src/,有違規 → exit 1 * node scripts/check-tenant-source.mjs --stdin <相對路徑> # 從 stdin 讀「即將寫入的內容」 * npm run check:tenant * * `--stdin` 是給 `.claude/hooks/pre-write-guard.sh`(規則 8.1)用的:在檔案**還沒寫下去之前** * 就擋,這樣違規根本進不了工作區。Edit 只給片段也沒關係——規則是逐行的,正好只看新寫的那幾行。 */ import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join, relative, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; import { scanSource } from './tenant-source-rules.mjs'; /** 遞迴列出目錄下的 .ts 檔(相對 root 的路徑)。 */ function listTsFiles(root, dir = root, out = []) { for (const name of readdirSync(dir)) { const full = join(dir, name); if (statSync(full).isDirectory()) listTsFiles(root, full, out); else if (name.endsWith('.ts')) out.push(relative(root, full)); } return out; } /** 掃整個 cypher-executor/src。回傳違規清單。 */ export function scanProject(projectRoot) { const srcRoot = join(projectRoot, 'src'); const all = []; for (const rel of listTsFiles(projectRoot, srcRoot)) { const relPosix = rel.split(sep).join('/'); all.push( ...scanSource(relPosix, readFileSync(join(projectRoot, rel), 'utf8')).map((v) => ({ ...v, file: relPosix, })), ); } return all; } /** stdin 模式:讀「即將寫入的內容」,印違規、有違規 → exit 1。 */ async function runStdin(relPath) { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const violations = scanSource(relPath, Buffer.concat(chunks).toString('utf8')); if (violations.length === 0) return 0; for (const v of violations) { console.error(`[${v.rule}] ${relPath}(新寫入的第 ${v.line} 行):${v.text}`); console.error(` → ${v.message}`); } return 1; } if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { if (process.argv[2] === '--stdin') { process.exit(await runStdin(process.argv[3] ?? 'src/unknown.ts')); } const projectRoot = process.argv[2] ?? process.cwd(); const violations = scanProject(projectRoot); if (violations.length === 0) { console.log('✓ 租戶來源檢查通過:資料面 owner_id 全部來自 src/lib/tenant.ts'); process.exit(0); } console.error('❌ 租戶來源檢查失敗(Arcrun#108 的閘)\n'); for (const v of violations) { console.error(` [${v.rule}] ${v.file}:${v.line}`); console.error(` ${v.text}`); console.error(` → ${v.message}\n`); } process.exit(1); }