范围 = 本线(插件投放与分库线)产物 + 记忆类,共 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/规则与载体/、归档/、接续包_*。
119 lines
6.6 KiB
JavaScript
119 lines
6.6 KiB
JavaScript
// 插件数据面 · **接入验收(静态面)** —— 用真实插件包跑平台真校验器(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}(需按接入计划处理)`)
|