Files
dsh_ai1net_server/交付物/插件接入验收-静态面-20260925.mjs
T
admin 318430c9e9 chore(工作区): 插件投放与分库线收口入库(第 35 棒 + 22:44 拍板落盘)
范围 = 本线(插件投放与分库线)产物 + 记忆类,共 27 件:
- 接续入口_插件投放与分库线(1 件):§0 新增 22:44 拍板行;§2 第 36 棒范围改 5 步、条件步转无条件
- 交付物(22 件):MCN 数据面接入阶段一/阶段二系列(含 B 案落地与 shadow 读数)、
  P0 修通与移动端真机验收、pnpm-EPERM、两机 lib 差异、共享层台账语义、
  基础插件身份与回滚、插件接入验收、插件数据面取数口、移动端迁包与字号扩面、跨机错误消息
- 记忆(4 件):2026-09-24 / 2026-09-25 日志、MEMORY.md、本棒 automation 执行记录

⛔ 未含他线在途改动(只报告、不代提交):机制层 CODEBUDDY.md / state.py /
.codebuddy/rules/server-ops.md、接续入口_IM线、接续入口_StoryForge验收线、
其余 29 个 automation 目录、docs/规则与载体/、归档/、接续包_*。
2026-09-25 23:06:29 +08:00

119 lines
6.6 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.
// 插件数据面 · **接入验收(静态面)** —— 用真实插件包跑平台真校验器(2026-09-25)
//
// 为什么要有这个脚本:`test/plugin-data-runtime.test.mjs` 验的是**端口判据**(用假执行器),
// 它证明不了"一个真实插件的声明能不能过平台校验、库名换算对不对、DDL 生成得对不对"。
// 本脚本拿**真插件包**(默认 `dsh-plugin-mcn`)调**平台真实现**(`lib/db/plugin-data/schema.js`),
// 把接入的静态面一次跑完 ⇒ 可复现、可回归。
//
// 覆盖四项:
// ① 声明解析与校验(`parseDeclFromDir`,与门户「检测第 3 项」同一个函数);
// ② 库名换算 + 白名单复校(`pluginDbNameOf` / `assertPluginDbName`);
// ③ 建库内 DDL 逐条(`declarationDdl`)—— 证明"建库后库里长什么样";
// ④ 红线机器判据(`DB-03 §二`:包内 ⛔ 不得出现 SQL / DB 驱动)—— **现状读数**,供接入排期用。
//
// 跑法:`node 交付物/插件接入验收-静态面-20260925.mjs [插件包目录]`
// ⚠️ 本脚本**只读**:不建库、不连 PG、不改插件包、不上传。
import { readFileSync, readdirSync, statSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
const HERE = dirname(fileURLToPath(import.meta.url))
/** 平台代码仓(真实现的来源)—— 与工作区无关,按绝对路径取。 */
const REPO = process.env.DSHS_REPO ?? 'D:/github/dsh_shenxian'
const PKG_DIR = process.argv[2] ?? 'D:/dshworkspace/plugin_package/dsh-plugin-mcn'
const PKG_NAME = process.env.PKG_NAME ?? 'dsh-plugin-mcn'
const schema = await import(pathToFileURL(join(REPO, 'lib/db/plugin-data/schema.js')).href)
/* ── 输出小件(结构化、可 grep)───────────────────────────────────────────── */
const rows = []
const say = (tag, text) => { rows.push(`${tag} ${text}`); console.log(`${tag} ${text}`) }
const PASS = (t) => say(' ✅', t)
const FAIL = (t) => say(' ❌', t)
const INFO = (t) => say(' ·', t)
console.log(`═══ 插件数据面 · 接入验收(静态面)═══`)
console.log(`插件包:${PKG_DIR}`)
console.log(`包 名:${PKG_NAME}`)
console.log(`校验器:${REPO}/lib/db/plugin-data/schema.js(平台真实现)\n`)
/* ── ① 声明解析与校验(与门户「检测第 3 项」同源)─────────────────────────── */
const parsed = schema.parseDeclFromDir(PKG_DIR, PKG_NAME)
INFO(`声明来源(origin)= ${parsed.origin}`)
if (parsed.level !== 'ok' || parsed.decl === null) {
FAIL(`声明未通过校验:level=${parsed.level}`)
for (const f of parsed.findings) FAIL(` ${f.target} [${f.code}] ${f.detail}`)
if (parsed.note !== null) INFO(`note:${parsed.note}`)
console.log('\n结论:❌ 接入未通过(声明层)')
process.exit(1)
}
const decl = parsed.decl
PASS(`声明通过校验:${decl.tables.length} 张表 · schemaVersion=${decl.schemaVersion} · source=${decl.source}`)
for (const t of decl.tables) {
PASS(` 表 ${t.name}(scope=${t.scope} · ${t.columns.length} 列 · ${t.indexes.length} 索引 · 物理名 ${t.physicalName})`)
}
/* ── ② 库名换算 + 白名单复校 ────────────────────────────────────────────── */
const dbName = schema.pluginDbNameOf(PKG_NAME)
try {
schema.assertPluginDbName(dbName)
PASS(`库名换算:${PKG_NAME} ⇒ ${dbName}(过白名单 ${schema.PLUGIN_DB_RE.source})`)
} catch (err) {
FAIL(`库名未过白名单:${String(err.message)}`)
process.exit(1)
}
if (decl.dbName !== dbName) FAIL(`⚠️ 声明自算的库名(${decl.dbName}) 与换算(${dbName}) 不一致`)
else PASS('声明自算库名与换算一致(这正是运行时「库名不一致即拒」判据的正面用例)')
/* ── ③ 建库内 DDL 逐条 ──────────────────────────────────────────────────── */
const ddl = schema.declarationDdl(decl)
console.log(`\n ── 建库后库内 DDL(${ddl.length} 条)──`)
for (const line of ddl) console.log(` ${line.replace(/\s+/g, ' ').slice(0, 150)}`)
const hasPk = ddl.filter((d) => d.startsWith('CREATE TABLE')).every((d) => /PRIMARY KEY \("[a-z_]+", "[a-z_]+"\)/.test(d))
if (hasPk) PASS('每张表都是「(归属列, 声明第一列)」复合主键 —— ⛔ 无代理自增 id')
else FAIL('有表的主键不是「(归属列, 第一列)」形态')
const hasAcl = ddl.filter((d) => d.startsWith('CREATE TABLE')).every((d) => d.includes('"user_id"') || d.includes('"room_id"'))
if (hasAcl) PASS('归属列已由内核补齐(scope:user ⇒ user_id)')
else FAIL('缺归属列')
/* ── ④ 红线机器判据(现状读数)──────────────────────────────────────────── */
const SQL_RE = /CREATE TABLE|ALTER TABLE|DROP TABLE|better-sqlite3|from 'pg'|require\('pg'\)/
function scan(dir, depth = 0) {
const hits = []
if (depth > 4) return hits
let entries
try { entries = readdirSync(dir) } catch { return hits }
for (const name of entries) {
if (name === 'node_modules' || name.startsWith('.')) continue
const abs = join(dir, name)
let st
try { st = statSync(abs) } catch { continue }
if (st.isDirectory()) { hits.push(...scan(abs, depth + 1)); continue }
if (!/\.(js|mjs|cjs|ts)$/.test(name)) continue
let text
try { text = readFileSync(abs, 'utf8') } catch { continue }
const lines = text.split('\n')
lines.forEach((line, i) => {
if (SQL_RE.test(line) && !line.trimStart().startsWith('*') && !line.trimStart().startsWith('//')) {
hits.push(`${abs.replace(PKG_DIR, '.')}:${i + 1}`)
}
})
}
return hits
}
const sqlHits = scan(PKG_DIR)
console.log('')
INFO(`红线扫描(DB-03 §二)命中 ${sqlHits.length} 处 —— 这是**现状读数**,不判本次声明成败:`)
for (const h of sqlHits.slice(0, 12)) console.log(` ${h}`)
if (sqlHits.length > 12) console.log(` …(另有 ${sqlHits.length - 12} 处)`)
if (sqlHits.length === 0) PASS('包内零 SQL / 零 DB 驱动 ⇒ 已符合接入前置条件')
else INFO('⚠️ 存量本地库(better-sqlite3)仍在 ⇒ 接入数据面须先改读写走内核 API(见交付件 §六)')
console.log(`\n结论:✅ 声明层接入验收通过(库名 ${dbName});红线项数 ${sqlHits.length}(需按接入计划处理)`)