@yss123/cage-cli 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (499) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -2
  3. package/bin/cage.js +6 -0
  4. package/bin/create-cage-app.js +6 -0
  5. package/dist/analyze.d.ts +18 -0
  6. package/dist/analyze.js +28 -0
  7. package/dist/analyze.js.map +1 -0
  8. package/dist/baseline.d.ts +5 -0
  9. package/dist/baseline.js +50 -0
  10. package/dist/baseline.js.map +1 -0
  11. package/dist/budget.d.ts +7 -0
  12. package/dist/budget.js +52 -0
  13. package/dist/budget.js.map +1 -0
  14. package/dist/config.d.ts +30 -0
  15. package/dist/config.js +46 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/context.d.ts +72 -0
  18. package/dist/context.js +202 -0
  19. package/dist/context.js.map +1 -0
  20. package/dist/create-prompt.d.ts +5 -0
  21. package/dist/create-prompt.js +50 -0
  22. package/dist/create-prompt.js.map +1 -0
  23. package/dist/create.d.ts +18 -0
  24. package/dist/create.js +64 -0
  25. package/dist/create.js.map +1 -0
  26. package/dist/derive.d.ts +14 -0
  27. package/dist/derive.js +163 -0
  28. package/dist/derive.js.map +1 -0
  29. package/dist/dev.d.ts +2 -0
  30. package/dist/dev.js +116 -0
  31. package/dist/dev.js.map +1 -0
  32. package/dist/env.d.ts +1 -0
  33. package/dist/env.js +15 -0
  34. package/dist/env.js.map +1 -0
  35. package/dist/findings.d.ts +11 -0
  36. package/dist/findings.js +13 -0
  37. package/dist/findings.js.map +1 -0
  38. package/dist/gen/contracts.d.ts +6 -0
  39. package/dist/gen/contracts.js +188 -0
  40. package/dist/gen/contracts.js.map +1 -0
  41. package/dist/gen/index.d.ts +19 -0
  42. package/dist/gen/index.js +177 -0
  43. package/dist/gen/index.js.map +1 -0
  44. package/dist/gen/zod-code.d.ts +7 -0
  45. package/dist/gen/zod-code.js +163 -0
  46. package/dist/gen/zod-code.js.map +1 -0
  47. package/dist/graph-command.d.ts +11 -0
  48. package/dist/graph-command.js +67 -0
  49. package/dist/graph-command.js.map +1 -0
  50. package/dist/graph-model.d.ts +21 -0
  51. package/dist/graph-model.js +101 -0
  52. package/dist/graph-model.js.map +1 -0
  53. package/dist/graph-presentation.d.ts +11 -0
  54. package/dist/graph-presentation.js +25 -0
  55. package/dist/graph-presentation.js.map +1 -0
  56. package/dist/graph-query.d.ts +171 -0
  57. package/dist/graph-query.js +242 -0
  58. package/dist/graph-query.js.map +1 -0
  59. package/dist/graph-relations.d.ts +14 -0
  60. package/dist/graph-relations.js +142 -0
  61. package/dist/graph-relations.js.map +1 -0
  62. package/dist/graph-search.d.ts +21 -0
  63. package/dist/graph-search.js +63 -0
  64. package/dist/graph-search.js.map +1 -0
  65. package/dist/graph-snapshot.d.ts +11 -0
  66. package/dist/graph-snapshot.js +29 -0
  67. package/dist/graph-snapshot.js.map +1 -0
  68. package/dist/graph.d.ts +11 -0
  69. package/dist/graph.js +105 -0
  70. package/dist/graph.js.map +1 -0
  71. package/dist/impact.d.ts +34 -0
  72. package/dist/impact.js +235 -0
  73. package/dist/impact.js.map +1 -0
  74. package/dist/index.d.ts +20 -0
  75. package/dist/index.js +22 -0
  76. package/dist/index.js.map +1 -0
  77. package/dist/init.d.ts +6 -0
  78. package/dist/init.js +131 -0
  79. package/dist/init.js.map +1 -0
  80. package/dist/load.d.ts +6 -0
  81. package/dist/load.js +17 -0
  82. package/dist/load.js.map +1 -0
  83. package/dist/main.d.ts +1 -0
  84. package/dist/main.js +221 -0
  85. package/dist/main.js.map +1 -0
  86. package/dist/migrate.d.ts +4 -0
  87. package/dist/migrate.js +30 -0
  88. package/dist/migrate.js.map +1 -0
  89. package/dist/ontology.d.ts +43 -0
  90. package/dist/ontology.js +280 -0
  91. package/dist/ontology.js.map +1 -0
  92. package/dist/process.d.ts +6 -0
  93. package/dist/process.js +39 -0
  94. package/dist/process.js.map +1 -0
  95. package/dist/project-structure.d.ts +8 -0
  96. package/dist/project-structure.js +94 -0
  97. package/dist/project-structure.js.map +1 -0
  98. package/dist/rules.d.ts +23 -0
  99. package/dist/rules.js +251 -0
  100. package/dist/rules.js.map +1 -0
  101. package/dist/static-check.d.ts +23 -0
  102. package/dist/static-check.js +222 -0
  103. package/dist/static-check.js.map +1 -0
  104. package/dist/sync.d.ts +5 -0
  105. package/dist/sync.js +118 -0
  106. package/dist/sync.js.map +1 -0
  107. package/dist/upgrade.d.ts +10 -0
  108. package/dist/upgrade.js +81 -0
  109. package/dist/upgrade.js.map +1 -0
  110. package/dist/verify.d.ts +7 -0
  111. package/dist/verify.js +88 -0
  112. package/dist/verify.js.map +1 -0
  113. package/dist/version.d.ts +4 -0
  114. package/dist/version.js +6 -0
  115. package/dist/version.js.map +1 -0
  116. package/graph-ui/app.js +1103 -0
  117. package/graph-ui/graph-model.js +100 -0
  118. package/graph-ui/graph-relations.js +141 -0
  119. package/graph-ui/graph-search.js +62 -0
  120. package/graph-ui/index.html +197 -0
  121. package/graph-ui/model.js +83 -0
  122. package/graph-ui/relations.js +2 -0
  123. package/graph-ui/structure.js +77 -0
  124. package/graph-ui/style.css +1719 -0
  125. package/package.json +60 -3
  126. package/templates/app/.dockerignore +10 -0
  127. package/templates/app/.env.example +61 -0
  128. package/templates/app/.github/workflows/cage.yml +44 -0
  129. package/templates/app/AGENTS.md +96 -0
  130. package/templates/app/Dockerfile +25 -0
  131. package/templates/app/README.md +88 -0
  132. package/templates/app/_gitignore +8 -0
  133. package/templates/app/apps/api/package.json +16 -0
  134. package/templates/app/apps/api/src/main.ts +10 -0
  135. package/templates/app/apps/api/src/worker.ts +14 -0
  136. package/templates/app/apps/api/tsconfig.json +5 -0
  137. package/templates/app/apps/web/index.html +13 -0
  138. package/templates/app/apps/web/package.json +22 -0
  139. package/templates/app/apps/web/src/app/app.tsx +18 -0
  140. package/templates/app/apps/web/src/app/styles/global.css +663 -0
  141. package/templates/app/apps/web/src/app/workspace-layout.tsx +70 -0
  142. package/templates/app/apps/web/src/main.tsx +13 -0
  143. package/templates/app/apps/web/src/modules/approval/hooks/use-approval-tasks.ts +20 -0
  144. package/templates/app/apps/web/src/modules/approval/index.ts +2 -0
  145. package/templates/app/apps/web/src/modules/approval/pages/approval-tasks-page.tsx +54 -0
  146. package/templates/app/apps/web/src/modules/auth/index.ts +1 -0
  147. package/templates/app/apps/web/src/modules/auth/pages/login-page.tsx +59 -0
  148. package/templates/app/apps/web/src/modules/inventory/index.ts +1 -0
  149. package/templates/app/apps/web/src/modules/inventory/pages/inventory-page.tsx +74 -0
  150. package/templates/app/apps/web/src/modules/purchase/components/purchase-order-form.tsx +60 -0
  151. package/templates/app/apps/web/src/modules/purchase/index.ts +2 -0
  152. package/templates/app/apps/web/src/modules/purchase/pages/purchase-orders-page.tsx +210 -0
  153. package/templates/app/apps/web/src/shared/hooks/use-command.ts +29 -0
  154. package/templates/app/apps/web/src/shared/lib/error-message.ts +15 -0
  155. package/templates/app/apps/web/src/shared/ui/notice.tsx +13 -0
  156. package/templates/app/apps/web/src/shared/ui/page-heading.tsx +28 -0
  157. package/templates/app/apps/web/tsconfig.json +11 -0
  158. package/templates/app/apps/web/vite.config.ts +19 -0
  159. package/templates/app/cage.config.json +114 -0
  160. package/templates/app/db/migrations/0001_init.sql +112 -0
  161. package/templates/app/docs/ai/README.md +77 -0
  162. package/templates/app/docs/ai/architecture.md +122 -0
  163. package/templates/app/docs/ai/ontology-context.md +117 -0
  164. package/templates/app/docs/ai/primitives.md +249 -0
  165. package/templates/app/docs/ai/project-structure.md +123 -0
  166. package/templates/app/docs/ai/purchase-example.md +97 -0
  167. package/templates/app/docs/ai/recipes.md +81 -0
  168. package/templates/app/docs/ai/relations.md +160 -0
  169. package/templates/app/docs/ai/transfers.md +92 -0
  170. package/templates/app/docs/ai/troubleshooting.md +60 -0
  171. package/templates/app/docs/ai/workflow.md +139 -0
  172. package/templates/app/docs/errors/CAGE-APPR-001.md +47 -0
  173. package/templates/app/docs/errors/CAGE-APPR-002.md +47 -0
  174. package/templates/app/docs/errors/CAGE-APPR-003.md +47 -0
  175. package/templates/app/docs/errors/CAGE-APPR-004.md +47 -0
  176. package/templates/app/docs/errors/CAGE-APPR-005.md +47 -0
  177. package/templates/app/docs/errors/CAGE-APPR-006.md +47 -0
  178. package/templates/app/docs/errors/CAGE-APPR-007.md +50 -0
  179. package/templates/app/docs/errors/CAGE-APPR-010.md +48 -0
  180. package/templates/app/docs/errors/CAGE-AUTH-001.md +47 -0
  181. package/templates/app/docs/errors/CAGE-BUDGET-001.md +47 -0
  182. package/templates/app/docs/errors/CAGE-BUDGET-002.md +47 -0
  183. package/templates/app/docs/errors/CAGE-BUDGET-003.md +47 -0
  184. package/templates/app/docs/errors/CAGE-CMD-002.md +47 -0
  185. package/templates/app/docs/errors/CAGE-CMD-003.md +47 -0
  186. package/templates/app/docs/errors/CAGE-CONN-001.md +47 -0
  187. package/templates/app/docs/errors/CAGE-CONN-002.md +47 -0
  188. package/templates/app/docs/errors/CAGE-CONN-003.md +47 -0
  189. package/templates/app/docs/errors/CAGE-CONN-004.md +47 -0
  190. package/templates/app/docs/errors/CAGE-CONN-005.md +47 -0
  191. package/templates/app/docs/errors/CAGE-CONN-006.md +47 -0
  192. package/templates/app/docs/errors/CAGE-CONN-007.md +47 -0
  193. package/templates/app/docs/errors/CAGE-CONN-008.md +47 -0
  194. package/templates/app/docs/errors/CAGE-CONN-009.md +47 -0
  195. package/templates/app/docs/errors/CAGE-CONN-010.md +47 -0
  196. package/templates/app/docs/errors/CAGE-CONN-011.md +47 -0
  197. package/templates/app/docs/errors/CAGE-CONN-012.md +47 -0
  198. package/templates/app/docs/errors/CAGE-CREATE-001.md +52 -0
  199. package/templates/app/docs/errors/CAGE-DECL-001.md +48 -0
  200. package/templates/app/docs/errors/CAGE-DECL-002.md +47 -0
  201. package/templates/app/docs/errors/CAGE-DECL-003.md +47 -0
  202. package/templates/app/docs/errors/CAGE-DECL-004.md +47 -0
  203. package/templates/app/docs/errors/CAGE-DEP-001.md +48 -0
  204. package/templates/app/docs/errors/CAGE-DEP-002.md +47 -0
  205. package/templates/app/docs/errors/CAGE-DEP-003.md +47 -0
  206. package/templates/app/docs/errors/CAGE-DEP-004.md +47 -0
  207. package/templates/app/docs/errors/CAGE-DEP-005.md +47 -0
  208. package/templates/app/docs/errors/CAGE-DLQ-001.md +48 -0
  209. package/templates/app/docs/errors/CAGE-ENV-001.md +52 -0
  210. package/templates/app/docs/errors/CAGE-EVENT-001.md +47 -0
  211. package/templates/app/docs/errors/CAGE-EVENT-002.md +50 -0
  212. package/templates/app/docs/errors/CAGE-FIELD-001.md +47 -0
  213. package/templates/app/docs/errors/CAGE-FIELD-002.md +47 -0
  214. package/templates/app/docs/errors/CAGE-FIELD-003.md +47 -0
  215. package/templates/app/docs/errors/CAGE-FIELD-004.md +47 -0
  216. package/templates/app/docs/errors/CAGE-GEN-001.md +48 -0
  217. package/templates/app/docs/errors/CAGE-GEN-002.md +47 -0
  218. package/templates/app/docs/errors/CAGE-GRANT-001.md +49 -0
  219. package/templates/app/docs/errors/CAGE-GRANT-002.md +48 -0
  220. package/templates/app/docs/errors/CAGE-GRANT-003.md +47 -0
  221. package/templates/app/docs/errors/CAGE-IMPACT-001.md +48 -0
  222. package/templates/app/docs/errors/CAGE-INIT-001.md +47 -0
  223. package/templates/app/docs/errors/CAGE-INIT-002.md +49 -0
  224. package/templates/app/docs/errors/CAGE-INIT-003.md +48 -0
  225. package/templates/app/docs/errors/CAGE-INIT-004.md +48 -0
  226. package/templates/app/docs/errors/CAGE-JOB-001.md +47 -0
  227. package/templates/app/docs/errors/CAGE-JOB-002.md +47 -0
  228. package/templates/app/docs/errors/CAGE-JOB-003.md +47 -0
  229. package/templates/app/docs/errors/CAGE-LINT-001.md +47 -0
  230. package/templates/app/docs/errors/CAGE-MIG-001.md +47 -0
  231. package/templates/app/docs/errors/CAGE-MIG-002.md +47 -0
  232. package/templates/app/docs/errors/CAGE-MIG-003.md +5 -0
  233. package/templates/app/docs/errors/CAGE-MIG-004.md +5 -0
  234. package/templates/app/docs/errors/CAGE-MIG-005.md +47 -0
  235. package/templates/app/docs/errors/CAGE-NAME-001.md +47 -0
  236. package/templates/app/docs/errors/CAGE-NAME-002.md +47 -0
  237. package/templates/app/docs/errors/CAGE-NAME-003.md +47 -0
  238. package/templates/app/docs/errors/CAGE-NAME-004.md +47 -0
  239. package/templates/app/docs/errors/CAGE-NAME-005.md +47 -0
  240. package/templates/app/docs/errors/CAGE-NAME-006.md +47 -0
  241. package/templates/app/docs/errors/CAGE-ORG-001.md +49 -0
  242. package/templates/app/docs/errors/CAGE-PAIR-002.md +50 -0
  243. package/templates/app/docs/errors/CAGE-QUERY-001.md +47 -0
  244. package/templates/app/docs/errors/CAGE-QUERY-002.md +47 -0
  245. package/templates/app/docs/errors/CAGE-QUERY-003.md +49 -0
  246. package/templates/app/docs/errors/CAGE-REACT-001.md +47 -0
  247. package/templates/app/docs/errors/CAGE-REACT-002.md +47 -0
  248. package/templates/app/docs/errors/CAGE-READS-001.md +47 -0
  249. package/templates/app/docs/errors/CAGE-REG-001.md +53 -0
  250. package/templates/app/docs/errors/CAGE-REG-002.md +48 -0
  251. package/templates/app/docs/errors/CAGE-REG-003.md +47 -0
  252. package/templates/app/docs/errors/CAGE-REL-001.md +64 -0
  253. package/templates/app/docs/errors/CAGE-ROUTE-001.md +47 -0
  254. package/templates/app/docs/errors/CAGE-SCOPE-001.md +47 -0
  255. package/templates/app/docs/errors/CAGE-SCOPE-002.md +47 -0
  256. package/templates/app/docs/errors/CAGE-SECRET-001.md +48 -0
  257. package/templates/app/docs/errors/CAGE-SECRET-002.md +47 -0
  258. package/templates/app/docs/errors/CAGE-SENS-001.md +48 -0
  259. package/templates/app/docs/errors/CAGE-SQL-001.md +47 -0
  260. package/templates/app/docs/errors/CAGE-SQL-002.md +47 -0
  261. package/templates/app/docs/errors/CAGE-STATE-001.md +49 -0
  262. package/templates/app/docs/errors/CAGE-STATE-002.md +47 -0
  263. package/templates/app/docs/errors/CAGE-STATE-003.md +47 -0
  264. package/templates/app/docs/errors/CAGE-STATE-004.md +47 -0
  265. package/templates/app/docs/errors/CAGE-STATE-005.md +48 -0
  266. package/templates/app/docs/errors/CAGE-STATE-010.md +48 -0
  267. package/templates/app/docs/errors/CAGE-STATE-011.md +48 -0
  268. package/templates/app/docs/errors/CAGE-STRUCT-001.md +51 -0
  269. package/templates/app/docs/errors/CAGE-STRUCT-002.md +51 -0
  270. package/templates/app/docs/errors/CAGE-SYNC-001.md +48 -0
  271. package/templates/app/docs/errors/CAGE-TEST-001.md +47 -0
  272. package/templates/app/docs/errors/CAGE-UNSAFE-001.md +48 -0
  273. package/templates/app/docs/errors/CAGE-UPGRADE-001.md +48 -0
  274. package/templates/app/docs/errors/README.md +307 -0
  275. package/templates/app/docs/errors/runtime.md +50 -0
  276. package/templates/app/package.json +22 -0
  277. package/templates/app/packages/contracts/package.json +16 -0
  278. package/templates/app/packages/contracts/src/aggregates/dept/index.ts +74 -0
  279. package/templates/app/packages/contracts/src/aggregates/inventory/index.ts +57 -0
  280. package/templates/app/packages/contracts/src/aggregates/inventory-lock/index.ts +52 -0
  281. package/templates/app/packages/contracts/src/aggregates/purchase-order/index.ts +72 -0
  282. package/templates/app/packages/contracts/src/aggregates/role/index.ts +85 -0
  283. package/templates/app/packages/contracts/src/aggregates/user/index.ts +89 -0
  284. package/templates/app/packages/contracts/src/index.ts +81 -0
  285. package/templates/app/packages/contracts/src/queries/index.ts +17 -0
  286. package/templates/app/packages/contracts/tsconfig.json +5 -0
  287. package/templates/app/packages/domain/package.json +23 -0
  288. package/templates/app/packages/domain/src/index.ts +6 -0
  289. package/templates/app/packages/domain/src/integrations/erp.ts +36 -0
  290. package/templates/app/packages/domain/src/modules/inventory/index.ts +2 -0
  291. package/templates/app/packages/domain/src/modules/inventory/inventory.ts +43 -0
  292. package/templates/app/packages/domain/src/modules/purchase/approvals.ts +18 -0
  293. package/templates/app/packages/domain/src/modules/purchase/index.ts +6 -0
  294. package/templates/app/packages/domain/src/modules/purchase/jobs.ts +13 -0
  295. package/templates/app/packages/domain/src/modules/purchase/purchase-order.test.ts +137 -0
  296. package/templates/app/packages/domain/src/modules/purchase/purchase-order.ts +122 -0
  297. package/templates/app/packages/domain/src/modules/purchase/queries.ts +10 -0
  298. package/templates/app/packages/domain/src/workflows/purchase-inventory.ts +24 -0
  299. package/templates/app/packages/domain/tsconfig.json +5 -0
  300. package/templates/app/pnpm-workspace.yaml +10 -0
  301. package/templates/app/renovate.json +5 -0
  302. package/templates/app/tsconfig.base.json +8 -0
  303. package/templates/blank/.dockerignore +10 -0
  304. package/templates/blank/.env.example +61 -0
  305. package/templates/blank/.github/workflows/cage.yml +44 -0
  306. package/templates/blank/AGENTS.md +96 -0
  307. package/templates/blank/Dockerfile +25 -0
  308. package/templates/blank/README.md +37 -0
  309. package/templates/blank/_gitignore +8 -0
  310. package/templates/blank/apps/api/package.json +15 -0
  311. package/templates/blank/apps/api/src/main.ts +11 -0
  312. package/templates/blank/apps/api/src/worker.ts +8 -0
  313. package/templates/blank/apps/api/tsconfig.json +5 -0
  314. package/templates/blank/apps/web/index.html +13 -0
  315. package/templates/blank/apps/web/package.json +22 -0
  316. package/templates/blank/apps/web/src/app/app.tsx +44 -0
  317. package/templates/blank/apps/web/src/app/styles/global.css +516 -0
  318. package/templates/blank/apps/web/src/app/workspace-layout.tsx +70 -0
  319. package/templates/blank/apps/web/src/main.tsx +13 -0
  320. package/templates/blank/apps/web/src/modules/auth/index.ts +1 -0
  321. package/templates/blank/apps/web/src/modules/auth/pages/login-page.tsx +87 -0
  322. package/templates/blank/apps/web/src/modules/workspace/index.ts +1 -0
  323. package/templates/blank/apps/web/src/modules/workspace/pages/home-page.tsx +45 -0
  324. package/templates/blank/apps/web/src/shared/lib/error-message.ts +11 -0
  325. package/templates/blank/apps/web/src/shared/ui/brand.tsx +11 -0
  326. package/templates/blank/apps/web/tsconfig.json +11 -0
  327. package/templates/blank/apps/web/vite.config.ts +19 -0
  328. package/templates/blank/cage.config.json +14 -0
  329. package/templates/blank/db/migrations/0001_init.sql +63 -0
  330. package/templates/blank/docs/ai/README.md +77 -0
  331. package/templates/blank/docs/ai/architecture.md +122 -0
  332. package/templates/blank/docs/ai/ontology-context.md +117 -0
  333. package/templates/blank/docs/ai/primitives.md +249 -0
  334. package/templates/blank/docs/ai/project-structure.md +123 -0
  335. package/templates/blank/docs/ai/purchase-example.md +97 -0
  336. package/templates/blank/docs/ai/recipes.md +81 -0
  337. package/templates/blank/docs/ai/relations.md +160 -0
  338. package/templates/blank/docs/ai/transfers.md +92 -0
  339. package/templates/blank/docs/ai/troubleshooting.md +60 -0
  340. package/templates/blank/docs/ai/workflow.md +139 -0
  341. package/templates/blank/docs/errors/CAGE-APPR-001.md +47 -0
  342. package/templates/blank/docs/errors/CAGE-APPR-002.md +47 -0
  343. package/templates/blank/docs/errors/CAGE-APPR-003.md +47 -0
  344. package/templates/blank/docs/errors/CAGE-APPR-004.md +47 -0
  345. package/templates/blank/docs/errors/CAGE-APPR-005.md +47 -0
  346. package/templates/blank/docs/errors/CAGE-APPR-006.md +47 -0
  347. package/templates/blank/docs/errors/CAGE-APPR-007.md +50 -0
  348. package/templates/blank/docs/errors/CAGE-APPR-010.md +48 -0
  349. package/templates/blank/docs/errors/CAGE-AUTH-001.md +47 -0
  350. package/templates/blank/docs/errors/CAGE-BUDGET-001.md +47 -0
  351. package/templates/blank/docs/errors/CAGE-BUDGET-002.md +47 -0
  352. package/templates/blank/docs/errors/CAGE-BUDGET-003.md +47 -0
  353. package/templates/blank/docs/errors/CAGE-CMD-002.md +47 -0
  354. package/templates/blank/docs/errors/CAGE-CMD-003.md +47 -0
  355. package/templates/blank/docs/errors/CAGE-CONN-001.md +47 -0
  356. package/templates/blank/docs/errors/CAGE-CONN-002.md +47 -0
  357. package/templates/blank/docs/errors/CAGE-CONN-003.md +47 -0
  358. package/templates/blank/docs/errors/CAGE-CONN-004.md +47 -0
  359. package/templates/blank/docs/errors/CAGE-CONN-005.md +47 -0
  360. package/templates/blank/docs/errors/CAGE-CONN-006.md +47 -0
  361. package/templates/blank/docs/errors/CAGE-CONN-007.md +47 -0
  362. package/templates/blank/docs/errors/CAGE-CONN-008.md +47 -0
  363. package/templates/blank/docs/errors/CAGE-CONN-009.md +47 -0
  364. package/templates/blank/docs/errors/CAGE-CONN-010.md +47 -0
  365. package/templates/blank/docs/errors/CAGE-CONN-011.md +47 -0
  366. package/templates/blank/docs/errors/CAGE-CONN-012.md +47 -0
  367. package/templates/blank/docs/errors/CAGE-CREATE-001.md +52 -0
  368. package/templates/blank/docs/errors/CAGE-DECL-001.md +48 -0
  369. package/templates/blank/docs/errors/CAGE-DECL-002.md +47 -0
  370. package/templates/blank/docs/errors/CAGE-DECL-003.md +47 -0
  371. package/templates/blank/docs/errors/CAGE-DECL-004.md +47 -0
  372. package/templates/blank/docs/errors/CAGE-DEP-001.md +48 -0
  373. package/templates/blank/docs/errors/CAGE-DEP-002.md +47 -0
  374. package/templates/blank/docs/errors/CAGE-DEP-003.md +47 -0
  375. package/templates/blank/docs/errors/CAGE-DEP-004.md +47 -0
  376. package/templates/blank/docs/errors/CAGE-DEP-005.md +47 -0
  377. package/templates/blank/docs/errors/CAGE-DLQ-001.md +48 -0
  378. package/templates/blank/docs/errors/CAGE-ENV-001.md +52 -0
  379. package/templates/blank/docs/errors/CAGE-EVENT-001.md +47 -0
  380. package/templates/blank/docs/errors/CAGE-EVENT-002.md +50 -0
  381. package/templates/blank/docs/errors/CAGE-FIELD-001.md +47 -0
  382. package/templates/blank/docs/errors/CAGE-FIELD-002.md +47 -0
  383. package/templates/blank/docs/errors/CAGE-FIELD-003.md +47 -0
  384. package/templates/blank/docs/errors/CAGE-FIELD-004.md +47 -0
  385. package/templates/blank/docs/errors/CAGE-GEN-001.md +48 -0
  386. package/templates/blank/docs/errors/CAGE-GEN-002.md +47 -0
  387. package/templates/blank/docs/errors/CAGE-GRANT-001.md +49 -0
  388. package/templates/blank/docs/errors/CAGE-GRANT-002.md +48 -0
  389. package/templates/blank/docs/errors/CAGE-GRANT-003.md +47 -0
  390. package/templates/blank/docs/errors/CAGE-IMPACT-001.md +48 -0
  391. package/templates/blank/docs/errors/CAGE-INIT-001.md +47 -0
  392. package/templates/blank/docs/errors/CAGE-INIT-002.md +49 -0
  393. package/templates/blank/docs/errors/CAGE-INIT-003.md +48 -0
  394. package/templates/blank/docs/errors/CAGE-INIT-004.md +48 -0
  395. package/templates/blank/docs/errors/CAGE-JOB-001.md +47 -0
  396. package/templates/blank/docs/errors/CAGE-JOB-002.md +47 -0
  397. package/templates/blank/docs/errors/CAGE-JOB-003.md +47 -0
  398. package/templates/blank/docs/errors/CAGE-LINT-001.md +47 -0
  399. package/templates/blank/docs/errors/CAGE-MIG-001.md +47 -0
  400. package/templates/blank/docs/errors/CAGE-MIG-002.md +47 -0
  401. package/templates/blank/docs/errors/CAGE-MIG-003.md +5 -0
  402. package/templates/blank/docs/errors/CAGE-MIG-004.md +5 -0
  403. package/templates/blank/docs/errors/CAGE-MIG-005.md +47 -0
  404. package/templates/blank/docs/errors/CAGE-NAME-001.md +47 -0
  405. package/templates/blank/docs/errors/CAGE-NAME-002.md +47 -0
  406. package/templates/blank/docs/errors/CAGE-NAME-003.md +47 -0
  407. package/templates/blank/docs/errors/CAGE-NAME-004.md +47 -0
  408. package/templates/blank/docs/errors/CAGE-NAME-005.md +47 -0
  409. package/templates/blank/docs/errors/CAGE-NAME-006.md +47 -0
  410. package/templates/blank/docs/errors/CAGE-ORG-001.md +49 -0
  411. package/templates/blank/docs/errors/CAGE-PAIR-002.md +50 -0
  412. package/templates/blank/docs/errors/CAGE-QUERY-001.md +47 -0
  413. package/templates/blank/docs/errors/CAGE-QUERY-002.md +47 -0
  414. package/templates/blank/docs/errors/CAGE-QUERY-003.md +49 -0
  415. package/templates/blank/docs/errors/CAGE-REACT-001.md +47 -0
  416. package/templates/blank/docs/errors/CAGE-REACT-002.md +47 -0
  417. package/templates/blank/docs/errors/CAGE-READS-001.md +47 -0
  418. package/templates/blank/docs/errors/CAGE-REG-001.md +53 -0
  419. package/templates/blank/docs/errors/CAGE-REG-002.md +48 -0
  420. package/templates/blank/docs/errors/CAGE-REG-003.md +47 -0
  421. package/templates/blank/docs/errors/CAGE-REL-001.md +64 -0
  422. package/templates/blank/docs/errors/CAGE-ROUTE-001.md +47 -0
  423. package/templates/blank/docs/errors/CAGE-SCOPE-001.md +47 -0
  424. package/templates/blank/docs/errors/CAGE-SCOPE-002.md +47 -0
  425. package/templates/blank/docs/errors/CAGE-SECRET-001.md +48 -0
  426. package/templates/blank/docs/errors/CAGE-SECRET-002.md +47 -0
  427. package/templates/blank/docs/errors/CAGE-SENS-001.md +48 -0
  428. package/templates/blank/docs/errors/CAGE-SQL-001.md +47 -0
  429. package/templates/blank/docs/errors/CAGE-SQL-002.md +47 -0
  430. package/templates/blank/docs/errors/CAGE-STATE-001.md +49 -0
  431. package/templates/blank/docs/errors/CAGE-STATE-002.md +47 -0
  432. package/templates/blank/docs/errors/CAGE-STATE-003.md +47 -0
  433. package/templates/blank/docs/errors/CAGE-STATE-004.md +47 -0
  434. package/templates/blank/docs/errors/CAGE-STATE-005.md +48 -0
  435. package/templates/blank/docs/errors/CAGE-STATE-010.md +48 -0
  436. package/templates/blank/docs/errors/CAGE-STATE-011.md +48 -0
  437. package/templates/blank/docs/errors/CAGE-STRUCT-001.md +51 -0
  438. package/templates/blank/docs/errors/CAGE-STRUCT-002.md +51 -0
  439. package/templates/blank/docs/errors/CAGE-SYNC-001.md +48 -0
  440. package/templates/blank/docs/errors/CAGE-TEST-001.md +47 -0
  441. package/templates/blank/docs/errors/CAGE-UNSAFE-001.md +48 -0
  442. package/templates/blank/docs/errors/CAGE-UPGRADE-001.md +48 -0
  443. package/templates/blank/docs/errors/README.md +307 -0
  444. package/templates/blank/docs/errors/runtime.md +50 -0
  445. package/templates/blank/docs/examples/purchase-admin/README.md +5 -0
  446. package/templates/blank/docs/examples/purchase-admin/apps/api/src/main.ts +10 -0
  447. package/templates/blank/docs/examples/purchase-admin/apps/api/src/worker.ts +14 -0
  448. package/templates/blank/docs/examples/purchase-admin/apps/web/src/app/app.tsx +18 -0
  449. package/templates/blank/docs/examples/purchase-admin/apps/web/src/app/styles/global.css +663 -0
  450. package/templates/blank/docs/examples/purchase-admin/apps/web/src/app/workspace-layout.tsx +70 -0
  451. package/templates/blank/docs/examples/purchase-admin/apps/web/src/main.tsx +13 -0
  452. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/approval/hooks/use-approval-tasks.ts +20 -0
  453. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/approval/index.ts +2 -0
  454. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/approval/pages/approval-tasks-page.tsx +54 -0
  455. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/auth/index.ts +1 -0
  456. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/auth/pages/login-page.tsx +59 -0
  457. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/inventory/index.ts +1 -0
  458. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/inventory/pages/inventory-page.tsx +74 -0
  459. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/purchase/components/purchase-order-form.tsx +60 -0
  460. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/purchase/index.ts +2 -0
  461. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/purchase/pages/purchase-orders-page.tsx +210 -0
  462. package/templates/blank/docs/examples/purchase-admin/apps/web/src/shared/hooks/use-command.ts +29 -0
  463. package/templates/blank/docs/examples/purchase-admin/apps/web/src/shared/lib/error-message.ts +15 -0
  464. package/templates/blank/docs/examples/purchase-admin/apps/web/src/shared/ui/notice.tsx +13 -0
  465. package/templates/blank/docs/examples/purchase-admin/apps/web/src/shared/ui/page-heading.tsx +28 -0
  466. package/templates/blank/docs/examples/purchase-admin/cage.config.json +114 -0
  467. package/templates/blank/docs/examples/purchase-admin/packages/contracts/src/aggregates/dept/index.ts +74 -0
  468. package/templates/blank/docs/examples/purchase-admin/packages/contracts/src/aggregates/inventory/index.ts +57 -0
  469. package/templates/blank/docs/examples/purchase-admin/packages/contracts/src/aggregates/inventory-lock/index.ts +52 -0
  470. package/templates/blank/docs/examples/purchase-admin/packages/contracts/src/aggregates/purchase-order/index.ts +72 -0
  471. package/templates/blank/docs/examples/purchase-admin/packages/contracts/src/aggregates/role/index.ts +85 -0
  472. package/templates/blank/docs/examples/purchase-admin/packages/contracts/src/aggregates/user/index.ts +89 -0
  473. package/templates/blank/docs/examples/purchase-admin/packages/contracts/src/index.ts +81 -0
  474. package/templates/blank/docs/examples/purchase-admin/packages/contracts/src/queries/index.ts +17 -0
  475. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/index.ts +6 -0
  476. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/integrations/erp.ts +36 -0
  477. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/inventory/index.ts +2 -0
  478. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/inventory/inventory.ts +43 -0
  479. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/approvals.ts +18 -0
  480. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/index.ts +6 -0
  481. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/jobs.ts +13 -0
  482. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/purchase-order.test.ts +137 -0
  483. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/purchase-order.ts +122 -0
  484. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/queries.ts +10 -0
  485. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/workflows/purchase-inventory.ts +24 -0
  486. package/templates/blank/package.json +22 -0
  487. package/templates/blank/packages/contracts/package.json +16 -0
  488. package/templates/blank/packages/contracts/src/aggregates/dept/index.ts +74 -0
  489. package/templates/blank/packages/contracts/src/aggregates/role/index.ts +85 -0
  490. package/templates/blank/packages/contracts/src/aggregates/user/index.ts +89 -0
  491. package/templates/blank/packages/contracts/src/index.ts +55 -0
  492. package/templates/blank/packages/contracts/src/queries/index.ts +13 -0
  493. package/templates/blank/packages/contracts/tsconfig.json +5 -0
  494. package/templates/blank/packages/domain/package.json +23 -0
  495. package/templates/blank/packages/domain/src/index.ts +4 -0
  496. package/templates/blank/packages/domain/tsconfig.json +5 -0
  497. package/templates/blank/pnpm-workspace.yaml +10 -0
  498. package/templates/blank/renovate.json +5 -0
  499. package/templates/blank/tsconfig.base.json +8 -0
