This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-MVP/tools/api-report.mjs
T
ChaiHaoandClaude Opus 5 5826b6f976 流程链按拓扑序读 —— 图纸坐标不配决定推进次序
任务详情里的「流程链」在 MVP 包上读作 **A1 → A7 → A2 → A6 → A3 → A5 → A4**。
不是随机:`toChain` 按 `layout.col` 排,而 MVP 图是**回形走线**(第 0 行左→右、
第 1 行右→左、第 2 行折回最左),A7 的列位(1)正好跟 A1 并列、A6(3)跟 A2 并列。
原注释还写着「列位是流程的推进次序(拓扑的可视化)」—— 那句话只对 v1 那种一行
从左画到右的包成立,换一张图就是错的。图纸声明为「不影响语义」,那它也不配决定次序。

改为按**边**做拓扑序,新增 `advanceOrder`(process 的 domain,纯函数零 IO):

  · 回边(reopen)排除在外 —— 它是重新入场不是往前推进;留着 G4 → A4 就成环,
    A4 会等自己的下游,拓扑序无从下手
  · 真并行支(G2 分出的 A2 / A3)图上本无先后,按**节点编号**定 ——
    同一个包在谁手上都读作同一条链
  · 进不了拓扑序的节点(不可达、或排除回边后仍有环)按编号缀在末尾:少排序,不丢节点

实测链已读作 A1 → A2 → A3 → A4 → A5 → A6 → A7(A1 current)。
六条 domain 测试盯住它,第一条就是那张回形图。

顺带修了两处会互相传染的闸:
  · `api-report.mjs` 在 tsc 失败那条路径不清 `.api-tmp`,半成品留在盘上,
    eslint 扫到生成的 .d.ts 报 1004 条假错 —— 于是 tsc 一红 lint 跟着红。实测踩到。
  · eslint 只忽略了 `api-report/**`,把同为产物的 `.api-tmp/**` 一并忽略

契约变更(加法,非破坏):①depcruise 反查六处下游无违规 ②CONTRACT.md 已注明
③快照 api-report/…/process/index.d.ts 仅多一个导出名。

typecheck ✓ lint ✓ depcruise 374 模块 ✓ 487 测试 ✓(以退出码验,不以 echo 验)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-28 10:45:41 +08:00

114 lines
3.7 KiB
JavaScript

#!/usr/bin/env node
/**
* 契约快照门禁 —— 编码规范 §11.5。
*
* tsc --emitDeclarationOnly 出全量 .d.ts,只留**契约面**落 api-report/:
* - packages/contracts/** (前后端唯一共享点,§8.1)
* - packages/<pkg>/src/index.d.ts
* - apps/api/src/modules/<m>/{index,contract,port}.d.ts
* 然后与 git 比对:改 internal/ 时 api-report/ 纹丝不动;碰了公开契约,diff 立刻可见。
*/
import { execFileSync } from 'node:child_process';
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
import { dirname, join, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = dirname(dirname(fileURLToPath(import.meta.url)));
const tmp = join(root, '.api-tmp');
const out = join(root, 'api-report');
function run(cmd, args, opts = {}) {
return execFileSync(cmd, args, { cwd: root, encoding: 'utf8', stdio: 'pipe', ...opts });
}
rmSync(tmp, { recursive: true, force: true });
try {
run('node', [
join(root, 'node_modules', 'typescript', 'bin', 'tsc'),
'-p',
'tsconfig.node.json',
'--emitDeclarationOnly',
'--declaration',
'--noEmit',
'false',
'--outDir',
'.api-tmp',
]);
} catch (err) {
process.stderr.write((err.stdout ?? '') + (err.stderr ?? ''));
process.stderr.write('\napi:check —— 声明生成失败,先修 tsc。\n');
/*
* 失败也要清干净。半成品留在盘上会**污染下一道闸**:`.api-tmp` 里是生成的 .d.ts,
* eslint 扫到它会报上千条 `consistent-type-imports` —— 于是 tsc 一红,lint 跟着红,
* 人以为自己又写坏了别的地方。实测踩过一次。
*/
rmSync(tmp, { recursive: true, force: true });
process.exit(1);
}
/** 契约面判据:相对 .api-tmp 的路径 */
function isContractFace(rel) {
const p = rel.split(sep).join('/');
if (p.startsWith('packages/contracts/')) return true;
if (/^packages\/[^/]+\/src\/index\.d\.ts$/.test(p)) return true;
if (/^apps\/api\/src\/modules\/[^/]+\/(index|contract|port)\.d\.ts$/.test(p)) return true;
return false;
}
function walk(dir, acc = []) {
if (!existsSync(dir)) return acc;
for (const name of readdirSync(dir)) {
const full = join(dir, name);
if (statSync(full).isDirectory()) walk(full, acc);
else if (full.endsWith('.d.ts')) acc.push(full);
}
return acc;
}
rmSync(out, { recursive: true, force: true });
mkdirSync(out, { recursive: true });
let kept = 0;
for (const file of walk(tmp)) {
const rel = relative(tmp, file);
if (!isContractFace(rel)) continue;
const dest = join(out, rel);
mkdirSync(dirname(dest), { recursive: true });
cpSync(file, dest);
kept += 1;
}
rmSync(tmp, { recursive: true, force: true });
process.stdout.write(`api:check —— 契约面 ${kept} 个声明文件 → api-report/\n`);
if (process.argv.includes('--update')) {
process.stdout.write('api:check —— --update:已刷新快照,请连同变更申请一并提交。\n');
process.exit(0);
}
let status = '';
try {
status = run('git', ['status', '--porcelain', '--', 'api-report']);
} catch {
process.stdout.write('api:check —— 非 git 仓库,跳过比对。\n');
process.exit(0);
}
if (status.trim() === '') {
process.stdout.write('api:check —— 公开契约无变化 ✔\n');
process.exit(0);
}
process.stderr.write('\napi:check —— 公开契约发生变化(§12.4):\n' + status + '\n');
try {
process.stderr.write(run('git', ['diff', '--', 'api-report']));
} catch {
/* 首次建立基线时无 diff 可打 */
}
process.stderr.write(
'\n契约变更须:①depcruise 反查下游 ②同步 CONTRACT.md ③本快照单独说明 ④破坏性走三步弃用。\n' +
'确认无误后:pnpm api:check --update && git add api-report\n',
);
process.exit(1);