Files
dsh_shenxian/scripts/check-layering.mjs
T
admin 640813e84e 覆盖网络线 S0+S1 落地:可达性单一入口 + 会合地址出 env
S0(本机):新增 src/net/reachability.ts(Reachability 描述 + agentBaseUrlOf 唯一取值入口)、src/net/rendezvous.ts、docs/architecture.md(四层划分 + 依赖方向 + R1-R4 判据)、scripts/check-layering.mjs + layering-baseline.json、test/reachability.test.mjs;src/supervisor/remote-spawner.ts 与 src/web/server.ts 改为统一走 agentBaseUrlOf(),agentUrl 降级为可选旧字段(向后兼容)。

S1(本机 + 106):新增 DSHS_RENDEZVOUS_URL 出 env(src/config.ts 解析 clusterRendezvousUrl,优先级 overrides > 新变量 > DSHS_TUNNEL_TARGET 兜底 > 空),src/worker/tunnel.ts 新增 normalizeTunnelTarget()、src/worker/agent.ts 改读新配置 ⇒ 双路径并存、可零代码回滚(删掉新 env 即走旧路径)。106 已上线,健康检查 tunnel.ready=true。

package.json 增加 check:layering 脚本并接入 verify;README 登记分层文档。

验收:npm run build 通过;npm test 44/44;check:layering 无新增违规(基线 5 条)。
2026-09-16 15:02:52 +08:00