@@ -0,0 +1,100 @@
1
+ export const kindLabels = {
2
+ aggregate: '业务对象',
3
+ command: '命令',
4
+ event: '事件',
5
+ reaction: '反应',
6
+ query: '查询',
7
+ approval: '审批',
8
+ route: '审批路由',
9
+ job: '定时任务',
10
+ connector: '连接器',
11
+ outbound: '出站映射',
12
+ inbound: '入站回调',
13
+ field: '字段',
14
+ state: '状态',
15
+ capability: '能力点',
16
+ entry: '新建入口',
17
+ };
18
+ export const edgeLabels = {
19
+ transition: '状态迁移',
20
+ reads: '读取',
21
+ emits: '发出',
22
+ uses: '使用字段',
23
+ triggers: '触发',
24
+ cancels: '取消审批',
25
+ targets: '调用目标',
26
+ queries: '查询',
27
+ refs: '引用',
28
+ calls: '调用',
29
+ binds: '审批绑定',
30
+ field: '声明字段',
31
+ ownership: '归属(标识推导)',
32
+ };
33
+ /** graph.labels 的显示覆盖不参与本体哈希;声明内的 description 仍由抽取器
34
+ * 按该声明的含义处理。中文展示不会替换路由、权限或数据表使用的稳定标识。 */
35
+ export function nodeLabel(node, labels = {}) {
36
+ if (node.kind === 'entry')
37
+ return '新建入口';
38
+ return labels[node.key] || String(node.attrs.description || node.id);
39
+ }
40
+ export function projectGraph(graph) {
41
+ const nodes = graph.nodes.map((n) => ({ ...n, key: `${n.kind}:${n.id}` }));
42
+ const edges = [];
43
+ const find = (id, kinds) => nodes.find((n) => n.id === id && kinds.includes(n.kind));
44
+ // 原始边以业务 ID 存端点;投影时按关系类型恢复端点身份,不能猜成同名的状态或能力点。
45
+ const endpoints = {
46
+ transition: [['state', 'entry'], ['state']],
47
+ field: [['aggregate'], ['field']],
48
+ refs: [['field'], ['aggregate']],
49
+ reads: [['command'], ['aggregate']],
50
+ queries: [['query', 'job'], ['aggregate']],
51
+ emits: [['command'], ['event']],
52
+ triggers: [['event'], ['reaction', 'approval', 'outbound']],
53
+ cancels: [['event'], ['approval']],
54
+ targets: [
55
+ ['reaction', 'outbound', 'inbound'],
56
+ ['command', 'connector'],
57
+ ],
58
+ calls: [['job'], ['command']],
59
+ binds: [
60
+ ['route', 'approval'],
61
+ ['approval', 'command'],
62
+ ],
63
+ uses: [['command', 'event', 'query', 'approval', 'route', 'outbound'], ['field']],
64
+ };
65
+ for (const edge of graph.edges) {
66
+ const [fromKinds, toKinds] = endpoints[edge.kind];
67
+ let from = find(edge.from, fromKinds);
68
+ const to = find(edge.to, toKinds);
69
+ if (!from && edge.kind === 'transition' && edge.from.endsWith('.[new]')) {
70
+ from = { key: `entry:${edge.from}`, id: edge.from, kind: 'entry', hash: '', attrs: {} };
71
+ nodes.push(from);
72
+ }
73
+ if (from && to)
74
+ edges.push({ ...edge, from: from.key, to: to.key, key: `edge:${edges.length}`, derived: false });
75
+ }
76
+ for (const node of nodes) {
77
+ if (!['command', 'state', 'capability', 'entry'].includes(node.kind))
78
+ continue;
79
+ const owner = nodes.find((n) => n.kind === 'aggregate' && node.id.startsWith(`${n.id}.`));
80
+ if (owner)
81
+ edges.push({
82
+ key: `owner:${node.key}`,
83
+ kind: 'ownership',
84
+ from: owner.key,
85
+ to: node.key,
86
+ derived: true,
87
+ });
88
+ }
89
+ return { nodes, edges };
90
+ }
91
+ export function neighborhood(model, key) {
92
+ const connected = new Set([key]);
93
+ for (const edge of model.edges) {
94
+ if (edge.from === key)
95
+ connected.add(edge.to);
96
+ if (edge.to === key)
97
+ connected.add(edge.from);
98
+ }
99
+ return connected;
100
+ }
@@ -0,0 +1,141 @@
1
+ import { kindLabels, nodeLabel } from './graph-model.js';
2
+ export function explainEdge(edge, model, labels = {}) {
3
+ const from = model.nodes.find((n) => n.key === edge.from);
4
+ const to = model.nodes.find((n) => n.key === edge.to);
5
+ const a = nodeLabel(from, labels), b = nodeLabel(to, labels);
6
+ const field = to.id.slice(to.id.indexOf('.') + 1);
7
+ const resource = (id) => labels[`resource:${id}`] ?? id;
8
+ const result = (label, category, why, purpose, evidence, source = from) => ({
9
+ label,
10
+ category,
11
+ why,
12
+ purpose,
13
+ evidence,
14
+ source,
15
+ basis: edge.derived || (edge.kind === 'uses' && ['事件载荷', '出站映射'].includes(edge.as ?? ''))
16
+ ? 'inferred'
17
+ : edge.kind === 'uses' && edge.as === '查询'
18
+ ? 'text-match'
19
+ : 'declared',
20
+ direction: `“${a}” → “${b}”:${label}。`,
21
+ });
22
+ switch (edge.kind) {
23
+ case 'field':
24
+ return result(to.attrs.virtual ? '声明关联字段' : '包含字段', '数据结构', `“${a}”的字段声明中包含“${b}”。`, to.attrs.virtual
25
+ ? '这是 fields 中的集合关系,不占主表列;点击它指向目标的连线,查看关系基数、存储和读写方式。'
26
+ : '说明业务对象由哪些数据组成;调整字段时,可以继续沿线查看受影响的查询、审批和事件。', [`${from.id}.fields.${field}`]);
27
+ case 'refs': {
28
+ if (edge.cardinality === 'one-to-many')
29
+ return result('一对多 1:N', '反向关联 · 只读', `“${a}”使用 hasMany 反查“${b}”,对应外键是 ${to.id}.${edge.mappedBy}。`, '按需分页列出属于当前对象的子对象,不新建列或中间表。改变归属要执行子对象的命令;读取同时受两侧权限和范围约束。', [`${from.id}: hasMany(${JSON.stringify(to.id)}, { mappedBy: ${JSON.stringify(edge.mappedBy)} })`]);
30
+ if (edge.cardinality === 'many-to-many')
31
+ return result('多对多 N:M', '集合关联 · 自动中间表', `“${a}”声明 manyToMany:一个所属对象可以关联多个“${b}”,同一个目标也可被多个所属对象关联。`, '框架生成中间表、同租户外键和组合唯一约束;所属对象的命令用 ID 数组替换关联,与主记录共同提交。解绑不删除目标,读取通过独立分页接口。', [`${from.id}: manyToMany(${JSON.stringify(to.id)})`, `自动中间表:${from.attrs.through}`]);
32
+ return result(edge.cardinality === 'one-to-one' ? '一对一 1:1' : '多对一 N:1', '数据关联', `“${a}”声明为引用“${b}”的字段,保存所关联对象的标识。`, edge.cardinality === 'one-to-one'
33
+ ? '租户内的唯一约束保证同一目标最多被这一字段引用一次;可选字段允许没有关联,并不要求双方一定存在。'
34
+ : `多个所属对象可以引用同一${kindLabels[to.kind]}。字段保存外键;具体查询仍受数据范围约束,这条线本身不执行查询。`, [`${from.id}: ref(${JSON.stringify(to.id)})${edge.cardinality === 'one-to-one' ? '.unique()' : ''}`]);
35
+ }
36
+ case 'ownership':
37
+ return result(`拥有${kindLabels[to.kind]}`, '标识归属 · 推导', `“${b}”的标识位于“${from.id}.”之下,投影据此将它归入“${a}”。`, '帮助整理同一业务对象的命令、状态和能力点。这是标识推导的归属,不是抽取器声明的依赖,也不会触发命令。', [`${to.id} → 所属聚合 ${from.id}`]);
38
+ case 'transition': {
39
+ const command = model.nodes.find((n) => n.kind === 'command' && n.id === edge.via);
40
+ const name = nodeLabel(command, labels);
41
+ const stays = edge.from === edge.to;
42
+ return result(stays ? '保持状态' : '迁移状态', '状态变化', `“${name}”声明${stays ? `在“${a}”状态执行后保持原状态` : `从“${a}”进入“${b}”`}。`, `用于检查业务生命周期是否完整。只有命令通过权限、约束并成功提交,${stays ? '操作才生效' : '状态才改变'}。`, [
43
+ `命令:${command.id}`,
44
+ `from: ${JSON.stringify(command.attrs.from)}`,
45
+ `to: ${JSON.stringify(command.attrs.to)}`,
46
+ ], command);
47
+ }
48
+ case 'reads':
49
+ case 'queries':
50
+ return result('读取数据', '读取依赖', `“${a}”在 reads 中声明读取“${b}”${edge.alias ? `,使用别名 ${edge.alias}` : ''}。`, '明确可读取的业务对象,并让影响分析找到依赖方;实际读取通过安全视图,受权限和数据范围约束。箭头指向数据来源,不表示先执行它。', [`${from.id}.reads${edge.alias ? `.${edge.alias}` : ''} → ${to.id}`]);
51
+ case 'emits':
52
+ return result('产生事件', '事件协作', `“${a}”的 emits 中声明了“${b}”事件。`, '命令成功执行时按声明生成事件,连接后续反应、审批或外部同步;事务失败时不能当作已经发生。', [`${from.id}.emits → ${to.id}`]);
53
+ case 'uses': {
54
+ const as = edge.as ?? '';
55
+ if (as === '可路由字段')
56
+ return result('允许审批读取', '审批可用字段', `“${a}”把“${b}”列入 routable,允许该字段作为审批流程变量。`, '明确审批可以拿到哪些业务数据。这里只表示允许使用;是否真的参与分支或审批选人,要查看审批路由的字段关系。', [`${from.id}.routable 包含 ${field}`]);
57
+ if (as === '路由条件') {
58
+ // fieldsUsed 同时包含 when 条件和 deptLeader 选人字段,不能一律解释成条件判断。
59
+ const nodes = from.attrs.nodes;
60
+ const conditions = nodes.filter((n) => n.kind === 'when' && n.cond.field === field);
61
+ const steps = nodes
62
+ .flatMap((n) => (n.kind === 'step' ? [n] : n.steps))
63
+ .filter((s) => s.approver.kind === 'deptLeader' && s.approver.field === field);
64
+ const operators = {
65
+ gt: '大于',
66
+ gte: '大于等于',
67
+ lt: '小于',
68
+ lte: '小于等于',
69
+ eq: '等于',
70
+ ne: '不等于',
71
+ in: '属于',
72
+ };
73
+ const reasons = [
74
+ ...steps.map((s) => `“${s.title}”步骤根据“${b}”找到对应部门的负责人`),
75
+ ...conditions.map((n) => `当“${b}”${operators[n.cond.op]} ${JSON.stringify(n.cond.value)} 时,加入${n.steps.map((s) => `“${s.title}”`).join('、')}步骤`),
76
+ ];
77
+ return result(steps.length && conditions.length ? '选人和分支依据' : steps.length ? '确定审批人' : '判断审批分支', '审批路由依赖', `${reasons.join(';')}。`, '字段值会影响审批人或需要经过的审批步骤;修改字段含义时,应一起检查这些路由配置。', [
78
+ ...steps.map((s) => `${s.title}: approver.deptLeader(${JSON.stringify(field)})`),
79
+ ...conditions.map((n) => `when(${n.cond.op}(${JSON.stringify(field)}, ${JSON.stringify(n.cond.value)}))`),
80
+ ]);
81
+ }
82
+ if (as === '事件载荷')
83
+ return result('携带字段', '事件数据依赖', `“${a}”的默认事件载荷按同名字段从业务对象取“${b}”。`, '订阅方可以用事件中的这份数据处理后续业务;字段变更需要检查事件及订阅方的数据约定。箭头指向载荷的数据来源。', [`事件 ${from.id} 的同名字段 ${field} ← ${to.id}`]);
84
+ if (as === '查询')
85
+ return result('查询使用字段', '查询字段线索', `抽取器在“${a}”的 SQL 或查询构造函数文本中匹配到了“${b}”的字段名。`, '提供检查查询依赖的线索。这里基于字段名文本匹配,可能匹配到注释或同名内容;实际筛选、分组或计算用途请核对查询声明。', [`${from.id}: SQL / build 文本匹配 ${field}`]);
86
+ if (as === '出站映射')
87
+ return result('映射外发字段', '外部同步数据', `“${a}”的 fields 声明包含“${b}”对应的字段名;抽取器将它关联到发出事件的聚合中同名字段。`, '用于核对外发字段范围和字段变更的同步影响;具体取值及转换由事件载荷与 map 函数决定。', [`${from.id}.fields 包含 ${field}`, `同名聚合字段:${to.id}`]);
88
+ if (as.startsWith('约束 ')) {
89
+ const rule = as.slice(3);
90
+ const purpose = rule.startsWith('ceiling(')
91
+ ? `用于校验“${b}”是否超出调用人的审批额度。`
92
+ : rule.startsWith('sod.notSameAs(')
93
+ ? `用于校验调用人与“${b}”记录的人不同,避免同一人完成需要分离的职责。`
94
+ : `该约束读取“${b}”执行命令前的业务检查;字段变更时应一起复核约束。`;
95
+ return result('执行约束检查', '命令校验依赖', `“${a}”声明的约束 ${rule} 使用“${b}”。`, purpose, [
96
+ `${from.id}.constraints → ${rule}`,
97
+ `约束 fields 包含 ${field}`,
98
+ ]);
99
+ }
100
+ return result('使用字段', '字段依赖', `“${a}”声明使用“${b}”。`, '用于追踪字段变更的影响范围。', [
101
+ as || edge.kind,
102
+ ]);
103
+ }
104
+ case 'cancels':
105
+ return result('取消审批', '流程取消', `“${b}”在 cancelOn 中声明了“${a}”事件。`, '事件发生后取消该单据仍在进行的审批;不会启动新流程,也不替代释放资源等业务反应。', [`${to.id}.cancelOn 包含 ${from.id}`], to);
106
+ case 'triggers': {
107
+ const label = to.kind === 'approval' ? '启动审批' : to.kind === 'outbound' ? '触发外部同步' : '触发反应';
108
+ return result(label, '事件触发', `“${b}”订阅“${a}”作为${to.kind === 'approval' ? '启动' : '触发'}事件。`, to.kind === 'approval'
109
+ ? '事件发生后启动对应审批流程,生成流程中的审批任务。'
110
+ : to.kind === 'outbound'
111
+ ? '事件发生后由 worker 处理外部推送,失败按投递策略重试。'
112
+ : `事件发生后执行订阅的反应${to.attrs.tx === 'same' ? ',该反应与原命令处于同一事务' : ';执行方式由反应的事务声明决定'}。`, [`${to.id}.${to.kind === 'approval' ? 'start' : 'event'} → ${from.id}`], to);
113
+ }
114
+ case 'targets': {
115
+ if (to.kind === 'connector')
116
+ return result('调用外部系统', '连接器调用', `“${a}”声明通过“${b}”执行操作 ${labels[`operation:${from.attrs.operation}`] ?? from.attrs.operation}。`, '由连接器统一处理地址与字段白名单、密钥、限流和超时,让外部同步保持可管控。', [`${from.id}.connector → ${to.id}`, `operation: ${from.attrs.operation}`]);
117
+ if (from.kind === 'outbound')
118
+ return result('成功后回写', '同步成功回调', `“${a}”的 onSuccess 绑定“${b}”。`, '外部操作成功后调用该命令,将返回结果按映射写回业务;失败时不执行成功回写。', [`${from.id}.onSuccess → ${to.id}`]);
119
+ if (from.kind === 'inbound')
120
+ return result('回调转为命令', '外部回调', `“${a}”将接收的外部回调映射到“${b}”。`, '外部回调通过验签和数据校验后,使用业务命令处理,统一遵守状态和业务约束。', [`${from.id}.command → ${to.id}`]);
121
+ const resources = [
122
+ edge.acquires ? `获取“${resource(edge.acquires)}”` : '',
123
+ edge.releases ? `释放“${resource(edge.releases)}”` : '',
124
+ ].filter(Boolean);
125
+ return result('调用业务命令', '反应调用', `“${a}”把事件映射后交给“${b}”处理。`, `将后续业务操作交给受约束的命令执行。${resources.length ? `此声明还标记${resources.join('、')},用于检查资源获取与释放是否成对。` : ''}`, [`${from.id}.command → ${to.id}`, ...resources]);
126
+ }
127
+ case 'binds': {
128
+ if (from.kind === 'route')
129
+ return result('定义审批步骤', '流程配置', `“${a}”与“${b}”使用同一流程标识 ${from.attrs.process}。`, '这份路由定义流程里的审批步骤、选人方式和条件分支,供代码内审批引擎使用。', [`process: ${from.attrs.process}`]);
130
+ const approved = from.attrs.onApproved === to.id;
131
+ const rejected = from.attrs.onRejected === to.id;
132
+ const outcome = approved && rejected ? '结束' : approved ? '通过' : '驳回';
133
+ return result(`${outcome}后执行`, '审批结论回写', `“${a}”${outcome}时绑定执行“${b}”。`, '将审批结论交给业务命令推进单据状态,并统一执行权限和业务约束。', [
134
+ approved ? `${from.id}.onApproved → ${to.id}` : '',
135
+ rejected ? `${from.id}.onRejected → ${to.id}` : '',
136
+ ].filter(Boolean));
137
+ }
138
+ case 'calls':
139
+ return result('允许调用命令', '任务调用范围', `“${a}”在 calls 中声明可以调用“${b}”。`, '明确任务能执行哪些业务写入,并支持影响分析。每次任务是否实际调用该命令,取决于 run 中的业务逻辑。', [`${from.id}.calls → ${to.id}`]);
140
+ }
141
+ }
@@ -0,0 +1,62 @@
1
+ // 轻量、可解释的名称检索;不调用模型,不把关键词匹配描述成“理解任意需求”。
2
+ // 中文用相邻双字作为线索,让“采购单取消”能匹配“取消采购单”,同时返回命中依据。
3
+ import { kindLabels, nodeLabel } from './graph-model.js';
4
+ const normalize = (value) => value.normalize('NFKC').toLowerCase().trim();
5
+ function searchTerms(query) {
6
+ const terms = query.match(/[a-z0-9_$.:/-]+/g) ?? [];
7
+ for (const segment of query.match(/[\p{Script=Han}]+/gu) ?? []) {
8
+ const chars = [...segment];
9
+ if (chars.length === 1)
10
+ terms.push(segment);
11
+ else
12
+ for (let i = 0; i < chars.length - 1; i++)
13
+ terms.push(chars[i] + chars[i + 1]);
14
+ }
15
+ return [...new Set(terms)];
16
+ }
17
+ function matchNode(node, query, labels, descriptions) {
18
+ const normalized = normalize(query);
19
+ if (!normalized)
20
+ return;
21
+ const terms = searchTerms(normalized);
22
+ const ownerId = ['field', 'state', 'command', 'capability', 'entry'].includes(node.kind)
23
+ ? node.id.split('.')[0]
24
+ : undefined;
25
+ const fields = [
26
+ ['key', node.key, 18],
27
+ ['id', node.id, 16],
28
+ ['label', nodeLabel(node, labels), 12],
29
+ ['description', descriptions[node.key] ?? String(node.attrs.description ?? ''), 10],
30
+ ['owner', ownerId ? (labels[`aggregate:${ownerId}`] ?? ownerId) : '', 2],
31
+ ];
32
+ const matches = [];
33
+ let score = 0;
34
+ for (const [field, value, weight] of fields) {
35
+ const text = normalize(value);
36
+ if (!text)
37
+ continue;
38
+ const exact = text === normalized;
39
+ const whole = text.includes(normalized);
40
+ const found = terms.filter((term) => text.includes(term));
41
+ if (!exact && !whole && found.length === 0)
42
+ continue;
43
+ const points = exact ? weight * 100 : whole ? weight * 30 : weight * found.length;
44
+ score = Math.max(score, points);
45
+ matches.push({ field, value, terms: exact || whole ? [query.trim()] : found });
46
+ }
47
+ if (!matches.length)
48
+ return;
49
+ return { key: node.key, id: node.id, kind: node.kind, label: nodeLabel(node, labels), score, matches };
50
+ }
51
+ /** 排序稳定,结果携带命中字段;多个候选不静默合成一个“正确需求”。 */
52
+ export function searchGraphNodes(model, query, labels = {}, options = {}) {
53
+ return model.nodes
54
+ .filter((node) => !options.kind || node.kind === options.kind)
55
+ .map((node) => matchNode(node, query, labels, options.descriptions ?? {}))
56
+ .filter((hit) => !!hit)
57
+ .sort((a, b) => b.score - a.score || a.key.localeCompare(b.key));
58
+ }
59
+ /** 画布的筛选与搜索下拉共用同一匹配规则,保留按节点类型查找的用法。 */
60
+ export function matchesNode(node, query, labels) {
61
+ return !!matchNode(node, query, labels, {}) || normalize(kindLabels[node.kind]).includes(normalize(query));
62
+ }
@@ -0,0 +1,197 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="theme-color" content="#f6f7f3" />
7
+ <title>Cage Atlas · 本体图谱</title>
8
+ <link
9
+ rel="icon"
10
+ href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' rx='10' fill='%232d665a'/%3E%3Cpath d='M11 12h18v16H11zm0 8h18m-9-8v16' stroke='%23fff' fill='none' stroke-width='2'/%3E%3C/svg%3E"
11
+ />
12
+ <link rel="stylesheet" href="/style.css" />
13
+ </head>
14
+ <body>
15
+ <div class="app-shell">
16
+ <header class="topbar">
17
+ <a class="brand" href="/" aria-label="Cage Atlas 首页"
18
+ ><span class="brand-mark"><i></i><i></i><i></i><i></i></span
19
+ ><strong>CAGE<span>ATLAS</span></strong></a
20
+ >
21
+ <div class="breadcrumb">
22
+ <span>开发者工作台</span><b>/</b><span id="project-name">正在读取项目</span>
23
+ </div>
24
+ <span class="local-badge"><i></i>本地声明</span>
25
+ <button
26
+ class="icon-button mobile-filters"
27
+ id="toggle-sidebar"
28
+ aria-label="展开筛选"
29
+ aria-expanded="false"
30
+ >
31
+ ☰
32
+ </button>
33
+ </header>
34
+
35
+ <aside class="sidebar" id="sidebar">
36
+ <div class="section-label">项目工作区 <span>01</span></div>
37
+ <div class="nav-active"><span class="nav-symbol">◇</span> 本体图谱 <span class="nav-dot"></span></div>
38
+ <div class="sidebar-heading">业务对象 <span id="aggregate-total"></span></div>
39
+ <div id="aggregate-list" class="aggregate-list"></div>
40
+ <div class="sidebar-heading" id="types-heading">
41
+ 节点类型 <button id="reset-types" class="text-button">重置</button>
42
+ </div>
43
+ <div id="type-filters" class="type-filters"></div>
44
+ <div id="structure-note" class="structure-note">
45
+ <strong>一张卡片,一个业务对象</strong>
46
+ <p>字段收在卡片内,引用从字段连出。点击字段可查看业务用途。</p>
47
+ <small>* 必填 ↗ 引用字段</small>
48
+ </div>
49
+ <label class="system-toggle"
50
+ ><input id="system-toggle" type="checkbox" /><span>包含系统组织模型</span></label
51
+ >
52
+ <div class="sidebar-note">
53
+ <span class="note-icon">⌘</span>
54
+ <div>
55
+ <strong>声明即地图</strong>
56
+ <p>修改 domain 后重新抽取,<br />让关系与代码保持同步。</p>
57
+ </div>
58
+ </div>
59
+ <a class="sidebar-footer" href="/api/graph" target="_blank" rel="noreferrer"
60
+ >查看原始图谱数据 <span>↗</span></a
61
+ >
62
+ </aside>
63
+
64
+ <main class="main">
65
+ <section class="page-heading">
66
+ <div>
67
+ <div class="eyebrow">业务关系探索</div>
68
+ <h1>本体图谱<span class="version-tag">v0.1</span></h1>
69
+ <p>从声明出发,看见业务如何连接。</p>
70
+ </div>
71
+ <div class="heading-actions">
72
+ <button id="download" class="button secondary"><span>↓</span> 导出 JSON</button
73
+ ><button id="refresh" class="button primary"><span class="refresh-icon">↻</span> 重新抽取</button>
74
+ </div>
75
+ </section>
76
+ <div id="notice" class="notice" role="status" hidden></div>
77
+ <section class="overview" aria-label="图谱统计">
78
+ <div class="overview-item">
79
+ <span class="metric-icon aggregate-icon">◇</span>
80
+ <div><small>业务对象</small><strong id="metric-aggregates">—</strong></div>
81
+ </div>
82
+ <div class="overview-item">
83
+ <span class="metric-icon node-icon">⠿</span>
84
+ <div><small>声明节点</small><strong id="metric-nodes">—</strong></div>
85
+ </div>
86
+ <div class="overview-item">
87
+ <span class="metric-icon edge-icon">↗</span>
88
+ <div><small>声明关系</small><strong id="metric-edges">—</strong></div>
89
+ </div>
90
+ <div class="overview-health">
91
+ <span id="health-dot" class="health-dot"></span>
92
+ <div><strong id="health">正在抽取声明</strong><small id="updated">稍候即可探索图谱</small></div>
93
+ <button id="show-findings" class="text-button" hidden>查看问题</button>
94
+ </div>
95
+ </section>
96
+
97
+ <section class="explorer" aria-label="交互式本体图谱">
98
+ <div class="graph-panel">
99
+ <div class="graph-toolbar">
100
+ <div class="tabs" role="group" aria-label="图谱视图">
101
+ <button data-mode="structure" class="active" aria-pressed="true">对象与字段</button
102
+ ><button data-mode="business" aria-pressed="false">业务流程</button
103
+ ><button data-mode="all" aria-pressed="false">完整本体</button>
104
+ </div>
105
+ <div class="search-wrap">
106
+ <span aria-hidden="true">⌕</span
107
+ ><input
108
+ id="search"
109
+ type="search"
110
+ placeholder="搜索中文名称或代码标识"
111
+ aria-label="搜索节点"
112
+ autocomplete="off"
113
+ /><kbd>/</kbd>
114
+ <div id="search-results" class="search-results" hidden></div>
115
+ </div>
116
+ </div>
117
+ <div class="graph-subbar">
118
+ <span
119
+ ><i class="tiny-dot"></i><strong id="view-title">全部业务</strong
120
+ ><span id="view-count"></span></span
121
+ ><button id="exit-focus" class="text-button" hidden>退出聚焦 ×</button
122
+ ><span class="drag-hint">点击连线看解释 · 拖拽节点 · 滚轮缩放</span>
123
+ </div>
124
+ <div class="relation-guide">
125
+ <span>↗</span><span id="relation-guide">箭头按线上的文字阅读;点击连线查看原因和用途。</span>
126
+ </div>
127
+ <div id="canvas" class="canvas">
128
+ <svg id="graph" role="group" aria-label="本体节点与依赖关系" xmlns="http://www.w3.org/2000/svg">
129
+ <defs>
130
+ <marker
131
+ id="arrow"
132
+ viewBox="0 0 10 10"
133
+ refX="9"
134
+ refY="5"
135
+ markerWidth="5"
136
+ markerHeight="5"
137
+ orient="auto-start-reverse"
138
+ >
139
+ <path d="M0 0L10 5L0 10z" fill="#a4b4ad" />
140
+ </marker>
141
+ <marker
142
+ id="arrow-active"
143
+ viewBox="0 0 10 10"
144
+ refX="9"
145
+ refY="5"
146
+ markerWidth="5"
147
+ markerHeight="5"
148
+ orient="auto-start-reverse"
149
+ >
150
+ <path d="M0 0L10 5L0 10z" fill="#357c6c" />
151
+ </marker>
152
+ </defs>
153
+ <g id="viewport">
154
+ <g id="edges"></g>
155
+ <g id="nodes"></g>
156
+ </g>
157
+ </svg>
158
+ <div id="empty" class="empty-state" hidden>
159
+ <span>◇</span><strong>当前筛选下没有节点</strong>
160
+ <p>调整左侧类型,或恢复默认业务视图。</p>
161
+ <button id="reset-view" class="button secondary">恢复视图</button>
162
+ </div>
163
+ <div id="loading" class="loading-state">
164
+ <span class="loader"></span><strong>正在展开业务地图</strong>
165
+ </div>
166
+ <div class="canvas-signature">CAGE <span>/</span> 业务声明关系图</div>
167
+ <div class="zoom-controls" aria-label="画布操作">
168
+ <button id="zoom-out" aria-label="缩小">−</button><span id="zoom-level">100%</span
169
+ ><button id="zoom-in" aria-label="放大">+</button><i></i
170
+ ><button id="fit" aria-label="适应画布" title="适应画布">⛶</button
171
+ ><button id="relayout" aria-label="重置布局" title="重置布局">↺</button>
172
+ </div>
173
+ </div>
174
+ <footer class="graph-footer">
175
+ <div id="edge-legend">
176
+ <span class="legend-line"></span> 声明关系 <span class="legend-line dashed"></span> 视图归属
177
+ </div>
178
+ <span id="canvas-status">读取声明中</span>
179
+ </footer>
180
+ </div>
181
+ <aside id="inspector" class="inspector" aria-label="节点与关系详情" aria-live="polite"></aside>
182
+ </section>
183
+ <footer class="page-footer">
184
+ <span id="domain-path">packages/domain/src/index.ts</span><span>只读探索 · 不连接业务数据库</span>
185
+ </footer>
186
+ </main>
187
+ </div>
188
+ <dialog id="findings-dialog">
189
+ <div class="dialog-heading">
190
+ <h2>声明检查</h2>
191
+ <button id="close-findings" class="icon-button" aria-label="关闭检查结果">×</button>
192
+ </div>
193
+ <div id="findings-list"></div>
194
+ </dialog>
195
+ <script type="module" src="/app.js"></script>
196
+ </body>
197
+ </html>
@@ -0,0 +1,83 @@
1
+ import { kindLabels } from './graph-model.js';
2
+ export { nodeLabel, projectGraph, neighborhood, edgeLabels } from './graph-model.js';
3
+ export { matchesNode } from './graph-search.js';
4
+ export const kinds = {
5
+ aggregate: { label: kindLabels.aggregate, color: '#197c72', short: '表', column: 0 },
6
+ command: { label: kindLabels.command, color: '#547db2', short: '令', column: 1 },
7
+ event: { label: kindLabels.event, color: '#c08c36', short: '事', column: 2 },
8
+ reaction: { label: kindLabels.reaction, color: '#aa7288', short: '应', column: 3 },
9
+ query: { label: kindLabels.query, color: '#648c5b', short: '查', column: 1 },
10
+ approval: { label: kindLabels.approval, color: '#9c739f', short: '审', column: 3 },
11
+ route: { label: kindLabels.route, color: '#9c739f', short: '路', column: 3 },
12
+ job: { label: kindLabels.job, color: '#7f82a0', short: '时', column: 0 },
13
+ connector: { label: kindLabels.connector, color: '#b57452', short: '连', column: 4 },
14
+ outbound: { label: kindLabels.outbound, color: '#b57452', short: '出', column: 3 },
15
+ inbound: { label: kindLabels.inbound, color: '#b57452', short: '入', column: 4 },
16
+ field: { label: kindLabels.field, color: '#67998f', short: '字', column: 1 },
17
+ state: { label: kindLabels.state, color: '#898b69', short: '态', column: 2 },
18
+ capability: { label: kindLabels.capability, color: '#81949f', short: '权', column: 1 },
19
+ entry: { label: kindLabels.entry, color: '#89938e', short: '+', column: 0 },
20
+ };
21
+ export const isSystem = (node) => /^(User|Role|Dept)(\.|$)/.test(node.id);
22
+ export const NODE_WIDTH = 190;
23
+ export const NODE_HEIGHT = 56;
24
+ /** 按类型分组的平衡列 + 邻接排序。相同声明每次打开位置相同,没有持续运动的力导向
25
+ * 动画;读长名称、比较刷新前后的关系时不会被随机布局打断。 */
26
+ export function layoutGraph(nodes, edges, focusKey) {
27
+ const positions = new Map();
28
+ // 小范围聚焦按关系方向排布:依赖方在左、当前节点居中、目标在右。
29
+ // 大图继续使用平衡列,避免高连接度聚合把所有邻居挤成一条长列。
30
+ if (focusKey && nodes.some((n) => n.key === focusKey) && nodes.length <= 12) {
31
+ positions.set(focusKey, { x: 330, y: 0 });
32
+ const incoming = new Set(edges.filter((e) => e.to === focusKey).map((e) => e.from));
33
+ const neighbors = nodes.filter((n) => n.key !== focusKey);
34
+ for (const [column, items] of [
35
+ neighbors.filter((n) => incoming.has(n.key)),
36
+ neighbors.filter((n) => !incoming.has(n.key)),
37
+ ].entries()) {
38
+ items.forEach((n, row) => positions.set(n.key, { x: column * 660, y: (row - (items.length - 1) / 2) * 105 }));
39
+ }
40
+ return positions;
41
+ }
42
+ // 命令通常远多于其他节点;强制“一类一列”会形成极长列,适应画布后字体
43
+ // 小到无法阅读。保持语义排序,再均衡分列,让概览尽量利用二维空间。
44
+ const ordered = [...nodes].sort((a, b) => kinds[a.kind].column - kinds[b.kind].column || a.id.localeCompare(b.id));
45
+ const columnCount = Math.min(4, Math.max(1, Math.ceil(Math.sqrt(nodes.length / 1.6))));
46
+ const perColumn = Math.ceil(nodes.length / columnCount);
47
+ const columns = Array.from({ length: columnCount }, (_, i) => ordered.slice(i * perColumn, (i + 1) * perColumn));
48
+ const place = () => columns.forEach((items, column) => items.forEach((n, row) => {
49
+ positions.set(n.key, { x: column * 330, y: (row - (items.length - 1) / 2) * 105 });
50
+ }));
51
+ place();
52
+ // 只做排序,节点仍各占一行;邻居平均纵坐标让同一业务链尽量接近。
53
+ for (let pass = 0; pass < 4; pass++) {
54
+ for (const items of columns) {
55
+ const score = (node) => {
56
+ const ys = edges
57
+ .filter((e) => e.from === node.key || e.to === node.key)
58
+ .map((e) => positions.get(e.from === node.key ? e.to : e.from)?.y)
59
+ .filter((y) => y !== undefined);
60
+ return ys.length ? ys.reduce((a, b) => a + b, 0) / ys.length : 0;
61
+ };
62
+ items.sort((a, b) => score(a) - score(b) || a.id.localeCompare(b.id));
63
+ place();
64
+ }
65
+ }
66
+ return positions;
67
+ }
68
+ export function edgePath(from, to, self) {
69
+ if (self)
70
+ return `M${from.x + NODE_WIDTH - 25},${from.y} C${from.x + NODE_WIDTH + 50},${from.y - 65} ${from.x + NODE_WIDTH + 50},${from.y + 100} ${from.x + NODE_WIDTH - 25},${from.y + NODE_HEIGHT}`;
71
+ // 同列关系从右侧绕行,给标签留出空间,也避免穿过中间节点。
72
+ if (from.x === to.x) {
73
+ const x = from.x + NODE_WIDTH;
74
+ const y1 = from.y + NODE_HEIGHT / 2, y2 = to.y + NODE_HEIGHT / 2;
75
+ const bend = 110 + Math.abs(y2 - y1) * 0.12;
76
+ return `M${x},${y1} C${x + bend},${y1} ${x + bend},${y2} ${x},${y2}`;
77
+ }
78
+ const forward = to.x >= from.x;
79
+ const x1 = from.x + (forward ? NODE_WIDTH : 0), x2 = to.x + (forward ? 0 : NODE_WIDTH);
80
+ const y1 = from.y + NODE_HEIGHT / 2, y2 = to.y + NODE_HEIGHT / 2;
81
+ const bend = Math.max(60, Math.abs(x2 - x1) / 2) * (forward ? 1 : -1);
82
+ return `M${x1},${y1} C${x1 + bend},${y1} ${x2 - bend},${y2} ${x2},${y2}`;
83
+ }
@@ -0,0 +1,2 @@
1
+ // Web 与 AI 上下文共用关系解释,保持证据和业务含义一致。
2
+ export { explainEdge } from './graph-relations.js';