// 模块之间的依赖规则,来源是 docs/结构.md 第二部分(模块表)和第六部分(检查规则)。 // 模块按 src/<模块名>/ 认;新建模块目录后自动受这些规则检查。规则只管 src 下的模块,tests/ 不受"只有某模块能用某外部包"的限制。 // 模块表"依赖"一列:每个模块除了自己和 vocab 以外,还能引用哪些模块。plugin 装配时可用各模块。 const ALLOWED = { plugin: [ "flow", "export", "config", "materials", "submissions", "ledger", "agents", "runner", "tasks", "workspace", "jev", "rules", ], flow: [ "ledger", "rules", "materials", "submissions", "agents", "runner", "tasks", "workspace", "config", "jev", ], export: ["ledger", "rules", "materials"], config: ["rules"], materials: ["ledger", "rules"], submissions: ["jev"], ledger: [], agents: [], runner: [], tasks: [], workspace: [], jev: [], rules: [], vocab: [], }; const MODULES = Object.keys(ALLOWED); const anyOf = (names) => `(${names.join("|")})`; // 外部的东西各由哪些名字认出来。核心模块带不带 node: 前缀都认; // npm 包装好后解析成 node_modules/ 下的路径,没装时是包名本身,两种都认。 const npm = (names) => `(^|node_modules/)${anyOf(names)}(/|$)`; const SUBPROCESS = [ "^(node:)?(child_process|cluster)$", npm(["cross-spawn", "execa"]), ]; const SQLITE = ["^node:sqlite$", npm(["better-sqlite3", "sqlite3"])]; const PI = npm(["@earendil-works/[^/]+"]); const directionRules = MODULES.map((name) => ({ name: `direction-${name}`, comment: `${name} 只能引用自己、vocab 和模块表里列出的模块:${ALLOWED[name].join("、") || "无"}`, severity: "error", from: { path: `^src/${name}/` }, to: { path: "^src/", pathNot: `^src/${anyOf([name, "vocab", ...ALLOWED[name]])}/` }, })); module.exports = { forbidden: [ { name: "no-circular", comment: "不许循环引用", severity: "error", from: {}, to: { circular: true }, }, { name: "unknown-module", comment: `src 下只有模块表里的模块目录:${MODULES.join("、")}`, severity: "error", from: { path: "^src/", pathNot: `^src/${anyOf(MODULES)}/` }, to: {}, }, ...directionRules, { name: "through-index", comment: "别的模块只能通过目录下的 index.ts 引用它", severity: "error", from: { path: "^src/([^/]+)/" }, to: { path: "^src/[^/]+/", pathNot: ["^src/$1/", "^src/[^/]+/index\\.ts$"] }, }, { name: "outside-src", comment: "src 下的文件只许引用 src/、node_modules/ 与 Node 内置模块,不能借 src 外的文件绕开规则", severity: "error", from: { path: "^src/" }, to: { pathNot: ["^src/", "^node_modules/"], dependencyTypesNot: ["core"] }, }, { name: "pure-allowlist", comment: "rules 和 vocab 不碰文件、数据库、网络、子进程和 Pi:除了 src 下允许的模块," + "只许引用 typebox 和 node:crypto", severity: "error", from: { path: "^src/(rules|vocab)/" }, to: { pathNot: ["^src/", "^node_modules/typebox/", "^(node:)?crypto$"] }, }, { name: "sqlite-only-ledger", comment: "只有 ledger 能用 SQLite", severity: "error", from: { path: "^src/", pathNot: "^src/ledger/" }, to: { path: SQLITE }, }, { name: "pi-only-agents-plugin", comment: "只有 agents 和 plugin 能用 Pi", severity: "error", from: { path: "^src/", pathNot: "^src/(agents|plugin)/" }, to: { path: PI }, }, { name: "subprocess-only-runner-workspace", comment: "只有 runner 和 workspace 能起子进程", severity: "error", from: { path: "^src/", pathNot: "^src/(runner|workspace)/" }, to: { path: SUBPROCESS }, }, { name: "tasks-not-runner", comment: "tasks 不许引用 runner:runner 只管怎么跑,tasks 只管怎么判,由 flow 串起来", severity: "error", from: { path: "^src/tasks/" }, to: { path: "^src/runner/" }, }, ], options: { doNotFollow: { path: "node_modules" }, tsPreCompilationDeps: true, tsConfig: { fileName: "tsconfig.json" }, // 按 Node 的 ESM 规则读 npm 包的 exports(dependency-cruiser 默认不读), // 否则 typebox 这类只在 exports 里给入口的包解析不到。 enhancedResolveOptions: { extensions: [".ts", ".js"], exportsFields: ["exports"], conditionNames: ["import", "node", "default"], }, }, };