Files
2026-07-13 13:12:33 +08:00

79 lines
2.5 KiB
JavaScript

import { readFileSync } from 'node:fs'
import { join, relative } from 'node:path'
import { fileURLToPath } from 'node:url'
import { walkFiles } from './lib/fs-walk.mjs'
const root = fileURLToPath(new URL('..', import.meta.url))
const srcRoot = join(root, 'src')
const allowedDesktopGlobal = new Set([
'src/platform/capabilities.ts',
'src/platform/desktop.ts',
'src/vite-env.d.ts',
])
const bannedPatterns = [
{
pattern: 'window.opensquillaDesktop',
allow: allowedDesktopGlobal,
message: 'Electron preload access must stay behind src/platform/.',
},
]
const stalePlatformPatterns = [
'activeProfile',
'cloudUrl',
'getDesktopRpcConnection',
'desktop:rpc-connection',
]
// live-turn fold fence: the append-only turn log and its pure reducer are an internal
// live-turn detail. Keep their imports inside the chat composables (where the
// legacy live refs live) plus ChatView, so the new path cannot leak into other
// layers before it is ever authoritative.
function isUnderChatComposables(rel) {
const normalized = rel.split('\\').join('/')
return normalized.startsWith('src/composables/chat/')
}
function isChatView(rel) {
return rel.split('\\').join('/') === 'src/views/ChatView.vue'
}
const turnLogModulePatterns = [
'@/composables/chat/useChatTurnLog',
'@/composables/chat/turnParity',
'@/utils/chat/foldTurn',
]
// Test files exercise the fenced modules directly (that is their job) and are
// not a runtime layer, so they are exempt from the import fence below.
function isTestFile(entry) {
return /\.(test|spec)\.(ts|tsx)$/.test(entry)
}
const failures = []
for (const file of walkFiles(srcRoot, /\.(ts|vue)$/, { skipFile: isTestFile })) {
const rel = relative(root, file).replace(/\\/g, '/')
const body = readFileSync(file, 'utf8')
for (const rule of bannedPatterns) {
if (body.includes(rule.pattern) && !rule.allow.has(rel)) {
failures.push(`${rel}: ${rule.message} Found "${rule.pattern}".`)
}
}
for (const pattern of stalePlatformPatterns) {
if (body.includes(pattern)) {
failures.push(`${rel}: stale desktop/cloud platform pattern found: "${pattern}".`)
}
}
if (!isUnderChatComposables(rel) && !isChatView(rel)) {
for (const moduleId of turnLogModulePatterns) {
if (body.includes(moduleId)) {
failures.push(`${rel}: live-turn log "${moduleId}" must stay within src/composables/chat/ or views/ChatView.vue.`)
}
}
}
}
if (failures.length > 0) {
console.error(failures.join('\n'))
process.exit(1)
}
console.log('Architecture guard passed.')