129 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 分层依赖方向检查 —— `docs/architecture.md` 的 R1–R4 **可执行判据**。
*
* ## 为什么必须有它
* 规则**只有文字**时靠自觉 ⇒ 扩张期一定会走丢。本脚本把四条规则变成一次静态扫描,
* "分层有没有走形"不再靠人判断(`npm run check:layering`)。
*
* ## 判据(层号 ① 最上、④ 最下;**目标层号 < 源层号 ⇒ 违规**)
* | 规则 | 内容 |
* |---|---|
* | R1 | 上层可依赖下层,**下层不得 import 上层** |
* | R3 | `net/*` 属能力层,**不得 import `web/*`** |
* | R4 | `config/crypto/isolation/index` **零业务依赖**(任何子目录 import 都算违规) |
*
* ## 基线(ratchet,不是豁免)
* 现存违规登记在 `scripts/layering-baseline.json`(**键 = 源→目标 的边**,不含行号 ⇒ 抗行号漂移)。
* · **新增**违规 ⇒ 退出码 **1**(规则从今天起生效)
* · 基线里**已消失**的项 ⇒ 打印提醒,可 `--update-baseline` 收紧
* 这样"立规矩"不会因为存量债而把 `npm run verify` 卡红。
*
* 用法:`node scripts/check-layering.mjs [--update-baseline] [--json]`
*/
import { readFileSync, readdirSync, statSync, writeFileSync, existsSync } from 'node:fs'
import { join, dirname, relative, sep } from 'node:path'
const ROOT = process.cwd()
const SRC = join(ROOT, 'src')
const BASELINE = join(ROOT, 'scripts', 'layering-baseline.json')
const args = new Set(process.argv.slice(2))
const posixify = (p) => p.split(sep).join('/')
/** 层归属:**只按目录判**,不做语义推断(判据要能一眼看懂)。 */
function layerOf(file) {
const p = posixify(file)
if (p === 'src/cli.ts' || p.startsWith('src/web/')) return 1
if (p.startsWith('src/domain/')) return 2
if (/^src\/(supervisor|db|fs|worker|net|nginx)\//.test(p)) return 3
if (/^src\/(config|crypto|isolation|index)\.ts$/.test(p)) return 4
return null // 未归类 ⇒ 新增目录必须先想清"它属于哪一层"
}
const NAME = { 1: '①入口', 2: '②领域', 3: '③能力', 4: '④基础' }
function walk(dir) {
const out = []
for (const e of readdirSync(dir)) {
const full = join(dir, e)
if (statSync(full).isDirectory()) out.push(...walk(full))
else if (e.endsWith('.ts')) out.push(full)
}
return out
}
/** 所有 `from '<相对>'` / `from "<相对>"`(含 `export … from`)。 */
function importsOf(text) {
const out = []
text.split('\n').forEach((line, i) => {
const re = /\bfrom\s+['"]([^'"]+)['"]/g
let m
while ((m = re.exec(line)) !== null) if (m[1].startsWith('.')) out.push({ spec: m[1], line: i + 1 })
})
return out
}
// ⚠️ 统一成**相对仓根**的 posix 路径 —— 层判据是按 `src/...` 写的,
// 直接喂绝对路径会让每个文件都落进"未归类"(首版就踩了这个坑,静默全绿)。
const files = walk(SRC).map((f) => posixify(relative(ROOT, f)))
const counts = { 1: 0, 2: 0, 3: 0, 4: 0 }
const unclassified = []
const found = [] // { edge, detail }
for (const file of files) {
const srcLayer = layerOf(file)
if (srcLayer === null) {
unclassified.push(file)
continue
}
counts[srcLayer] += 1
for (const { spec, line } of importsOf(readFileSync(join(ROOT, file), 'utf8'))) {
const target = posixify(join(dirname(file), spec)).replace(/\.js$/, '.ts')
const tgtLayer = layerOf(target)
if (tgtLayer === null) continue
const edge = `${file} → ${target}`
const detail = `${file}:${line} → ${target}`
if (tgtLayer < srcLayer) {
const tag = srcLayer === 3 && file.includes('/net/') && target.startsWith('src/web/') ? 'R3' : 'R1'
found.push({ edge, detail: `${tag} ${detail} [${NAME[srcLayer]} → ${NAME[tgtLayer]}]` })
} else if (srcLayer === 4) {
found.push({ edge, detail: `R4 ${detail} [④基础层必须零业务依赖]` })
}
}
}
// ── 基线比对 ────────────────────────────────────────────────────────────────
const baseline = existsSync(BASELINE) ? new Set(JSON.parse(readFileSync(BASELINE, 'utf8')).edges ?? []) : null
const isNew = (v) => baseline === null || !baseline.has(v.edge)
const added = found.filter(isNew)
const known = found.filter((v) => !isNew(v))
const fixed = baseline === null ? [] : [...baseline].filter((e) => !found.some((v) => v.edge === e))
if (args.has('--update-baseline')) {
writeFileSync(BASELINE, `${JSON.stringify({ note: 'R1–R4 存量违规基线(ratchet):只许减、不许增;新增违规会让 check:layering 失败', edges: [...new Set(found.map((v) => v.edge))].sort() }, null, 2)}\n`)
console.log(`✓ 基线已更新:${found.length} 条边 → ${posixify(BASELINE)}`)
process.exit(0)
}
if (args.has('--json')) {
console.log(JSON.stringify({ counts, unclassified, added, known, fixed }, null, 2))
process.exit(added.length > 0 ? 1 : 0)
}
console.log('=== 分层依赖检查(docs/architecture.md R1–R4)===')
console.log(`扫描:${files.length} 个 .ts|归属 ①${counts[1]} ②${counts[2]} ③${counts[3]} ④${counts[4]}|未归类 ${unclassified.length}`)
if (unclassified.length > 0) console.log(`⚠️ 未归类(新增目录必须先定层):${unclassified.slice(0, 5).join(', ')}${unclassified.length > 5 ? ' …' : ''}`)
console.log(`现存违规 ${found.length} 条(其中基线内 ${known.length} 条)`)
if (added.length > 0) {
console.log(`\n🔴 新增违规 ${added.length} 条(⛔ 规则已生效,不得引入):`)
for (const v of added) console.log(` ${v.detail}`)
}
if (fixed.length > 0) {
console.log(`\n🟢 基线内已消除 ${fixed.length} 条(可 --update-baseline 收紧):`)
for (const e of fixed.slice(0, 8)) console.log(` ${e}`)
}
if (added.length === 0) {
console.log(baseline === null ? '\n⚠️ 尚无基线文件:首次请先跑 --update-baseline 建立(否则任何存量违规都会被算作"新增")' : '\n✅ 无新增违规')
}
process.exit(added.length > 0 ? 1 : 0)