feat(a0): 完成工程骨架与契约基线
ci / 质量门禁 (push) Canceled after 0s

- 建立 pnpm workspace + Turborepo 单仓、ESLint 9/Prettier/commitlint/husky 门禁
- NestJS + Fastify API:统一错误体、trace_id、Zod 管道、/health 真实依赖探活、OpenAPI 3.1 契约
- Prisma 基线:outbox_events/idempotency_keys/system_configs 与首版迁移、幂等种子
- shared 契约包:错误码、权限点、ULID、游标分页、命名/时间工具
- ui 包与 Web 壳层:Semi + Remix 图标统一出口、Cloud Blue 视觉、登录/工作台/错误页
- worker 骨架、Playwright 冒烟、Gitea CI、OpenAPI 快照与 oasdiff 回退脚本
- 补充 A0 实施报告与本地开发说明
This commit is contained in:
Stev_Wang committed 2026-11-02 17:01:50 +08:00
1 parent 9c3ab9768a
commit 04f49690f4
155 files changed
+18402 -28

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env node
/**
* OpenAPI 兼容性检查(A0 回退实现)。
*
* 优先使用 oasdiff 二进制;未安装时使用本脚本的结构化回退检查:
* - 删除 path / operation
* - 删除 components.schemas
* - 请求体必填字段新增、字段类型变更
*
* 基线来自 git HEAD 中的快照;首次提交(快照尚未入库)时跳过。
*/
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const SNAPSHOT = resolve(process.cwd(), 'openapi/lingxi.openapi.json');
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
if (!existsSync(SNAPSHOT)) {
console.error(`未找到 OpenAPI 快照:${SNAPSHOT},请先执行 pnpm openapi:export`);
process.exit(1);
}
const current = JSON.parse(readFileSync(SNAPSHOT, 'utf8'));
let baseline = null;
try {
const raw = execFileSync('git', ['show', `HEAD:${'openapi/lingxi.openapi.json'}`], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
});
baseline = JSON.parse(raw);
} catch {
console.log('基线快照尚未入库,跳过 OpenAPI diff(首次提交)。');
process.exit(0);
}
const breaking = [];
for (const [path, baselinePath] of Object.entries(baseline.paths ?? {})) {
const currentPath = current.paths?.[path];
if (!currentPath) {
breaking.push(`删除路径:${path}`);
continue;
}
for (const method of HTTP_METHODS) {
if (baselinePath[method] !== undefined && currentPath[method] === undefined) {
breaking.push(`删除操作:${method.toUpperCase()} ${path}`);
}
}
}
for (const schemaName of Object.keys(baseline.components?.schemas ?? {})) {
if (current.components?.schemas?.[schemaName] === undefined) {
breaking.push(`删除 schema:${schemaName}`);
}
}
const baselineSchemas = baseline.components?.schemas ?? {};
const currentSchemas = current.components?.schemas ?? {};
for (const [name, baselineSchema] of Object.entries(baselineSchemas)) {
const currentSchema = currentSchemas[name];
if (currentSchema === undefined) {
continue;
}
const baselineRequired = new Set(baselineSchema.required ?? []);
for (const required of currentSchema.required ?? []) {
if (!baselineRequired.has(required)) {
breaking.push(`schema ${name} 新增必填字段:${required}`);
}
}
for (const [property, baselineProperty] of Object.entries(baselineSchema.properties ?? {})) {
const currentProperty = currentSchema.properties?.[property];
if (
currentProperty !== undefined &&
baselineProperty.type !== undefined &&
currentProperty.type !== undefined &&
baselineProperty.type !== currentProperty.type
) {
breaking.push(
`schema ${name}.${property} 类型变更:${baselineProperty.type} -> ${currentProperty.type}`,
);
}
}
}
if (breaking.length > 0) {
console.error('检测到破坏性 OpenAPI 变更:');
for (const item of breaking) {
console.error(` - ${item}`);
}
process.exit(1);
}
console.log('OpenAPI 无破坏性变更。');