Files
waoowaoo/scripts/guards/task-target-states-no-polling-guard.mjs

97 lines
3.1 KiB
JavaScript

#!/usr/bin/env node
import fs from 'fs'
import path from 'path'
import process from 'process'
const root = process.cwd()
function fail(title, details = []) {
console.error(`\n[task-target-states-no-polling-guard] ${title}`)
for (const line of details) {
console.error(` - ${line}`)
}
process.exit(1)
}
function readFile(relativePath) {
const fullPath = path.join(root, relativePath)
if (!fs.existsSync(fullPath)) {
fail('Missing required file', [relativePath])
}
return fs.readFileSync(fullPath, 'utf8')
}
function walk(dir, out = []) {
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
if (entry.name === '.git' || entry.name === '.next' || entry.name === 'node_modules') continue
const full = path.join(dir, entry.name)
if (entry.isDirectory()) {
walk(full, out)
} else {
out.push(full)
}
}
return out
}
function toRel(fullPath) {
return path.relative(root, fullPath).split(path.sep).join('/')
}
function collectPattern(pattern) {
const files = walk(path.join(root, 'src'))
const hits = []
for (const fullPath of files) {
if (!fullPath.endsWith('.ts') && !fullPath.endsWith('.tsx')) continue
const text = fs.readFileSync(fullPath, 'utf8')
const lines = text.split('\n')
for (let i = 0; i < lines.length; i += 1) {
if (pattern.test(lines[i])) {
hits.push(`${toRel(fullPath)}:${i + 1}`)
}
}
}
return hits
}
const refetchIntervalMsHits = collectPattern(/\brefetchIntervalMs\b/)
if (refetchIntervalMsHits.length > 0) {
fail('Found forbidden refetchIntervalMs usage', refetchIntervalMsHits)
}
const voiceStagePath =
'src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/VoiceStage.tsx'
const voiceStageText = readFile(voiceStagePath)
if (voiceStageText.includes('setInterval(')) {
fail('VoiceStage must not use timer polling', [voiceStagePath])
}
const targetStateMapPath = 'src/lib/query/hooks/useTaskTargetStateMap.ts'
const targetStateMapText = readFile(targetStateMapPath)
if (!/refetchInterval:\s*false/.test(targetStateMapText)) {
fail('useTaskTargetStateMap must keep refetchInterval disabled', [targetStateMapPath])
}
const ssePath = 'src/lib/query/hooks/useSSE.ts'
const sseText = readFile(ssePath)
const targetStatesInvalidateExprMatch = sseText.match(
/const shouldInvalidateTargetStates\s*=\s*([\s\S]*?)\n\s*\n/,
)
if (!targetStatesInvalidateExprMatch) {
fail('Unable to locate shouldInvalidateTargetStates expression', [ssePath])
}
const targetStatesInvalidateExpr = targetStatesInvalidateExprMatch[1]
if (!/TASK_EVENT_TYPE\.COMPLETED/.test(targetStatesInvalidateExpr) || !/TASK_EVENT_TYPE\.FAILED/.test(targetStatesInvalidateExpr)) {
fail('useSSE must invalidate target states only for terminal events', [ssePath])
}
if (/TASK_EVENT_TYPE\.CREATED/.test(targetStatesInvalidateExpr)) {
fail('useSSE target-state invalidation must not include CREATED', [ssePath])
}
if (/TASK_EVENT_TYPE\.PROCESSING/.test(targetStatesInvalidateExpr)) {
fail('useSSE target-state invalidation must not include PROCESSING', [ssePath])
}
console.log('[task-target-states-no-polling-guard] OK')