Schema Versioning + Gray Release Implementation Plan
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: 对比两份 FormSnapshot,给出 SQL 变更、字段引用影响和破坏性等级;设计器提供版本面板,存储走 HostAdapter。
Architecture: 影响分析是纯函数,输入 old/new snapshot,输出 SchemaImpact。SQL 部分直接调用已有 diffSqlAlter。字段引用扫描 rule 树里的公式、control、event、validate。UI 是左侧新入口 VersionPanel.vue,默认 config.showVersion === true 且存在 adapter.listVersions 时才显示。
Tech Stack: 现有 sql.js、FormSnapshot、Element Plus Dialog。
Depends on: 2026-08-23-00-shared-foundation.md
File map
- Create:
designer3.0/src/modules/schema/collectRefs.js - Create:
designer3.0/src/modules/schema/impact.js - Create:
designer3.0/tests/modules/schema/impact.spec.js - Create:
designer3.0/src/designer/schema/VersionPanel.vue - Modify:
designer3.0/src/designer/FcDesigner.vue— 左侧入口、getSchemaImpact、save 时带 snapshot - Modify:
designer3.0/src/locale/zh-cn.js/en.js/jp.js - Modify:
designer3.0/types/index.d.ts - Create:
fc-design-pro/src/help/feature/schema-version.md
Task 1: 扫描字段引用
Files:
Create:
designer3.0/src/modules/schema/collectRefs.jsCreate:
designer3.0/tests/modules/schema/impact.spec.js[ ] Step 1: 写失败测试
import { describe, it, expect } from 'vitest';
import { collectFieldRefs } from '../../../src/modules/schema/collectRefs.js';
describe('collectFieldRefs', () => {
it('finds formula and control references', () => {
const refs = collectFieldRefs([
{
type: 'input',
field: 'total',
_fc_id: 'id_total',
computed: 'SUM(qty, price)',
},
{
type: 'input',
field: 'qty',
_fc_id: 'id_qty',
control: [{ handle: 'hidden', condition: '$price > 0' }],
},
]);
const qty = refs.get('qty');
expect(qty.some((r) => r.kind === 'formula' && r.field === 'total')).toBe(true);
expect(qty.some((r) => r.kind === 'control' && r.field === 'qty')).toBe(true);
});
});公式字段名以设计器真实存储为准:打开任意带计算公式的 rule,确认是 computed 还是 value 里的 。实现时按真实字段扫描,测试跟着改,禁止猜错 key 还硬写。
- [ ] Step 2: 实现 collectFieldRefs
递归 rule 与 children。对每个节点:
把
field登记为定义。用正则
/\b([A-Za-z_][A-Za-z0-9_]*)\b/g扫computed、control的 condition 字符串、on事件函数字符串。忽略 JS 关键字(
iftruefalsereturn)。返回
Map<field, Array<{kind, field, fcId}>>,key 是被引用的字段。[ ] Step 3: 跑测试
cd /Users/xaboy/Documents/git/form-create-package/designer3.0
pnpm test tests/modules/schema/impact.spec.jsExpected: PASS
- [ ] Step 4: Commit
git add src/modules/schema/collectRefs.js tests/modules/schema/impact.spec.js
git commit -m "feat: collect field references from form rules"Task 2: SchemaImpact
Files:
Create:
designer3.0/src/modules/schema/impact.jsModify:
designer3.0/tests/modules/schema/impact.spec.js[ ] Step 1: 写失败测试
import { buildSchemaImpact } from '../../../src/modules/schema/impact.js';
it('marks dropped referenced field as breaking', () => {
const oldSnap = {
format: '1.0.0',
formId: 'f1',
name: 't',
rule: [
{ type: 'input', field: 'price', title: '单价', _fc_id: 'a', _fc_drag_tag: 'input' },
{ type: 'input', field: 'qty', title: '数量', _fc_id: 'b', _fc_drag_tag: 'input', computed: 'price' },
],
options: { formName: 't' },
schema: [],
createdAt: '',
};
const newSnap = {
...oldSnap,
rule: [{ type: 'input', field: 'qty', title: '数量', _fc_id: 'b', _fc_drag_tag: 'input', computed: 'price' }],
};
const impact = buildSchemaImpact(oldSnap, newSnap, { input: { sqlType: 'string' } });
expect(impact.level).toBe('breaking');
expect(impact.removedFields).toContain('price');
expect(impact.refs.some((r) => r.field === 'price')).toBe(true);
});
it('marks add-only as safe', () => {
const oldSnap = { format: '1.0.0', formId: 'f1', name: 't', rule: [], options: { formName: 't' }, schema: [], createdAt: '' };
const newSnap = {
...oldSnap,
rule: [{ type: 'input', field: 'name', title: '姓名', _fc_id: 'n', _fc_drag_tag: 'input' }],
};
const impact = buildSchemaImpact(oldSnap, newSnap, { input: { sqlType: 'string' } });
expect(impact.level).toBe('safe');
});- [ ] Step 2: 实现 buildSchemaImpact
import { sqlTemplate, diffSqlAlter } from '../../utils/sql.js';
import { collectFieldRefs } from './collectRefs.js';
export function buildSchemaImpact(oldSnap, newSnap, dragRuleList) {
const oldSql = sqlTemplate(oldSnap.rule, dragRuleList, oldSnap.options);
const newSql = sqlTemplate(newSnap.rule, dragRuleList, newSnap.options);
const sql = diffSqlAlter(oldSql, newSql);
const oldFields = new Set(listFields(oldSnap.rule));
const newFields = new Set(listFields(newSnap.rule));
const removedFields = [...oldFields].filter((f) => !newFields.has(f));
const addedFields = [...newFields].filter((f) => !oldFields.has(f));
const refs = collectFieldRefs(newSnap.rule);
const broken = [];
removedFields.forEach((field) => {
(refs.get(field) || []).forEach((r) => broken.push({ ...r, field }));
});
const hasDrop = sql.some((s) => s.action === 'dropColumn' || s.action === 'dropTable');
const level = broken.length || hasDrop ? 'breaking' : addedFields.length || sql.length ? 'warn' : 'safe';
return { level, sql, addedFields, removedFields, refs: broken };
}
function listFields(rules, acc = []) {
(rules || []).forEach((r) => {
if (r.field) acc.push(r.field);
listFields(r.children, acc);
});
return acc;
}- [ ] Step 3: 跑测试并提交
pnpm test tests/modules/schema/impact.spec.jsExpected: PASS
git add src/modules/schema/impact.js tests/modules/schema/impact.spec.js
git commit -m "feat: compute schema impact between snapshots"Task 3: getSchemaImpact + 版本面板
Files:
Create:
designer3.0/src/designer/schema/VersionPanel.vueModify:
designer3.0/src/designer/FcDesigner.vueModify:
designer3.0/src/locale/zh-cn.js(以及 en/jp 同步 key)Modify:
designer3.0/types/index.d.ts[ ] Step 1: 暴露 API
getSchemaImpact(oldSnapshot) {
return buildSchemaImpact(oldSnapshot, methods.getSnapshot(), /* 与 getSQL 相同的 dragRuleList */);
}types:
getSchemaImpact: (oldSnapshot: FormSnapshot) => SchemaImpact;export type SchemaImpactLevel = 'safe' | 'warn' | 'breaking';
export interface SchemaImpact {
level: SchemaImpactLevel;
sql: SqlAlter[];
addedFields: string[];
removedFields: string[];
refs: Array<{ kind: string; field: string; fcId?: string }>;
}- [ ] Step 2: VersionPanel UI
面板职责只有三块:
- 调用
adapter.listVersions(formId)列出版本。 - 选中一项后调用
getSchemaImpact(item.snapshot)展示 level / 删除字段 / SQL。 - 「回滚预览」执行
setRule(item.snapshot.rule)+setOptions(item.snapshot.options);「发布」调用adapter.publishVersion。
无 adapter 时展示空状态文案 t('schema.needAdapter'),不要报错。
左侧入口与「模块管理」并列,getConfig('showVersion') === true 且(开发模式或存在 adapter)时渲染。默认 showVersion 为 false,避免老客户界面突变。
- [ ] Step 3: save 事件带上 snapshot
现有 @save(约 1868 行)扩展为:
vm.emit('save', {
rule: methods.getJson(),
options: methods.getOptionsJson(),
snapshot: methods.getSnapshot(),
});旧宿主只读前两个字段,保持兼容。
- [ ] Step 4: 文档
fc-design-pro/src/help/feature/schema-version.md 写:版本是什么、如何接 adapter、灰度由宿主读 published 决定。侧边栏挂到 help/feature 现有导航。
- [ ] Step 5: 手动验收
cd /Users/xaboy/Documents/git/form-create-package/designer3.0
pnpm run dev在 examples 里 config.adapter = createMemoryAdapter(),showVersion: true。保存两次,改掉一个被公式引用的字段,影响等级必须是 breaking。
- [ ] Step 6: Commit
git add src/designer/schema src/designer/FcDesigner.vue src/locale types/index.d.ts
git commit -m "feat: add schema version panel and impact API"

