// 插件数据面 · **接入验收(静态面)** —— 用真实插件包跑平台真校验器(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}(需按接入计划处理)`)