release: opensource snapshot 2026-02-27 19:25:00
This commit is contained in:
132
scripts/guards/task-loading-guard.mjs
Normal file
132
scripts/guards/task-loading-guard.mjs
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import process from 'process'
|
||||
|
||||
const workspaceRoot = process.cwd()
|
||||
const baselinePath = path.join(workspaceRoot, 'scripts/guards/task-loading-baseline.json')
|
||||
|
||||
function walkFiles(dir, out = []) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.next') continue
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
walkFiles(fullPath, out)
|
||||
} else {
|
||||
out.push(fullPath)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function toPosixRelative(filePath) {
|
||||
return path.relative(workspaceRoot, filePath).split(path.sep).join('/')
|
||||
}
|
||||
|
||||
function collectMatches(files, pattern) {
|
||||
const matches = []
|
||||
for (const fullPath of files) {
|
||||
if (!fullPath.endsWith('.ts') && !fullPath.endsWith('.tsx')) continue
|
||||
const relPath = toPosixRelative(fullPath)
|
||||
const content = fs.readFileSync(fullPath, 'utf8')
|
||||
const lines = content.split('\n')
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
if (lines[i].includes(pattern)) {
|
||||
matches.push(`${relPath}:${i + 1}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
function fail(title, lines) {
|
||||
console.error(`\n[task-loading-guard] ${title}`)
|
||||
for (const line of lines) {
|
||||
console.error(` - ${line}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(baselinePath)) {
|
||||
fail('Missing baseline file', [toPosixRelative(baselinePath)])
|
||||
}
|
||||
|
||||
const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'))
|
||||
const allowedFiles = new Set(baseline.allowedDirectTaskStateUsageFiles || [])
|
||||
const allowedLegacyGeneratingFiles = new Set(baseline.allowedLegacyGeneratingUsageFiles || [])
|
||||
const allFiles = walkFiles(path.join(workspaceRoot, 'src'))
|
||||
|
||||
const directTaskStateUsage = collectMatches(allFiles, 'useTaskTargetStates(')
|
||||
const directUsageOutOfAllowlist = directTaskStateUsage
|
||||
.map((entry) => entry.split(':')[0])
|
||||
.filter((file) => !allowedFiles.has(file))
|
||||
|
||||
if (directUsageOutOfAllowlist.length > 0) {
|
||||
fail(
|
||||
'Found component-level direct useTaskTargetStates outside baseline allowlist',
|
||||
Array.from(new Set(directUsageOutOfAllowlist)),
|
||||
)
|
||||
}
|
||||
|
||||
const crossDomainLabels = collectMatches(allFiles, 'video.panelCard.generating')
|
||||
if (crossDomainLabels.length > 0) {
|
||||
fail('Found cross-domain loading label reuse (video.panelCard.generating)', crossDomainLabels)
|
||||
}
|
||||
|
||||
const uiFiles = allFiles.filter((file) => {
|
||||
const relPath = toPosixRelative(file)
|
||||
return relPath.startsWith('src/app/') || relPath.startsWith('src/components/')
|
||||
})
|
||||
const legacyGeneratingPatterns = [
|
||||
'appearance.generating',
|
||||
'panel.generatingImage',
|
||||
'shot.generatingImage',
|
||||
'line.generating',
|
||||
]
|
||||
const legacyGeneratingMatches = legacyGeneratingPatterns.flatMap((pattern) =>
|
||||
collectMatches(uiFiles, pattern),
|
||||
)
|
||||
const legacyGeneratingOutOfAllowlist = legacyGeneratingMatches
|
||||
.map((entry) => entry.split(':')[0])
|
||||
.filter((file) => !allowedLegacyGeneratingFiles.has(file))
|
||||
if (legacyGeneratingOutOfAllowlist.length > 0) {
|
||||
fail(
|
||||
'Found legacy generating truth usage in UI components',
|
||||
Array.from(new Set(legacyGeneratingOutOfAllowlist)),
|
||||
)
|
||||
}
|
||||
|
||||
const hooksIndexPath = path.join(workspaceRoot, 'src/lib/query/hooks/index.ts')
|
||||
if (fs.existsSync(hooksIndexPath)) {
|
||||
const hooksIndex = fs.readFileSync(hooksIndexPath, 'utf8')
|
||||
const bannedReexports = [
|
||||
{
|
||||
pattern: /export\s*\{[^}]*useGenerateCharacterImage[^}]*\}\s*from\s*['"]\.\/useGlobalAssets['"]/m,
|
||||
message: 'hooks/index.ts must not export useGenerateCharacterImage from useGlobalAssets',
|
||||
},
|
||||
{
|
||||
pattern: /export\s*\{[^}]*useGenerateLocationImage[^}]*\}\s*from\s*['"]\.\/useGlobalAssets['"]/m,
|
||||
message: 'hooks/index.ts must not export useGenerateLocationImage from useGlobalAssets',
|
||||
},
|
||||
{
|
||||
pattern: /export\s*\{[^}]*useGenerateProjectCharacterImage[^}]*\}\s*from\s*['"]\.\/useProjectAssets['"]/m,
|
||||
message: 'hooks/index.ts must not export useGenerateProjectCharacterImage from useProjectAssets',
|
||||
},
|
||||
{
|
||||
pattern: /export\s*\{[^}]*useGenerateProjectLocationImage[^}]*\}\s*from\s*['"]\.\/useProjectAssets['"]/m,
|
||||
message: 'hooks/index.ts must not export useGenerateProjectLocationImage from useProjectAssets',
|
||||
},
|
||||
]
|
||||
|
||||
const violations = bannedReexports
|
||||
.filter((item) => item.pattern.test(hooksIndex))
|
||||
.map((item) => item.message)
|
||||
|
||||
if (violations.length > 0) {
|
||||
fail('Found non-canonical mutation re-exports', violations)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[task-loading-guard] OK')
|
||||
Reference in New Issue
Block a user