Files

1101 lines
42 KiB
Markdown
Raw Permalink 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.
# 纯 Node.js 开发库 Schema 导出器实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 使用 Node.js 22.x 与 `mysql2` 完整导出 MySQL 5.7.x 开发库 Schema,移除本机 `mysql`/`mysqldump` 依赖,并恢复自动版本开发门禁。
**Architecture:** `tools/database/schema-baseline.mjs` 保留环境、版本、产物和历史验证职责;新增 `tools/database/mysql2-schema-exporter.mjs` 负责单连接只读目录查询、`SHOW CREATE`、视图排序、定义一致性复核和确定性 SQL 渲染。新产物使用 format v3,历史 format v1/v2 永久只读兼容。
**Tech Stack:** Node.js 22.x、原生 ESM、Node test runner、`mysql2` 3.23.3、MySQL 5.7.x、SHA-256、GitNexus、Git/Gitea。
## Global Constraints
- 只能读取 `APP_ENV=development` 的 `a_share_analysis_dev`,主机必须命中现有代码白名单。
- MySQL 服务端必须为 5.7.x,数据库字符集必须为 `utf8mb4`,排序规则必须为 `utf8mb4_general_ci`。
- 禁止查询业务表数据、访问生产数据库、执行 DDL/DML、多语句、锁表或迁移。
- 密码不得进入命令参数、日志、异常、SQL、清单、测试快照或 Git。
- format v1/v2 历史目录不得重写;format v3 不复用旧客户端字段语义。
- 关键逻辑提供准确中文注释,文件按稳定职责拆分,不继续细分无独立职责的 helper 文件。
- 每次提交使用中文 Conventional Commit 并立即推送 `origin`;提交前执行 GitNexus detect-changes。
- `v0.0.9` 基线必须作为独立提交推送,不能与导出器实现混在一个提交。
- GOV-012 作为独立治理版本发布为 `v0.0.10`;发布标签推送后还要封存 `v0.0.10` Schema,才能恢复下一功能的严格开发门禁。
## 文件职责
- Create: `tools/database/mysql2-schema-exporter.mjs`:连接配置、目录读取、对象排序、`SHOW CREATE`、定义复核和 SQL 渲染。
- Create: `tools/database/mysql2-schema-exporter.test.mjs`:纯假连接的导出器单元测试。
- Modify: `tools/database/schema-baseline.mjs`:format v3 产物、历史格式分支、异步生成入口和 exporter 集成。
- Modify: `tools/database/schema-baseline.test.mjs`:format v3、异步入口、历史兼容和持久化测试。
- Modify: `package.json`:根工作区直接依赖 `mysql2`,Schema 测试脚本包含两个测试文件。
- Modify: `pnpm-lock.yaml`:记录根工作区依赖关系,不新增第二份不同版本。
- Modify: `docs/governance/code-development-standards.md`:现行 Schema 规范改为纯 Node `SHOW CREATE`。
- Modify: `docs/governance/development-workflow.md`:format v3 和无 CLI 前置条件。
- Modify: `README.md`:说明本机无需安装 MySQL Server 或 CLI。
- Create: `database/schema-baselines/v0.0.9/manifest.yaml`,或在实际非空时同时创建 `schema.sql` 与 `checksum.sha256`。
- Modify: `VERSION`、`CHANGELOG.md`、`docs/index.md`:发布 GOV-012 为 `v0.0.10`。
- Create in Task 9: `database/schema-baselines/v0.0.10/manifest.yaml`,或在实际非空时同时创建 `schema.sql` 与 `checksum.sha256`。
- Create in Task 7: GOV-012 `qa/`、`review/`、`gitnexus/after.md`、`detected-changes.md`、`comparison.md`、`development-summary.md` 和 `release-notes.md`。
---
### Task 1: 闭合 GitNexus 风险并建立导出器安全契约
**Files:**
- Modify: `docs/features/GOV-012-node-schema-exporter/gitnexus/impact-plan.md`
- Create: `tools/database/mysql2-schema-exporter.test.mjs`
- Create: `tools/database/mysql2-schema-exporter.mjs`
- Modify: `package.json:22`
- Modify: `pnpm-lock.yaml`
**Interfaces:**
- Consumes: 已由 `assertSafeDevelopmentConfiguration(config)` 校验的 development 配置对象。
- Produces: `MYSQL2_EXPORTER_METADATA`、`DEFAULT_EXPORT_LIMITS`、`SchemaExportError`、`quoteIdentifier()`、`buildConnectionOptions()` 和 `assertMysql57ServerMetadata()`。
- [ ] **Step 1: 重新执行治理关键符号的 upstream impact**
对以下符号运行 GitNexus upstream impact,并把直接调用方、受影响流程和实际风险回填 `gitnexus/impact-plan.md`:
```text
runBaselineGeneration
assertMysql57Metadata
buildCapturedArtifacts
verifyBaselineDirectory
runCommandLine
```
若结果为 HIGH 或 CRITICAL,先向用户报告实际影响再继续。若 MCP 仍返回 `Transport closed`,运行 `npx gitnexus analyze` 和 `npx gitnexus status`,保存工具限制,并用 `rg` 列出全部静态引用,不能记录为“无影响”。
- [ ] **Step 2: 安装根工作区直接依赖**
Run:
```bash
pnpm add --save-dev --workspace-root mysql2@3.23.3
```
Expected: 根 `package.json` 的 `devDependencies.mysql2` 为 `3.23.3`;锁文件复用仓库已有的 `mysql2@3.23.3` 包记录。
- [ ] **Step 3: 写安全基础的失败测试**
在 `mysql2-schema-exporter.test.mjs` 写入以下契约:
```js
import assert from 'node:assert/strict';
import test from 'node:test';
import {
DEFAULT_EXPORT_LIMITS,
MYSQL2_EXPORTER_METADATA,
SchemaExportError,
assertMysql57ServerMetadata,
buildConnectionOptions,
quoteIdentifier,
} from './mysql2-schema-exporter.mjs';
const config = Object.freeze({
MYSQL_HOST: '127.0.0.1',
MYSQL_PORT: '3306',
MYSQL_DATABASE: 'a_share_analysis_dev',
MYSQL_USERNAME: 'schema_reader',
MYSQL_PASSWORD: 'not-for-logs',
});
test('连接配置固定单连接、utf8mb4、东八区且禁用多语句', () => {
const options = buildConnectionOptions(config);
assert.deepEqual(options, {
host: '127.0.0.1',
port: 3306,
user: 'schema_reader',
password: 'not-for-logs',
database: 'a_share_analysis_dev',
charset: 'utf8mb4',
timezone: '+08:00',
connectTimeout: 5_000,
multipleStatements: false,
enableKeepAlive: true,
});
});
test('标识符转义反引号并拒绝空值和控制字符', () => {
assert.equal(quoteIdentifier('market`view'), '`market``view`');
for (const value of ['', 'line\nbreak', 'zero\0byte']) {
assert.throws(() => quoteIdentifier(value), SchemaExportError);
}
});
test('只接受 MySQL 5.7.x 与固定字符规则', () => {
const valid = {
serverVersion: '5.7.42-log',
databaseCharset: 'utf8mb4',
databaseCollation: 'utf8mb4_general_ci',
};
assert.doesNotThrow(() => assertMysql57ServerMetadata(valid));
for (const serverVersion of ['5.6.51', '8.0.40', '10.6.18-MariaDB']) {
assert.throws(() => assertMysql57ServerMetadata({ ...valid, serverVersion }));
}
});
```
- [ ] **Step 4: 运行测试并确认红灯**
Run:
```bash
node --test tools/database/mysql2-schema-exporter.test.mjs
```
Expected: FAIL,原因是 `mysql2-schema-exporter.mjs` 或导出符号尚不存在。
- [ ] **Step 5: 实现最小安全基础**
在新模块中建立以下公开接口和固定保护值:
```js
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const mysql2Version = require('mysql2/package.json').version;
export const MYSQL2_EXPORTER_METADATA = Object.freeze({
name: 'mysql2-show-create',
version: '1.0.0',
driverName: 'mysql2',
driverVersion: mysql2Version,
});
export const DEFAULT_EXPORT_LIMITS = Object.freeze({
connectTimeoutMs: 5_000,
queryTimeoutMs: 5_000,
totalTimeoutMs: 30_000,
maxObjects: 5_000,
maxDefinitionBytes: 1_048_576,
});
export class SchemaExportError extends Error {
constructor(code, message) {
super(message);
this.name = 'SchemaExportError';
this.code = code;
}
}
```
`buildConnectionOptions()` 只映射已校验配置,`quoteIdentifier()` 只接受非空、无控制字符字符串,`assertMysql57ServerMetadata()` 固定校验 5.7.x、`utf8mb4` 和 `utf8mb4_general_ci`。
- [ ] **Step 6: 运行测试和依赖审计**
Run:
```bash
node --test tools/database/mysql2-schema-exporter.test.mjs
pnpm audit --audit-level high
```
Expected: 新测试 PASS;审计无 high/critical 漏洞。若审计失败,停止并记录依赖风险,不升级到未评估版本。
- [ ] **Step 7: 检测影响、提交并推送**
先调用 GitNexus MCP:
```text
detect_changes({ repo: "a-share-analysis", scope: "all" })
```
确认只命中本任务的依赖、安全契约和测试后,再运行:
```bash
git add package.json pnpm-lock.yaml tools/database/mysql2-schema-exporter.mjs tools/database/mysql2-schema-exporter.test.mjs docs/features/GOV-012-node-schema-exporter/gitnexus/impact-plan.md
git commit -m "feat(GOV-012): 建立纯 Node 导出器安全契约"
git push origin main
```
Expected: 只新增治理工具依赖、安全基础和测试;远程 `main` 与本地提交一致。
---
### Task 2: 实现对象目录读取与视图依赖排序
**Files:**
- Modify: `tools/database/mysql2-schema-exporter.mjs`
- Modify: `tools/database/mysql2-schema-exporter.test.mjs`
**Interfaces:**
- Consumes: mysql2 promise connection 的 `query({ sql, timeout, values })`。
- Produces: `readSchemaCatalog(connection, database, limits)`、`orderViewNames(catalog)` 和稳定 `SchemaCatalog` 对象。
- [ ] **Step 1: 为目录和拓扑排序写失败测试**
测试固定目录结构:
```js
const catalog = Object.freeze({
tables: ['market_snapshot'],
views: ['market_summary', 'market_dashboard'],
viewDependencies: [
{ view: 'market_dashboard', referencedSchema: 'a_share_analysis_dev', referencedObject: 'market_summary' },
{ view: 'market_summary', referencedSchema: 'a_share_analysis_dev', referencedObject: 'market_snapshot' },
],
routines: [
{ name: 'calculate_breadth', type: 'FUNCTION' },
{ name: 'refresh_market_snapshot', type: 'PROCEDURE' },
],
triggers: [
{ name: 'snapshot_before_insert', table: 'market_snapshot', timing: 'BEFORE', event: 'INSERT', order: 1 },
],
events: ['purge_market_snapshot'],
});
assert.deepEqual(orderViewNames(catalog), ['market_summary', 'market_dashboard']);
```
再覆盖:视图循环依赖、跨 Schema 依赖、依赖未知对象、超过 5000 个对象和目录查询返回未知 `TABLE_TYPE` 时抛出固定 `SchemaExportError.code`。
- [ ] **Step 2: 运行测试确认红灯**
Run:
```bash
node --test --test-name-pattern="目录|视图" tools/database/mysql2-schema-exporter.test.mjs
```
Expected: FAIL,缺少目录读取和拓扑排序实现。
- [ ] **Step 3: 实现固定参数化目录查询**
模块内固定以下查询,数据库名只通过 `values: [database]` 传入:
```sql
SELECT TABLE_NAME, TABLE_TYPE
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = ?
ORDER BY TABLE_NAME
```
```sql
SELECT VIEW_NAME, TABLE_SCHEMA AS REFERENCED_SCHEMA_NAME,
TABLE_NAME AS REFERENCED_TABLE_NAME
FROM information_schema.VIEW_TABLE_USAGE
WHERE VIEW_SCHEMA = ?
ORDER BY VIEW_NAME, REFERENCED_SCHEMA_NAME, REFERENCED_TABLE_NAME
```
其余目录查询固定为:
```sql
SELECT ROUTINE_NAME, ROUTINE_TYPE
FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = ?
ORDER BY ROUTINE_TYPE, ROUTINE_NAME
```
```sql
SELECT TRIGGER_NAME, EVENT_OBJECT_TABLE, ACTION_TIMING,
EVENT_MANIPULATION, ACTION_ORDER
FROM information_schema.TRIGGERS
WHERE TRIGGER_SCHEMA = ?
ORDER BY EVENT_OBJECT_TABLE, ACTION_TIMING, EVENT_MANIPULATION,
ACTION_ORDER, TRIGGER_NAME
```
```sql
SELECT EVENT_NAME
FROM information_schema.EVENTS
WHERE EVENT_SCHEMA = ?
ORDER BY EVENT_NAME
```
五次目录调用都使用 `connection.query({ sql, values: [database], timeout: limits.queryTimeoutMs })`。只接受 `BASE TABLE` 和 `VIEW`;例程类型只接受 `FUNCTION` 和 `PROCEDURE`;触发器时机、事件和顺序必须非空且可解析。表、视图、例程、触发器和事件的累计对象数超过 `maxObjects` 时抛出 `OBJECT_LIMIT_EXCEEDED`。
- [ ] **Step 4: 实现确定性视图拓扑排序**
使用 Kahn 算法,只把当前库中的视图依赖视为图边;基础表依赖不产生边。以下情况直接失败:
```text
VIEW_DEPENDENCY_CYCLE
CROSS_SCHEMA_VIEW_DEPENDENCY
UNKNOWN_VIEW_DEPENDENCY
UNKNOWN_TABLE_TYPE
OBJECT_LIMIT_EXCEEDED
```
同一层可创建视图按名称排序,保证结果确定性。
- [ ] **Step 5: 运行聚焦与完整导出器测试**
Run:
```bash
node --test --test-name-pattern="目录|视图" tools/database/mysql2-schema-exporter.test.mjs
node --test tools/database/mysql2-schema-exporter.test.mjs
```
Expected: 全部 PASS。
- [ ] **Step 6: 检测影响、提交并推送**
先调用 GitNexus MCP:
```text
detect_changes({ repo: "a-share-analysis", scope: "all" })
```
确认只影响导出器目录读取和视图排序后,再运行:
```bash
git add tools/database/mysql2-schema-exporter.mjs tools/database/mysql2-schema-exporter.test.mjs
git commit -m "feat(GOV-012): 实现 Schema 对象目录与视图排序"
git push origin main
```
---
### Task 3: 实现 SHOW CREATE、确定性 SQL 和并发变化检测
**Files:**
- Modify: `tools/database/mysql2-schema-exporter.mjs`
- Modify: `tools/database/mysql2-schema-exporter.test.mjs`
**Interfaces:**
- Consumes: Task 2 的 `SchemaCatalog` 和视图顺序。
- Produces: `readMysql57ServerMetadata(connection, database, limits)`、`readDefinitions(connection, catalog, limits)`、`normalizeCreateDefinition(definition)`、`renderSchemaSql(catalog, definitions)` 和 `exportMysql57Schema({ config, createConnection, now, limits })`,最终返回 `{ metadata, rawSql, objectCounts, durationMs }`。
- [ ] **Step 1: 写完整对象导出的失败测试**
建立按 SQL 文本返回固定行的假连接,覆盖这些列名:
```text
SHOW CREATE TABLE -> Create Table
SHOW CREATE VIEW -> Create View
SHOW CREATE PROCEDURE -> Create Procedure
SHOW CREATE FUNCTION -> Create Function
SHOW CREATE TRIGGER -> SQL Original Statement
SHOW CREATE EVENT -> Create Event
```
断言输出顺序为表、函数/过程、拓扑视图、触发器、事件;例程、触发器和事件使用:
```sql
DELIMITER ;;
CREATE PROCEDURE `refresh_market_snapshot`()
BEGIN
UPDATE `market_snapshot` SET `updated_at` = NOW();
END ;;
DELIMITER ;
```
测试还必须断言:
- `CREATE DEFINER=\`schema_user\`@\`192.168.1.10\`` 规范化后不再包含 `DEFINER`、用户名或主机。
- `CREATE TABLE ... ENGINE=InnoDB AUTO_INCREMENT=123 DEFAULT CHARSET=utf8mb4` 只移除表选项中的当前 `AUTO_INCREMENT=123`,列级 `AUTO_INCREMENT` 保留。
- 例程字符串字面量中的 `DEFINER=`、`AUTO_INCREMENT=123` 和合法 DML 原样保留。
- 连接选项不被写入错误信息,`connection.end()` 恰好调用一次。
- 单个定义超过 1 MiB 抛出 `DEFINITION_LIMIT_EXCEEDED`,总时长超过 30 秒抛出 `EXPORT_TIMEOUT`。
- [ ] **Step 2: 写对象变化和不完整结果的失败测试**
分别模拟:
- 第二次目录读取新增对象。
- 第二次 `SHOW CREATE` 返回不同定义。
- `SHOW CREATE` 返回空数组。
- 返回行缺少预期创建语句列。
- 查询抛出包含主机和密码的原始错误。
期望只抛出固定脱敏错误码:
```text
CATALOG_CHANGED_DURING_EXPORT
DEFINITION_CHANGED_DURING_EXPORT
SHOW_CREATE_EMPTY
SHOW_CREATE_SHAPE_INVALID
DATABASE_QUERY_FAILED
```
- [ ] **Step 3: 运行测试确认红灯**
```bash
node --test --test-name-pattern="SHOW CREATE|变化|关闭连接|脱敏" tools/database/mysql2-schema-exporter.test.mjs
```
Expected: FAIL,缺少导出主流程。
- [ ] **Step 4: 实现服务端元数据、SHOW CREATE 与两阶段复核**
连接后先执行固定参数化元数据查询:
```sql
SELECT VERSION() AS SERVER_VERSION,
DEFAULT_CHARACTER_SET_NAME AS DATABASE_CHARSET,
DEFAULT_COLLATION_NAME AS DATABASE_COLLATION
FROM information_schema.SCHEMATA
WHERE SCHEMA_NAME = ?
```
结果必须恰好一行,并交给 `assertMysql57ServerMetadata()`;零行或多行抛出 `DATABASE_METADATA_INVALID`。随后按对象类型构造以下唯一允许的 `SHOW CREATE` 语句,库名和对象名都必须经过 `quoteIdentifier()`:
```text
SHOW CREATE TABLE `<database>`.`<table>`
SHOW CREATE VIEW `<database>`.`<view>`
SHOW CREATE PROCEDURE `<database>`.`<procedure>`
SHOW CREATE FUNCTION `<database>`.`<function>`
SHOW CREATE TRIGGER `<database>`.`<trigger>`
SHOW CREATE EVENT `<database>`.`<event>`
```
每次查询使用 `connection.query({ sql, timeout: limits.queryTimeoutMs })`,禁止拼入配置值或任意调用方输入。`readDefinitions()` 只接受 Task 2 已验证目录中的名称,并按固定列名取创建语句。
`exportMysql57Schema()` 执行:
```js
const beforeCatalog = await readSchemaCatalog(connection, database, limits);
const beforeDefinitions = await readDefinitions(connection, beforeCatalog, limits);
const afterCatalog = await readSchemaCatalog(connection, database, limits);
const afterDefinitions = await readDefinitions(connection, afterCatalog, limits);
```
使用稳定 JSON 和 SHA-256 比较两个目录及定义集合。目录或定义不一致立即失败,不使用锁表。所有查询前检查总时长,所有查询设置 5 秒超时,最终在 `finally` 中 `await connection.end()`。mysql2 创建连接、目录查询或 `SHOW CREATE` 的原始错误统一转换为不带 `cause` 正文的 `SchemaExportError('DATABASE_QUERY_FAILED', '开发库 Schema 查询失败')`。
- [ ] **Step 5: 实现可恢复 SQL 渲染**
输出固定会话保护:
```sql
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET NAMES utf8mb4;
-- 按固定顺序输出对象
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
```
`normalizeCreateDefinition()` 按以下顺序处理每个创建语句:
```js
const normalized = String(definition)
.replace(/\r\n?/g, '\n')
.replace(/^(CREATE\s+(?:ALGORITHM=\S+\s+)?)DEFINER=`(?:``|[^`])+`@`(?:`(?:``|[^`])+`|'(?:''|[^'])+'|\S+)\s+/i, '$1')
.replace(/\sAUTO_INCREMENT=\d+(?=\s(?:DEFAULT\s+)?CHARSET=)/i, '')
.trim()
.replace(/;+\s*$/, '');
```
若规范化后仍在 SQL 字符串字面量之外发现 `DEFINER=`、主机信息或疑似凭据,则抛出 `UNSAFE_DEFINITION`。渲染器不全局格式化对象体;表和视图统一追加 `;`,过程、函数、触发器和事件统一包入 `DELIMITER ;;`。外键循环依赖由 `FOREIGN_KEY_CHECKS=0` 支持;跨 Schema 视图依赖仍失败。
- [ ] **Step 6: 运行全部导出器测试**
```bash
node --test tools/database/mysql2-schema-exporter.test.mjs
```
Expected: PASS,并确认测试不访问网络或真实数据库。
- [ ] **Step 7: 检测影响、提交并推送**
先调用 GitNexus MCP:
```text
detect_changes({ repo: "a-share-analysis", scope: "all" })
```
确认实际影响仍限于导出器执行流后,再运行:
```bash
git add tools/database/mysql2-schema-exporter.mjs tools/database/mysql2-schema-exporter.test.mjs
git commit -m "feat(GOV-012): 实现完整 SHOW CREATE 结构导出"
git push origin main
```
---
### Task 4: 集成 format v3 与异步 Schema 基线入口
**Files:**
- Modify: `tools/database/schema-baseline.mjs:18-690`
- Modify: `tools/database/schema-baseline.test.mjs:8-540`
- Modify: `package.json:22`
**Interfaces:**
- Consumes: `exportMysql57Schema()` 返回的 v3 metadata、rawSql 和 objectCounts。
- Produces: format v3 `captured`、`captured_empty`、`not_applicable`,保留 v1/v2 `verifyBaselineDirectory()`,并让 `persistBaselineArtifacts(targetDirectory, artifacts, fileOperations = DEFAULT_FILE_OPERATIONS)` 支持纯测试故障注入。
- [ ] **Step 1: 写 format v3 失败测试**
定义 v3 metadata:
```js
const validNodeMetadata = Object.freeze({
formatVersion: 3,
applicationVersion: 'v0.0.9',
gitCommit: '88e2fdffc8c71dd682405c148a81953a8dcee9d6',
exporterName: 'mysql2-show-create',
exporterVersion: '1.0.0',
driverName: 'mysql2',
driverVersion: '3.23.3',
serverVersion: '5.7.42-log',
databaseCharset: 'utf8mb4',
databaseCollation: 'utf8mb4_general_ci',
generatedAt: '2026-08-18T00:00:00.000Z',
});
```
断言 v3 manifest 包含:
```json
{
"format_version": 3,
"generator": "schema-baseline/3.0.0",
"exporter_name": "mysql2-show-create",
"exporter_version": "1.0.0",
"database_driver": "mysql2",
"database_driver_version": "3.23.3"
}
```
并断言不包含 `mysql_client_version` 与 `mysqldump_client_version`。为 v1/v2 固定夹具补测试,证明旧字段仍按旧规则验证。`not_applicable` format v3 固定写入 exporter/driver 元数据,服务端版本为 `not_enabled`,且不得调用 `exportSchema`。
- [ ] **Step 2: 写异步入口失败测试**
将启用路径改为注入 `exportSchema`:
```js
const result = await runBaselineGeneration({
repositoryRoot: root,
envFile,
applicationVersion: 'v0.0.9',
gitCommit: validNodeMetadata.gitCommit,
generatedAt: validNodeMetadata.generatedAt,
exportSchema: async ({ config }) => ({
metadata: {
...validNodeMetadata,
applicationVersion: undefined,
gitCommit: undefined,
generatedAt: undefined,
},
rawSql: '',
objectCounts: { tables: 0, views: 0, triggers: 0, routines: 0, events: 0 },
}),
});
assert.equal(result.status, 'captured_empty');
```
未启用路径断言 `exportSchema` 调用次数为 0;额外命令参数仍异步拒绝。再把现有 `mkdirSync`、`writeFileSync`、`renameSync` 和 `rmSync` 收敛为默认 `DEFAULT_FILE_OPERATIONS`,测试通过第三个可选参数注入一个在第三个文件写入时抛错的适配器,断言目标版本目录和 `.tmp-*` 目录都不存在。生产调用不传第三个参数,行为保持不变。
- [ ] **Step 3: 运行 Schema 测试确认红灯**
```bash
npm run test:schema-baseline
```
Expected: FAIL,旧实现仍依赖 mysql/mysqldump 和 format v2。
- [ ] **Step 4: 实现 format 分支与精确对象计数**
将当前版本常量升级:
```js
const BASELINE_FORMAT_VERSION = 3;
const PREVIOUS_BASELINE_FORMAT_VERSION = 2;
const LEGACY_BASELINE_FORMAT_VERSION = 1;
const TOOL_VERSION = '3.0.0';
const PREVIOUS_TOOL_VERSION = '2.0.0';
const LEGACY_TOOL_VERSION = '1.0.0';
```
`buildManifest()` 根据 metadata format 写 v3 或历史字段。`buildCapturedArtifacts(metadata, sql, objectCounts)` 在 v3 强制使用 exporter 提供的五类非负整数计数,并用既有 `countSchemaObjects(sql)` 交叉验证;任一类型不一致、SQL 为空但计数非零、SQL 非空但全部计数为零时拒绝产物。v1/v2 历史验证继续使用原规则。
- [ ] **Step 5: 替换子进程生成路径**
删除生产路径中的 `execFileSync('mysql')`、`execFileSync('mysqldump')`、客户端版本提取和参数构造调用。`runBaselineGeneration()` 改为:
```js
export async function runBaselineGeneration(options) {
// 固定配置和既有目录验证保持不变
if (!config.MYSQL_ENABLED) {
const artifacts = buildNotApplicableArtifacts({
...commonMetadata,
...MYSQL2_EXPORTER_METADATA,
formatVersion: 3,
});
persistBaselineArtifacts(targetDirectory, artifacts);
return verifyBaselineDirectory(targetDirectory, options.applicationVersion);
}
const exported = await (options.exportSchema ?? exportMysql57Schema)({ config });
const metadata = {
...commonMetadata,
...exported.metadata,
formatVersion: 3,
};
const artifacts = buildCapturedArtifacts(metadata, exported.rawSql, exported.objectCounts);
persistBaselineArtifacts(targetDirectory, artifacts);
return verifyBaselineDirectory(targetDirectory, options.applicationVersion);
}
```
`runCommandLine()` 改为 `async` 并在入口 `await runCommandLine()`;异常继续以非零退出,不输出配置和底层数据库错误正文。`generate` 和 `verify` 的 npm 命令及参数保持不变。
- [ ] **Step 6: 保留历史辅助 API 或明确删除引用**
`buildMysqldumpArguments()`、`extractClientVersion()` 和旧生成辅助只有在所有静态引用及测试删除后才能移除。`assertMysql57Metadata()` 保留为 v1/v2 历史清单校验,不得改成接受 v3 driver 字段。
- [ ] **Step 7: 更新 Schema 测试脚本并运行**
`package.json`:
```json
"test:schema-baseline": "node --test tools/database/schema-baseline.test.mjs tools/database/mysql2-schema-exporter.test.mjs"
```
Run:
```bash
npm run test:schema-baseline
npm run test:governance
npm run test:mysql-baseline
```
Expected: 全部 PASS;测试期间无真实数据库连接。
- [ ] **Step 8: 检测影响、提交并推送**
先调用 GitNexus MCP:
```text
detect_changes({ repo: "a-share-analysis", scope: "all" })
```
重点确认 `schema:baseline`、`schema:verify`、`governance:start` 和历史验证流程均在预期影响内,再运行:
```bash
git add package.json tools/database/schema-baseline.mjs tools/database/schema-baseline.test.mjs
git commit -m "feat(GOV-012): 升级 Schema 基线 format v3"
git push origin main
```
---
### Task 5: 更新现行规范并完成静态质量验证
**Files:**
- Modify: `docs/governance/code-development-standards.md:144-151`
- Modify: `docs/governance/development-workflow.md:100-107`
- Modify: `README.md:32-40`
- Modify: `docs/features/GOV-012-node-schema-exporter/design.md`
- Modify: `docs/features/GOV-012-node-schema-exporter/test-plan.md`
**Interfaces:**
- Consumes: 已通过测试的 format v3 行为。
- Produces: 不再要求本机 MySQL CLI 的现行规范,历史 GOV-007 文档保持不变。
- [ ] **Step 1: 更新代码开发规范**
明确:
```text
导出使用 Node.js mysql2 与 MySQL 5.7 SHOW CREATE;不要求本机安装 MySQL Server、mysql 或 mysqldump。
format v3 记录 exporter 和 driver 版本;format v1/v2 永久只读兼容。
导出前后复核目录和对象定义,变化时失败。
```
删除现行规范中“密码通过子进程环境”和“必须使用 5.7 mysqldump”的要求,历史功能档案不改写。
- [ ] **Step 2: 更新开发流程和 README**
说明 `schema:baseline` 只需要 Node 依赖和可达的 development MySQL 5.7.x;本机不安装数据库服务或 CLI。把 `captured_empty` 当前格式更新为 v3,并明确旧格式只读。
同时把 `design.md` 的现有安全和性能内容分别放入治理门禁要求的 `## 安全策略与评估`、`## 性能目标与评估` 二级标题;把 `test-plan.md` 的安全与性能合并章节拆为 `## 安全验证` 和 `## 性能验证`。更新实施迁移章节,明确先补建 `v0.0.9`、再发布 `v0.0.10`、最后封存 `v0.0.10`,不再描述覆盖既有功能档案的 `governance:start`。
- [ ] **Step 3: 运行静态验证**
```bash
pnpm exec prettier --check tools/database package.json docs/governance README.md docs/features/GOV-012-node-schema-exporter
pnpm lint
pnpm typecheck
npm run test:schema-baseline
npm run test:governance
```
Expected: 全部 PASS。
- [ ] **Step 4: 执行敏感信息和旧依赖扫描**
```bash
rg -n "executeFile\('mysql'|executeFile\('mysqldump'|execFileSync\('mysql'|execFileSync\('mysqldump'|buildMysqldumpArguments|buildMysqlMetadataArguments" tools/database/schema-baseline.mjs
rg -n "mysql 客户端必须|mysqldump 客户端必须|必须安装.*mysqldump" docs/governance README.md
rg -n "Root@|MYSQL_PASSWORD=.+|BEGIN .*PRIVATE KEY|112\.192\.20\.11" database/schema-baselines/v0.0.9 docs/features/GOV-012-node-schema-exporter
```
Expected: 生产生成路径和现行规范不再依赖 CLI;敏感值扫描无结果。测试中的历史错误文本可以保留时,必须只存在于明确的 v1/v2 兼容测试。
- [ ] **Step 5: 检测影响、提交并推送**
先调用 GitNexus MCP:
```text
detect_changes({ repo: "a-share-analysis", scope: "all" })
```
确认规范更新与实现行为一致后,再运行:
```bash
git add README.md docs/governance/code-development-standards.md docs/governance/development-workflow.md docs/features/GOV-012-node-schema-exporter/design.md docs/features/GOV-012-node-schema-exporter/test-plan.md
git commit -m "docs(GOV-012): 更新纯 Node Schema 基线规范"
git push origin main
```
---
### Task 6: 真实生成 v0.0.9 基线并恢复自动门禁
**Files:**
- Create: `database/schema-baselines/v0.0.9/manifest.yaml`
- Create conditionally: `database/schema-baselines/v0.0.9/schema.sql`
- Create conditionally: `database/schema-baselines/v0.0.9/checksum.sha256`
- Modify: `docs/features/GOV-012-node-schema-exporter/schema-baseline-waiver.md`
- Create: `docs/features/GOV-012-node-schema-exporter/qa/schema-baseline-result.md`
**Interfaces:**
- Consumes: 被 Git 忽略的 `deploy/env/development/api.env.local` 和固定开发库。
- Produces: 已发布 `v0.0.9` 对应、可验证、已推送的 format v3 Schema 基线。
- [ ] **Step 1: 执行真实基线生成**
```bash
npm run schema:baseline
npm run schema:verify
```
Expected: 不调用本机 MySQL CLI;真实库为空时只生成 `manifest.yaml` 且状态为 `captured_empty`,发现对象时生成完整三文件 `captured`。不得按预期强制为空。
- [ ] **Step 2: 检查产物边界**
```bash
find database/schema-baselines/v0.0.9 -maxdepth 1 -type f -print | sort
rg -n "INSERT INTO|REPLACE INTO|Root@|MYSQL_PASSWORD|DEFINER=|112\.192\.20\.11" database/schema-baselines/v0.0.9
```
Expected: 文件集合与状态匹配;敏感与数据语句扫描无结果。若为 captured,手工核对 manifest 对象计数与 SQL 对象数量相符。
- [ ] **Step 3: 记录真实验证但不记录秘密**
`qa/schema-baseline-result.md` 只记录:服务端主版本、字符规则、状态、对象计数、耗时、checksum 和命令结果;不记录密码、用户名、主机地址或 SQL 正文。把 waiver 状态更新为“已由真实 v0.0.9 基线补偿”,保留历史失败事实。
- [ ] **Step 4: 单独提交并推送基线**
先调用 GitNexus MCP:
```text
detect_changes({ repo: "a-share-analysis", scope: "all" })
```
确认只新增 `v0.0.9` 基线和本功能补偿证据后,再运行:
```bash
git add database/schema-baselines/v0.0.9 docs/features/GOV-012-node-schema-exporter/schema-baseline-waiver.md docs/features/GOV-012-node-schema-exporter/qa/schema-baseline-result.md
git commit -m "chore(GOV-012): 封存 v0.0.9 开发库 Schema"
git push origin main
git ls-remote origin refs/heads/main refs/tags/v0.0.9 refs/tags/v0.0.9^{}
```
Expected: 当前分支已推送;`v0.0.9` 仍指向原发布提交,基线 manifest 的 `git_commit` 与标签解引用提交一致。
- [ ] **Step 5: 验证自动门禁核心检查**
```bash
node -e "import('./tools/database/schema-baseline.mjs').then((m) => m.assertCurrentReleasedBaselinePublished(process.cwd()))"
```
Expected: 退出码 0。
严格门禁要求当前分支名在 `origin` 存在且 SHA 完全一致,因此不使用 detached worktree。改用一次性本地共享克隆,复用当前仓库 `node_modules`,同时把临时克隆的 `origin` 恢复为项目 Gitea:
```bash
repository_root="$(pwd -P)"
origin_url="$(git remote get-url origin)"
smoke_parent="$(mktemp -d "${TMPDIR:-/tmp}/a-share-schema-gate-smoke.XXXXXX")"
smoke_repository="$smoke_parent/repository"
cleanup_smoke() {
node -e "import('node:fs').then(({ rmSync }) => rmSync(process.argv[1], { recursive: true, force: true }))" "$smoke_parent"
}
trap cleanup_smoke EXIT
git clone --shared --branch main --single-branch "$repository_root" "$smoke_repository"
git -C "$smoke_repository" remote set-url origin "$origin_url"
ln -s "$repository_root/node_modules" "$smoke_repository/node_modules"
(
cd "$smoke_repository"
npm run governance:start -- GOV-900 schema-gate-smoke
test -s docs/features/GOV-900-schema-gate-smoke/gitnexus/before.md
test -s docs/features/GOV-900-schema-gate-smoke/gitnexus/impact-plan.md
)
```
Expected: 命令退出码 0;临时克隆只产生 GOV-900 文件并由 `trap` 清除,主工作区和 Gitea 不新增 GOV-900 文件、分支或提交。
---
### Task 7: 完整验证、Review 与开发收尾
**Files:**
- Create: `docs/features/GOV-012-node-schema-exporter/development-summary.md`
- Create: `docs/features/GOV-012-node-schema-exporter/qa/test-results.md`
- Create: `docs/features/GOV-012-node-schema-exporter/qa/visual-qa.md`
- Create: `docs/features/GOV-012-node-schema-exporter/review/code-review.md`
- Create: `docs/features/GOV-012-node-schema-exporter/review/standards-review.md`
- Create: `docs/features/GOV-012-node-schema-exporter/gitnexus/after.md`
- Create: `docs/features/GOV-012-node-schema-exporter/gitnexus/detected-changes.md`
- Create: `docs/features/GOV-012-node-schema-exporter/gitnexus/comparison.md`
- Create: `docs/features/GOV-012-node-schema-exporter/release-notes.md`
- Modify: `docs/features/GOV-012-node-schema-exporter/manifest.yaml`
**Interfaces:**
- Consumes: 全部实现提交和 v0.0.9 基线提交。
- Produces: 可发布 GOV-012 证据和双 Review 结论;此任务不创建发布标签。
- [ ] **Step 1: 运行完整验证**
```bash
pnpm verify
pnpm verify:source
pnpm audit --audit-level high
```
Expected: 所有适用测试、构建、契约、E2E 和源码部署验证通过。GOV-012 不修改 UI;视觉 QA 明确记录“不适用,已运行既有 E2E 防回归”,不得伪造新增页面截图。
- [ ] **Step 2: 执行 GitNexus 开发后分析**
```bash
npx gitnexus analyze
npx gitnexus status
```
随后调用 GitNexus MCP:
```text
detect_changes({ repo: "a-share-analysis", scope: "compare", base_ref: "3555b7c2224e22c64ca228dcc2d6a3a5dddfbfef" })
```
记录变更符号、受影响流程、风险、计划与实际差异。重点核对 Schema 生成、验证、发布核对和 governance:start 流程,HTTP/Web/Worker 等无关模块不应出现直接影响。
- [ ] **Step 3: 填写开发总结和 QA**
`development-summary.md` 必须包含原始需求、范围变化、实际完成、修复缺陷、延期项、关键提交和验证证据。`qa/test-results.md` 保存命令、通过数量、真实基线状态、安全扫描、性能耗时和残余风险。
- [ ] **Step 4: 完成代码 Review**
检查:
- 是否存在业务表数据查询或多语句。
- 是否所有连接路径关闭并脱敏错误。
- 是否六类对象完整且顺序可恢复。
- 是否定义前后复核能发现并发变化。
- 是否 v1/v2 历史验证完全保留。
- 是否 v3 对象计数、SQL 和 checksum 一致。
- 是否依赖版本、许可证和审计通过。
Critical/Important 问题必须修复、重测并记录复审提交;最后一行仅在通过时写 `最终结论:passed`。
- [ ] **Step 5: 完成开发规范 Review**
逐条核对安全、性能、注释、文件职责、依赖评估、中文提交、GitNexus、开发总结和 Gitea 推送。最后一行仅在通过时写 `最终结论:passed`。
- [ ] **Step 6: 执行完成门禁**
```bash
npm run governance:finish -- GOV-012 node-schema-exporter
```
Expected: 所有证据文件、Review、GitNexus 后报告和质量章节通过。
- [ ] **Step 7: 检测影响、提交并刷新 GitNexus**
先调用 GitNexus MCP:
```text
detect_changes({ repo: "a-share-analysis", scope: "all" })
```
把实际结果写入 `gitnexus/detected-changes.md` 和 `comparison.md`,确认无未处置的高风险变化后,再运行:
```bash
git add docs/features/GOV-012-node-schema-exporter AGENTS.md
git commit -m "docs(GOV-012): 完成纯 Node Schema 导出器审查"
git push origin main
npx gitnexus analyze
npx gitnexus status
```
若 GitNexus 自动更新 `AGENTS.md`,用独立中文提交推送统计变更。最后核对本地 HEAD、`origin/main` 和 GitNexus indexed commit 完全一致。
---
### Task 8: 发布 GOV-012 为 v0.0.10
**Files:**
- Modify: `VERSION`
- Modify: `CHANGELOG.md`
- Modify: `docs/index.md`
- Modify: `docs/features/GOV-012-node-schema-exporter/manifest.yaml`
- Modify: `docs/features/GOV-012-node-schema-exporter/development-summary.md`
- Modify: `docs/features/GOV-012-node-schema-exporter/qa/test-results.md`
- Modify: `docs/features/GOV-012-node-schema-exporter/release-notes.md`
**Interfaces:**
- Consumes: Task 7 已通过的完整验证、双 Review 和完成门禁。
- Produces: 发布提交、Gitea `main` 与带注释标签 `v0.0.10`。
- [ ] **Step 1: 更新版本和发布档案**
将 `VERSION` 更新为 `0.0.10`;在 `CHANGELOG.md` 新增 `0.0.10 - 2026-08-18`,只记录纯 Node Schema 导出器、format v3、历史兼容和门禁恢复。`docs/index.md` 把 GOV-012 标记为 `v0.0.10`。`manifest.yaml` 更新为:
```yaml
status: released
release: v0.0.10
```
并补齐 `test_plan`、`development_summary`、GitNexus、双 Review 和 release notes 路径。`release-notes.md` 此时写明“发布提交以 `git rev-parse 'v0.0.10^{}'` 的结果为准”,不写伪造 SHA;Task 8 Step 5 推送标签后再回填真实值。
- [ ] **Step 2: 执行发布前最终验证**
```bash
pnpm verify:release
npm run governance:finish -- GOV-012 node-schema-exporter
git diff --check
git status --short
```
Expected: 全部门禁退出码 0;工作区只包含版本与发布档案变化,不包含秘密、临时目录或计划外业务代码。
- [ ] **Step 3: 刷新 GitNexus 并执行发布前变更检测**
```bash
npx gitnexus analyze
npx gitnexus status
```
随后调用:
```text
detect_changes({ repo: "a-share-analysis", scope: "all" })
```
Expected: GitNexus 只报告版本、发布档案和可能的 `AGENTS.md` 索引统计变化;若出现代码符号变化,停止发布并核对未提交文件。
- [ ] **Step 4: 创建并推送发布提交**
```bash
git add VERSION CHANGELOG.md docs/index.md docs/features/GOV-012-node-schema-exporter AGENTS.md
git commit -m "chore(GOV-012): 发布 v0.0.10 纯 Node Schema 导出器"
git push origin main
local_head="$(git rev-parse HEAD)"
remote_head="$(git ls-remote origin refs/heads/main | awk '{print $1}')"
test "$local_head" = "$remote_head"
```
Expected: 本地 HEAD 和 Gitea `main` 完全一致。
- [ ] **Step 5: 创建、精确推送并核对标签**
```bash
git tag -a v0.0.10 -m "发布 v0.0.10:纯 Node.js 开发库 Schema 导出器"
git push origin refs/tags/v0.0.10
release_commit="$(git rev-parse 'v0.0.10^{}')"
remote_release_commit="$(git ls-remote origin 'refs/tags/v0.0.10^{}' | awk '{print $1}')"
test "$release_commit" = "$remote_release_commit"
```
Expected: 标签解引用后指向 Step 4 的发布提交;不使用 `--tags`、`--force` 或移动既有标签。
- [ ] **Step 6: 回填真实发布引用**
把 `v0.0.10^{}` 的真实提交、远程分支核对和标签推送结果写入 `release-notes.md`、`development-summary.md` 和 `qa/test-results.md`。先调用:
```text
detect_changes({ repo: "a-share-analysis", scope: "all" })
```
再运行:
```bash
git add docs/features/GOV-012-node-schema-exporter/release-notes.md docs/features/GOV-012-node-schema-exporter/development-summary.md docs/features/GOV-012-node-schema-exporter/qa/test-results.md
git commit -m "docs(GOV-012): 回填 v0.0.10 发布引用"
git push origin main
```
Expected: 标签保持指向发布封存提交;后续文档提交只补追溯事实,不移动标签。
---
### Task 9: 封存 v0.0.10 Schema 并恢复下一功能门禁
**Files:**
- Create: `database/schema-baselines/v0.0.10/manifest.yaml`
- Create conditionally: `database/schema-baselines/v0.0.10/schema.sql`
- Create conditionally: `database/schema-baselines/v0.0.10/checksum.sha256`
- Create: `docs/features/GOV-012-node-schema-exporter/qa/post-release-schema-result.md`
- Modify conditionally: `AGENTS.md`
**Interfaces:**
- Consumes: 已推送的 `v0.0.10` 标签和固定 development 数据库。
- Produces: 已跟踪、已推送且与 `v0.0.10^{}` 一致的下一版本开发前 Schema 基线。
- [ ] **Step 1: 生成并验证当前发布版本基线**
```bash
npm run schema:baseline
npm run schema:verify
find database/schema-baselines/v0.0.10 -maxdepth 1 -type f -print | sort
rg -n "INSERT INTO|REPLACE INTO|Root@|MYSQL_PASSWORD|DEFINER=|112\.192\.20\.11" database/schema-baselines/v0.0.10
```
Expected: 状态由真实开发库决定为 `captured_empty` 或 `captured`;manifest 的 `application_version` 为 `v0.0.10`,`git_commit` 等于 `git rev-parse 'v0.0.10^{}'`,敏感与顶层数据语句扫描无结果。
- [ ] **Step 2: 记录发布后基线结果**
`qa/post-release-schema-result.md` 只记录版本、服务端主版本、字符规则、状态、对象计数、耗时、checksum、标签提交和命令结果;不记录主机、用户名、密码或 SQL 正文。
- [ ] **Step 3: 检测、提交并推送基线**
先调用:
```text
detect_changes({ repo: "a-share-analysis", scope: "all" })
```
确认只新增 `v0.0.10` 基线与 QA 证据后,再运行:
```bash
git add database/schema-baselines/v0.0.10 docs/features/GOV-012-node-schema-exporter/qa/post-release-schema-result.md
git commit -m "chore(GOV-012): 封存 v0.0.10 开发库 Schema"
git push origin main
node -e "import('./tools/database/schema-baseline.mjs').then((m) => m.assertCurrentReleasedBaselinePublished(process.cwd()))"
```
Expected: 提交和推送成功,严格发布基线检查退出码 0。
- [ ] **Step 4: 最终刷新 GitNexus**
```bash
npx gitnexus analyze
npx gitnexus status
```
若 `AGENTS.md` 仅发生 GitNexus 统计更新,先调用 `detect_changes({ repo: "a-share-analysis", scope: "all" })`,再用 `docs(GOV-012): 更新发布后 GitNexus 基线` 独立提交并推送。最终要求本地 HEAD、Gitea `main` 和 GitNexus indexed commit 一致;完成后 FEAT-MO-001 才可进入业务代码实现。
---
## 计划自审
- 规格覆盖:PRD、工程需求、设计和测试计划中的纯 Node、完整对象、format v3、历史兼容、安全、性能、并发变化、真实开发库和门禁恢复均有对应任务。
- 类型一致性:跨任务接口固定为 `SchemaCatalog`、`readSchemaCatalog()`、`readDefinitions()`、`exportMysql57Schema()` 和 format v3 metadata,没有名称漂移。
- 影响边界:运行时代码只修改 Schema 治理工具;HTTP/OpenAPI、Web API、Redis、RocketMQ、API、Scheduler 和 Worker 不修改,但完整回归仍验证其未受影响。
- 占位符检查:所有实现、测试、工具调用和发布步骤均给出明确接口、命令与预期结果,MCP 调用与 shell 命令已分开表达。
- 发布闭环:先补建 `v0.0.9`,再发布 `v0.0.10`,最后封存 `v0.0.10`,下一功能不会再次卡在缺失上一发布版本 Schema。
- 回退边界:代码、依赖和文档可用中文回退提交恢复;已发布 v1/v2/v3 基线和已推送标签不得删除、覆盖或改写。
## 计划阶段验证记录
- `pnpm exec prettier --check`:实施计划、设计和测试计划格式通过。
- `npm run test:governance`:10 项治理测试全部通过。
- `npx gitnexus analyze`:成功索引 2,185 nodes、2,863 edges、49 clusters、50 flows。
- GitNexus MCP `detect_changes` 在重索引前后两次返回 `Transport closed`;按治理降级规则使用 `git status`、未跟踪文件清单和 `git diff --no-index --stat` 交叉验证,实际范围只有本实施计划文档,不涉及代码符号和执行流。