@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,1103 @@
1
+ import { kinds, nodeLabel, projectGraph, neighborhood, isSystem, layoutGraph, edgePath, NODE_WIDTH, NODE_HEIGHT, } from './model.js';
2
+ import { explainEdge } from './relations.js';
3
+ import { searchGraphNodes as rankGraphNodes } from './graph-search.js';
4
+ import { tableCards, tableOwner, layoutTables, tableEdgePath, fieldType, TABLE_WIDTH, TABLE_HEADER, TABLE_ROW, } from './structure.js';
5
+ // 页面不引入图形框架或远程 CDN。百级声明图使用 SVG,即可保留清晰缩放、
6
+ // 键盘选择和可检查的节点语义;所有数据都由本地 CLI 抽取服务提供。
7
+ const element = (id) => document.getElementById(id);
8
+ const svg = document.getElementById('graph');
9
+ const viewport = document.getElementById('viewport');
10
+ const edgeLayer = document.getElementById('edges');
11
+ const nodeLayer = document.getElementById('nodes');
12
+ const ns = 'http://www.w3.org/2000/svg';
13
+ let snapshot;
14
+ let model = { nodes: [], edges: [] };
15
+ let visibleNodes = [];
16
+ let visibleEdges = [];
17
+ let positions = new Map();
18
+ let selected = null;
19
+ let selectedEdge = null;
20
+ let emphasizeSelection = false;
21
+ let aggregate = null;
22
+ let focus = null;
23
+ let mode = 'structure';
24
+ let cards = [];
25
+ let visibleCards = [];
26
+ let searchTarget = null;
27
+ let showSystem = false;
28
+ let camera = { x: 0, y: 0, scale: 1 };
29
+ const businessKinds = Object.keys(kinds).filter((k) => !['field', 'state', 'capability', 'entry'].includes(k));
30
+ let enabled = new Set(businessKinds);
31
+ const nodeElements = new Map();
32
+ const edgeElements = new Map();
33
+ function html(tag, className = '', text = '') {
34
+ const node = document.createElement(tag);
35
+ node.className = className;
36
+ node.textContent = text;
37
+ return node;
38
+ }
39
+ function vector(tag, attrs = {}, text = '') {
40
+ const node = document.createElementNS(ns, tag);
41
+ for (const [key, value] of Object.entries(attrs))
42
+ node.setAttribute(key, String(value));
43
+ node.textContent = text;
44
+ return node;
45
+ }
46
+ const byKey = (key) => model.nodes.find((n) => n.key === key);
47
+ const displayName = (node) => nodeLabel(node, snapshot.presentation.labels);
48
+ // 中文字符约占两个半角字宽,不能沿用英文按字符数截断,否则标题会溢出卡片。
49
+ function shortName(text, width = 23) {
50
+ let result = '', used = 0;
51
+ for (const char of text) {
52
+ used += /[\u0000-\u00ff]/.test(char) ? 1 : 2;
53
+ if (used > width)
54
+ return `${result}…`;
55
+ result += char;
56
+ }
57
+ return result;
58
+ }
59
+ const named = (id, kind) => {
60
+ const node = model.nodes.find((n) => n.id === id && (!kind || n.kind === kind));
61
+ return node
62
+ ? displayName(node)
63
+ : ((kind ? snapshot.presentation.labels[`${kind}:${id}`] : undefined) ?? String(id));
64
+ };
65
+ function notify(message) {
66
+ element('notice').textContent = message;
67
+ element('notice').hidden = !message;
68
+ }
69
+ async function load(refresh = false) {
70
+ const button = element('refresh');
71
+ button.disabled = true;
72
+ button.textContent = refresh ? '正在重新抽取…' : '读取中…';
73
+ notify('');
74
+ try {
75
+ const response = await fetch(refresh ? '/api/refresh' : '/api/graph', {
76
+ method: refresh ? 'POST' : 'GET',
77
+ });
78
+ const data = await response.json();
79
+ if (!response.ok)
80
+ throw new Error(data.error);
81
+ snapshot = data;
82
+ model = projectGraph(snapshot.graph);
83
+ cards = tableCards(model);
84
+ // 边的序号可能因重新抽取而变化,清除旧边,避免把解释套到另一条关系上。
85
+ selectedEdge = null;
86
+ // 刷新保留用户筛选与有效选中项,已删除节点不继续显示旧详情。
87
+ if (selected && !model.nodes.some((n) => n.key === selected))
88
+ selected = null;
89
+ if (focus && !model.nodes.some((n) => n.key === focus))
90
+ focus = null;
91
+ if (aggregate && !model.nodes.some((n) => n.kind === 'aggregate' && n.id === aggregate))
92
+ aggregate = null;
93
+ if (!selected) {
94
+ const aggregates = model.nodes.filter((n) => n.kind === 'aggregate' && !isSystem(n));
95
+ const commands = (a) => model.nodes.filter((n) => n.kind === 'command' && n.id.startsWith(`${a.id}.`)).length;
96
+ selected = aggregates.sort((a, b) => commands(b) - commands(a))[0]?.key ?? model.nodes[0]?.key ?? null;
97
+ }
98
+ renderSummary();
99
+ renderFilters();
100
+ renderGraph();
101
+ renderInspector();
102
+ renderFindings();
103
+ }
104
+ catch (error) {
105
+ // 抽取失败时保留上次成功的视图,并明确告诉用户它是旧数据。
106
+ notify(`${snapshot ? '重新抽取失败,当前保留上次成功的图谱。' : '图谱加载失败。'}\n${error.message}`);
107
+ if (!snapshot) {
108
+ element('health').textContent = '尚未取得图谱';
109
+ element('updated').textContent = '修复问题后点击重新抽取';
110
+ }
111
+ }
112
+ finally {
113
+ button.disabled = false;
114
+ button.textContent = '↻ 重新抽取';
115
+ element('loading').hidden = true;
116
+ }
117
+ }
118
+ function renderSummary() {
119
+ element('project-name').textContent = snapshot.presentation.title;
120
+ element('project-name').title = snapshot.project;
121
+ document.title = `${snapshot.presentation.title} · Cage 本体图谱`;
122
+ element('domain-path').textContent = snapshot.domain;
123
+ element('metric-aggregates').textContent = String(snapshot.graph.nodes.filter((n) => n.kind === 'aggregate').length);
124
+ element('metric-nodes').textContent = String(snapshot.graph.nodes.length);
125
+ element('metric-edges').textContent = String(snapshot.graph.edges.length);
126
+ const errors = snapshot.findings.filter((f) => f.severity !== 'warning').length;
127
+ const warnings = snapshot.findings.length - errors;
128
+ element('health').textContent = errors
129
+ ? `${errors} 个声明问题`
130
+ : warnings
131
+ ? `${warnings} 个检查提醒`
132
+ : '声明与图规则通过';
133
+ element('health-dot').classList.toggle('warning', snapshot.findings.length > 0);
134
+ element('show-findings').hidden = !snapshot.findings.length;
135
+ const time = new Date(snapshot.generatedAt).toLocaleTimeString('zh-CN', { hour12: false });
136
+ element('updated').textContent = `最近抽取 ${time}`;
137
+ }
138
+ function renderFilters() {
139
+ element('types-heading').hidden = mode === 'structure';
140
+ element('type-filters').hidden = mode === 'structure';
141
+ element('structure-note').hidden = mode !== 'structure';
142
+ const aggregates = model.nodes.filter((n) => n.kind === 'aggregate' && (showSystem || !isSystem(n)));
143
+ element('aggregate-total').textContent = String(aggregates.length);
144
+ const list = element('aggregate-list');
145
+ list.replaceChildren();
146
+ const addAggregate = (id, label, count) => {
147
+ const button = html('button', `aggregate-button${aggregate === id ? ' active' : ''}`);
148
+ button.setAttribute('aria-pressed', String(aggregate === id));
149
+ button.append(html('span', '', id ? '◇' : '⊞'), html('span', '', label), html('b', '', String(count)));
150
+ button.onclick = () => {
151
+ aggregate = id;
152
+ focus = null;
153
+ renderFilters();
154
+ renderGraph();
155
+ };
156
+ list.append(button);
157
+ };
158
+ addAggregate(null, '全部业务', aggregates.length);
159
+ for (const a of aggregates)
160
+ addAggregate(a.id, displayName(a), model.nodes.filter((n) => n.id.startsWith(`${a.id}.`)).length);
161
+ const filters = element('type-filters');
162
+ filters.replaceChildren();
163
+ for (const [kind, meta] of Object.entries(kinds)) {
164
+ if (kind === 'entry')
165
+ continue;
166
+ const count = model.nodes.filter((n) => n.kind === kind && (showSystem || !isSystem(n))).length;
167
+ if (!count)
168
+ continue;
169
+ const label = html('label', 'type-filter');
170
+ const input = html('input');
171
+ input.type = 'checkbox';
172
+ input.checked = enabled.has(kind);
173
+ input.setAttribute('aria-label', `显示${meta.label}`);
174
+ input.onchange = () => {
175
+ input.checked ? enabled.add(kind) : enabled.delete(kind);
176
+ if (kind === 'state')
177
+ input.checked ? enabled.add('entry') : enabled.delete('entry');
178
+ renderGraph();
179
+ };
180
+ const dot = html('span', 'type-dot');
181
+ dot.style.backgroundColor = meta.color;
182
+ label.append(input, dot, html('span', '', meta.label), html('small', '', String(count)));
183
+ filters.append(label);
184
+ }
185
+ element('system-toggle').checked = showSystem;
186
+ document.querySelectorAll('[data-mode]').forEach((button) => {
187
+ button.classList.toggle('active', button.dataset.mode === mode);
188
+ button.setAttribute('aria-pressed', String(button.dataset.mode === mode));
189
+ });
190
+ }
191
+ /** 选一个聚合时,先纳入它自己的声明,再沿真实关系扩展业务链。
192
+ * 不沿其他聚合的归属边扩散,避免选采购单却展开所有组织管理命令。 */
193
+ function aggregateNodes(id) {
194
+ const keys = new Set(model.nodes.filter((n) => n.id === id || n.id.startsWith(`${id}.`)).map((n) => n.key));
195
+ for (let hop = 0; hop < 3; hop++) {
196
+ const previous = new Set(keys);
197
+ for (const edge of model.edges.filter((e) => !e.derived)) {
198
+ if (previous.has(edge.from))
199
+ keys.add(edge.to);
200
+ if (previous.has(edge.to) && ['uses', 'reads', 'queries', 'triggers'].includes(edge.kind))
201
+ keys.add(edge.from);
202
+ }
203
+ }
204
+ return keys;
205
+ }
206
+ function renderGraph(resetLayout = true) {
207
+ searchTarget = null;
208
+ const scope = aggregate ? aggregateNodes(aggregate) : null;
209
+ const focused = focus ? neighborhood(model, focus) : null;
210
+ visibleNodes = model.nodes.filter((n) => enabled.has(n.kind) &&
211
+ (showSystem || !isSystem(n)) &&
212
+ (!scope || scope.has(n.key)) &&
213
+ (!focused || focused.has(n.key)));
214
+ const visible = new Set(visibleNodes.map((n) => n.key));
215
+ visibleEdges = model.edges.filter((e) => visible.has(e.from) && visible.has(e.to) && (!focus || e.from === focus || e.to === focus));
216
+ if (mode === 'structure') {
217
+ const center = focus ? tableOwner(cards, focus) : cards.find((c) => c.node.id === aggregate);
218
+ const focusedField = focus && byKey(focus).kind === 'field' ? focus : null;
219
+ const refs = model.edges.filter((e) => e.kind === 'refs' &&
220
+ (focusedField
221
+ ? e.from === focusedField
222
+ : !center || e.to === center.node.key || tableOwner(cards, e.from)?.node.key === center.node.key));
223
+ const related = center ? new Set([center.node.key]) : null;
224
+ // 选对象后只展开直接引用它、或被它引用的对象;不沿命令与事件扩散。
225
+ if (center)
226
+ for (const edge of refs) {
227
+ const source = tableOwner(cards, edge.from);
228
+ if (source.node.key === center.node.key)
229
+ related.add(edge.to);
230
+ if (edge.to === center.node.key)
231
+ related.add(source.node.key);
232
+ }
233
+ visibleCards = cards.filter((card) => (showSystem || !isSystem(card.node)) && (!related || related.has(card.node.key)));
234
+ const keys = new Set(visibleCards.flatMap((card) => [card.node.key, ...card.fields.map((f) => f.key)]));
235
+ visibleNodes = visibleCards.map((card) => card.node);
236
+ visibleEdges = refs.filter((edge) => keys.has(edge.from) && keys.has(edge.to));
237
+ }
238
+ if (selectedEdge && !visibleEdges.some((e) => e.key === selectedEdge.key)) {
239
+ selectedEdge = null;
240
+ renderInspector();
241
+ }
242
+ if (resetLayout)
243
+ positions =
244
+ mode === 'structure'
245
+ ? layoutTables(visibleCards, visibleEdges, svg.clientWidth < 680 ? 1 : 2)
246
+ : layoutGraph(visibleNodes, visibleEdges, focus);
247
+ nodeLayer.replaceChildren();
248
+ edgeLayer.replaceChildren();
249
+ nodeElements.clear();
250
+ edgeElements.clear();
251
+ for (const edge of visibleEdges) {
252
+ const explanation = explainEdge(edge, model, snapshot.presentation.labels);
253
+ const group = vector('g', {
254
+ class: `edge-group${edge.derived ? ' derived' : ''}`,
255
+ tabindex: 0,
256
+ role: 'button',
257
+ 'data-key': edge.key,
258
+ 'aria-label': `${displayName(byKey(edge.from))} → ${displayName(byKey(edge.to))}:${explanation.label},查看关系说明`,
259
+ });
260
+ const path = vector('path', {
261
+ class: 'edge',
262
+ 'marker-end': 'url(#arrow)',
263
+ });
264
+ // 透明描边扩大点击范围;文字牌与线属于同一按钮,鼠标和键盘看到同一解释。
265
+ const hit = vector('path', { class: 'edge-hit' });
266
+ const label = vector('g', { class: 'edge-label' });
267
+ const width = explanation.label.length * 14 + 20;
268
+ label.append(vector('rect', { x: -width / 2, y: -12, width, height: 24, rx: 12 }), vector('text', { 'text-anchor': 'middle', y: 4 }, explanation.label));
269
+ group.append(vector('title', {}, `${explanation.why}\n点击查看用途与声明依据`), hit, path, label);
270
+ group.onclick = () => selectEdge(edge);
271
+ group.onkeydown = (event) => {
272
+ if (event.key === 'Enter' || event.key === ' ') {
273
+ event.preventDefault();
274
+ selectEdge(edge);
275
+ }
276
+ };
277
+ edgeLayer.append(group);
278
+ edgeElements.set(edge.key, { group, path, hit, label, width });
279
+ }
280
+ for (const node of visibleNodes) {
281
+ if (mode === 'structure') {
282
+ renderTable(visibleCards.find((card) => card.node.key === node.key));
283
+ continue;
284
+ }
285
+ const meta = kinds[node.kind];
286
+ const g = vector('g', {
287
+ class: 'node',
288
+ tabindex: 0,
289
+ role: 'button',
290
+ 'aria-label': `${meta.label} ${displayName(node)} ${node.id}`,
291
+ 'data-key': node.key,
292
+ });
293
+ g.append(vector('title', {}, `${meta.label} · ${displayName(node)}\n${node.id}`));
294
+ g.append(vector('rect', {
295
+ class: 'selected-ring',
296
+ x: -5,
297
+ y: -5,
298
+ width: NODE_WIDTH + 10,
299
+ height: NODE_HEIGHT + 10,
300
+ rx: 12,
301
+ }));
302
+ g.append(vector('rect', { class: 'node-box', width: NODE_WIDTH, height: NODE_HEIGHT, rx: 8 }));
303
+ g.append(vector('rect', { x: 10, y: 14, width: 27, height: 27, rx: 6, fill: meta.color, 'fill-opacity': 0.1 }));
304
+ g.append(vector('text', { class: 'node-code', x: 23.5, y: 31, 'text-anchor': 'middle', fill: meta.color }, meta.short));
305
+ g.append(vector('text', { class: 'node-title', x: 46, y: 25 }, shortName(displayName(node), 18)));
306
+ g.append(vector('text', { class: 'node-subtitle', x: 46, y: 40 }, shortName(node.id, 23)));
307
+ g.onkeydown = (event) => {
308
+ if (event.key === 'Enter' || event.key === ' ') {
309
+ event.preventDefault();
310
+ selectNode(node.key);
311
+ }
312
+ };
313
+ nodeLayer.append(g);
314
+ nodeElements.set(node.key, g);
315
+ }
316
+ updatePositions();
317
+ highlight();
318
+ element('empty').hidden = visibleNodes.length > 0;
319
+ element('view-title').textContent = focus
320
+ ? `聚焦 · ${displayName(byKey(focus))}`
321
+ : aggregate
322
+ ? named(aggregate, 'aggregate')
323
+ : '全部业务';
324
+ element('view-count').textContent =
325
+ mode === 'structure'
326
+ ? ` / ${visibleCards.length} 个对象 · ${visibleCards.reduce((sum, card) => sum + card.fields.length, 0)} 个字段`
327
+ : ` / ${visibleNodes.length} 个节点`;
328
+ element('exit-focus').hidden = !focus;
329
+ element('canvas-status').textContent = `${visibleEdges.filter((e) => !e.derived).length} 条声明关系`;
330
+ element('relation-guide').textContent =
331
+ mode === 'structure'
332
+ ? '字段收在对象卡片内;点击字段看详情,点击引用线看连接原因。'
333
+ : visibleEdges.length > 18
334
+ ? '概览标注选中节点的关系;悬停或点击其他连线查看说明。'
335
+ : '按“起点 → 关系名称 → 终点”阅读;点击连线查看为什么连接、有什么用。';
336
+ if (mode === 'structure') {
337
+ const hiddenRefs = model.edges.filter((e) => e.kind === 'refs' && tableOwner(visibleCards, e.from) && !tableOwner(visibleCards, e.to)).length;
338
+ element('canvas-status').textContent =
339
+ `${visibleEdges.length} 条对象引用${hiddenRefs ? ` · ${hiddenRefs} 条指向隐藏对象` : ''}`;
340
+ element('edge-legend').textContent = '↗ 外键引用 ⇄ 集合关系(非主表列) * 必填';
341
+ }
342
+ else {
343
+ element('edge-legend').replaceChildren(html('span', 'legend-line'), document.createTextNode(' 声明关系 '), html('span', 'legend-line dashed'), document.createTextNode(' 视图归属'));
344
+ }
345
+ if (resetLayout)
346
+ requestAnimationFrame(fit);
347
+ }
348
+ /** 绿色表头、逐行字段和表名保持在同一 SVG 卡片里。字段是可选择的原始节点,
349
+ * 表头负责拖动卡片,字段点击不会误触画布拖动或把数据结构重新拆散。 */
350
+ function renderTable(card) {
351
+ const { node, fields, height } = card;
352
+ const group = vector('g', {
353
+ class: 'node table-card',
354
+ 'data-key': node.key,
355
+ role: 'group',
356
+ 'aria-label': `业务对象 ${displayName(node)},${fields.length} 个字段`,
357
+ });
358
+ group.append(vector('rect', {
359
+ class: 'table-shadow',
360
+ x: -5,
361
+ y: -5,
362
+ width: TABLE_WIDTH + 10,
363
+ height: height + 10,
364
+ rx: 10,
365
+ }));
366
+ group.append(vector('rect', { class: 'table-body', width: TABLE_WIDTH, height, rx: 6 }));
367
+ const header = vector('g', {
368
+ class: 'table-header',
369
+ role: 'button',
370
+ tabindex: 0,
371
+ 'aria-label': `查看业务对象 ${displayName(node)} ${node.id}`,
372
+ });
373
+ header.append(vector('path', {
374
+ d: `M6,0H${TABLE_WIDTH - 6}Q${TABLE_WIDTH},0 ${TABLE_WIDTH},6V${TABLE_HEADER}H0V6Q0,0 6,0Z`,
375
+ }));
376
+ header.append(vector('text', { x: 16, y: 34, class: 'table-icon' }, '▦'), vector('text', { x: 39, y: 34, class: 'table-name' }, shortName(displayName(node), 18)), vector('text', { x: TABLE_WIDTH - 15, y: 33, 'text-anchor': 'end', class: 'table-id' }, shortName(node.id, 20)));
377
+ header.append(vector('title', {}, `${displayName(node)} · ${node.id}\n拖动表头移动;点击查看业务对象详情`));
378
+ header.onkeydown = (event) => {
379
+ if (event.key === 'Enter' || event.key === ' ') {
380
+ event.preventDefault();
381
+ selectNode(node.key);
382
+ }
383
+ };
384
+ group.append(header);
385
+ for (const [index, field] of fields.entries()) {
386
+ const code = field.id.slice(node.id.length + 1);
387
+ const name = displayName(field);
388
+ const row = vector('g', {
389
+ class: 'table-field',
390
+ transform: `translate(0,${TABLE_HEADER + index * TABLE_ROW})`,
391
+ role: 'button',
392
+ tabindex: 0,
393
+ 'aria-label': `字段 ${name} ${field.id},${fieldType(field)}${field.attrs.virtual ? ',集合关系,不占主表列' : field.attrs.optional ? ',可选' : ',必填'}`,
394
+ });
395
+ row.append(vector('rect', { width: TABLE_WIDTH, height: TABLE_ROW, class: index % 2 ? 'alternate' : '' }));
396
+ row.append(vector('text', { x: 14, y: 24, class: field.attrs.ref ? 'field-ref' : 'field-dot' }, field.attrs.virtual ? '⇄' : field.attrs.ref ? '↗' : '·'));
397
+ row.append(vector('text', { x: 34, y: 24, class: 'field-code' }, shortName(code, 16)));
398
+ if (!field.attrs.optional && !field.attrs.virtual)
399
+ row.append(vector('text', { x: 150, y: 24, class: 'field-required' }, '*'));
400
+ row.append(vector('text', { x: 166, y: 24, class: 'field-name' }, shortName(name, 14)), vector('text', { x: TABLE_WIDTH - 13, y: 24, 'text-anchor': 'end', class: 'field-type' }, fieldType(field)));
401
+ row.append(vector('title', {}, `${name} · ${field.id}\n${fieldType(field)}${field.attrs.virtual ? ' · 集合关系,不占主表列' : field.attrs.optional ? ' · 可选' : ' · 必填'}${field.attrs.ref ? ` · 关联 ${named(field.attrs.ref, 'aggregate')}` : ''}`));
402
+ row.onclick = (event) => {
403
+ event.stopPropagation();
404
+ selectNode(field.key);
405
+ };
406
+ row.onkeydown = (event) => {
407
+ if (event.key === 'Enter' || event.key === ' ') {
408
+ event.preventDefault();
409
+ event.stopPropagation();
410
+ selectNode(field.key);
411
+ }
412
+ };
413
+ group.append(row);
414
+ nodeElements.set(field.key, row);
415
+ }
416
+ const footerY = height - 34;
417
+ group.append(vector('line', { x1: 0, x2: TABLE_WIDTH, y1: footerY, y2: footerY, class: 'table-divider' }));
418
+ const states = model.nodes.filter((n) => n.kind === 'state' && n.id.startsWith(`${node.id}.`)).length;
419
+ group.append(vector('text', { x: 14, y: footerY + 21, class: 'table-footer' }, `${fields.length} 字段 · ${states} 状态`));
420
+ group.append(vector('text', { x: TABLE_WIDTH - 13, y: footerY + 21, 'text-anchor': 'end', class: 'table-sql' }, shortName(String(node.attrs.table), 28)));
421
+ nodeLayer.append(group);
422
+ nodeElements.set(node.key, group);
423
+ }
424
+ const nodeSize = (key) => mode === 'structure'
425
+ ? { width: TABLE_WIDTH, height: tableOwner(visibleCards, key).height }
426
+ : { width: NODE_WIDTH, height: NODE_HEIGHT };
427
+ function updatePositions() {
428
+ for (const n of visibleNodes) {
429
+ const p = positions.get(n.key);
430
+ nodeElements.get(n.key).setAttribute('transform', `translate(${p.x},${p.y})`);
431
+ }
432
+ // 在曲线上选标签落点,优先避开节点和已放置标签。文字始终贴在线上,
433
+ // 不为避让随意挪到空白区域,免得用户认错对应关系。
434
+ const occupied = visibleNodes.map((node) => ({
435
+ x: positions.get(node.key).x - 8,
436
+ y: positions.get(node.key).y - 8,
437
+ width: nodeSize(node.key).width + 16,
438
+ height: nodeSize(node.key).height + 16,
439
+ }));
440
+ for (const e of visibleEdges) {
441
+ const { path, hit, label, width } = edgeElements.get(e.key);
442
+ const d = mode === 'structure'
443
+ ? tableEdgePath(e, visibleCards, positions)
444
+ : edgePath(positions.get(e.from), positions.get(e.to), e.from === e.to);
445
+ path.setAttribute('d', d);
446
+ hit.setAttribute('d', d);
447
+ const length = path.getTotalLength();
448
+ const candidates = [0.5, 0.35, 0.65, 0.23, 0.77].map((t) => {
449
+ const p = path.getPointAtLength(length * t);
450
+ return { x: p.x - width / 2, y: p.y - 12, width, height: 24 };
451
+ });
452
+ const collisions = (a) => occupied.filter((b) => a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y).length;
453
+ candidates.sort((a, b) => collisions(a) - collisions(b));
454
+ const box = candidates[0];
455
+ label.setAttribute('transform', `translate(${box.x + width / 2},${box.y + 12})`);
456
+ occupied.push(box);
457
+ }
458
+ }
459
+ function highlight() {
460
+ const connected = selectedEdge
461
+ ? new Set([selectedEdge.from, selectedEdge.to])
462
+ : emphasizeSelection && selected && nodeElements.has(selected)
463
+ ? neighborhood(model, selected)
464
+ : null;
465
+ for (const [key, g] of nodeElements) {
466
+ if (mode === 'structure') {
467
+ const owner = tableOwner(visibleCards, key);
468
+ const related = selectedEdge
469
+ ? [selectedEdge.from, selectedEdge.to].some((end) => tableOwner(visibleCards, end)?.node.key === owner.node.key)
470
+ : tableOwner(visibleCards, selected ?? '')?.node.key === owner.node.key;
471
+ g.classList.toggle('selected', g.classList.contains('table-card') ? related : selectedEdge ? connected.has(key) : key === selected);
472
+ // 保留其他表和字段的文字对比度,选中态用边框表达,不把整张数据字典褪成背景。
473
+ g.classList.remove('dim');
474
+ g.setAttribute('aria-pressed', String(!selectedEdge && key === selected));
475
+ continue;
476
+ }
477
+ g.classList.toggle('selected', selectedEdge ? connected.has(key) : key === selected);
478
+ g.classList.toggle('dim', !!connected && !connected.has(key));
479
+ g.setAttribute('aria-pressed', String(!selectedEdge && key === selected));
480
+ }
481
+ for (const e of visibleEdges) {
482
+ const active = selectedEdge
483
+ ? e.key === selectedEdge.key
484
+ : e.from === selected ||
485
+ e.to === selected ||
486
+ (mode === 'structure' && tableOwner(cards, e.from)?.node.key === selected);
487
+ const { group, path } = edgeElements.get(e.key);
488
+ group.classList.toggle('active', active);
489
+ group.classList.toggle('chosen', e.key === selectedEdge?.key);
490
+ group.classList.toggle('dim', !!connected && !active);
491
+ group.classList.toggle('show-label', visibleEdges.length <= 18 || active);
492
+ group.setAttribute('aria-pressed', String(e.key === selectedEdge?.key));
493
+ path.setAttribute('marker-end', active ? 'url(#arrow-active)' : 'url(#arrow)');
494
+ }
495
+ }
496
+ function selectNode(key, reveal = false) {
497
+ searchTarget = null;
498
+ selectedEdge = null;
499
+ selected = key;
500
+ emphasizeSelection = true;
501
+ if (mode === 'structure' && !tableOwner(cards, key)) {
502
+ mode = ['state', 'capability', 'entry'].includes(byKey(key).kind) ? 'all' : 'business';
503
+ enabled = new Set(mode === 'all' ? Object.keys(kinds) : businessKinds);
504
+ aggregate = null;
505
+ focus = null;
506
+ renderFilters();
507
+ renderGraph();
508
+ }
509
+ if (reveal && !nodeElements.has(key)) {
510
+ // 搜索覆盖整张图:命中隐藏类型时显式取消作用域并开启该类型,不能点击后无反应。
511
+ const node = byKey(key);
512
+ enabled.add(node.kind);
513
+ showSystem ||= isSystem(node);
514
+ aggregate = null;
515
+ focus = null;
516
+ renderFilters();
517
+ renderGraph();
518
+ }
519
+ highlight();
520
+ renderInspector();
521
+ if (reveal && mode === 'structure') {
522
+ searchTarget = key;
523
+ requestAnimationFrame(centerSearchResult);
524
+ }
525
+ }
526
+ /** 搜索到长画布下方的字段时将该行移入视口。窄屏详情高度变化可能再次触发
527
+ * ResizeObserver,适应画布后仍需保留搜索位置,否则字段会瞬间跳回屏幕外。 */
528
+ function centerSearchResult() {
529
+ // 等待绘制期间切换视图会清除目标,这一帧就不再执行旧的搜索定位。
530
+ if (!searchTarget)
531
+ return;
532
+ const card = tableOwner(visibleCards, searchTarget);
533
+ const position = positions.get(card.node.key);
534
+ const row = card.fields.findIndex((field) => field.key === searchTarget);
535
+ const offset = row >= 0
536
+ ? TABLE_HEADER + (row + 0.5) * TABLE_ROW
537
+ : Math.min(card.height / 2, svg.clientHeight / (2 * camera.scale));
538
+ camera.x = svg.clientWidth / 2 - (position.x + TABLE_WIDTH / 2) * camera.scale;
539
+ camera.y = svg.clientHeight / 2 - (position.y + offset) * camera.scale;
540
+ applyCamera();
541
+ }
542
+ function selectEdge(edge) {
543
+ searchTarget = null;
544
+ selectedEdge = edge;
545
+ if (mode === 'structure' && edge.kind !== 'refs') {
546
+ // 卡片中包含字段的关系已经用行表达;显式查看其他关系时进入完整本体,
547
+ // 保留原始端点和上一次实现的关系解释,而不是伪造一条表间连线。
548
+ mode = 'all';
549
+ enabled = new Set(Object.keys(kinds));
550
+ aggregate = null;
551
+ focus = null;
552
+ renderFilters();
553
+ }
554
+ // 右侧关系列表也包含当前筛选隐藏的邻居;选中后显式展示两端,解释有图可对照。
555
+ if (!edgeElements.has(edge.key)) {
556
+ for (const key of [edge.from, edge.to]) {
557
+ const node = byKey(key);
558
+ enabled.add(node.kind);
559
+ showSystem ||= isSystem(node);
560
+ }
561
+ aggregate = null;
562
+ focus = null;
563
+ renderFilters();
564
+ renderGraph();
565
+ }
566
+ highlight();
567
+ renderInspector();
568
+ element('inspector').scrollTop = 0;
569
+ }
570
+ function renderEdgeInspector(panel, edge) {
571
+ const explanation = explainEdge(edge, model, snapshot.presentation.labels);
572
+ const top = html('div', 'inspector-top');
573
+ top.append(html('span', '', '关系说明'), html('span', '', edge.derived ? '虚线 · 视图推导' : '实线 · 声明抽取'));
574
+ panel.append(top, html('span', 'relation-category', explanation.category), html('h2', '', explanation.label));
575
+ const route = html('div', 'relation-route');
576
+ for (const [index, key] of [edge.from, edge.to].entries()) {
577
+ if (index)
578
+ route.append(html('span', 'relation-arrow', `↓ ${explanation.label}`));
579
+ const node = byKey(key);
580
+ const button = html('button', 'relation-endpoint');
581
+ button.title = `查看${displayName(node)}的节点详情`;
582
+ button.append(html('small', '', `${index ? '终点' : '起点'} · ${kinds[node.kind].label}`), html('strong', '', displayName(node)), html('code', '', node.id));
583
+ button.onclick = () => selectNode(key, true);
584
+ route.append(button);
585
+ }
586
+ panel.append(route);
587
+ const section = (title, text) => {
588
+ const box = html('section', 'relation-explanation');
589
+ box.append(html('h3', '', title), html('p', '', text));
590
+ panel.append(box);
591
+ return box;
592
+ };
593
+ section('为什么连接', explanation.why);
594
+ section('连接有什么用', explanation.purpose);
595
+ section('箭头怎么读', explanation.direction);
596
+ const source = section('声明依据', '以下是抽取到的声明属性,可与源码核对。');
597
+ for (const evidence of explanation.evidence)
598
+ source.append(html('div', 'source-location', evidence));
599
+ const node = explanation.source;
600
+ const owner = model.nodes.find((n) => n.kind === 'aggregate' && node.id.startsWith(`${n.id}.`));
601
+ const location = node.file ? node : owner?.file ? owner : null;
602
+ source.append(html('p', 'source-note', location
603
+ ? `${location.file}:${location.line ?? 1}${location !== node ? '(所属业务对象声明)' : ''}`
604
+ : '来自框架包,当前项目未记录源码位置。'));
605
+ }
606
+ function renderInspector() {
607
+ const panel = element('inspector');
608
+ panel.replaceChildren();
609
+ if (selectedEdge) {
610
+ renderEdgeInspector(panel, selectedEdge);
611
+ return;
612
+ }
613
+ if (!selected) {
614
+ panel.append(html('p', 'inspector-description', '选择节点查看属性,或点击连线了解为什么连接、连接有什么用。'));
615
+ return;
616
+ }
617
+ const node = byKey(selected), meta = kinds[node.kind];
618
+ const top = html('div', 'inspector-top');
619
+ top.append(html('span', '', '节点详情'), html('span', '', '↗'));
620
+ panel.append(top);
621
+ const icon = html('div', 'inspector-icon', meta.short);
622
+ icon.style.color = meta.color;
623
+ icon.style.backgroundColor = `${meta.color}12`;
624
+ panel.append(icon);
625
+ panel.append(html('h2', '', displayName(node)), html('div', 'node-identifier', node.id));
626
+ const pill = html('span', 'kind-pill', meta.label);
627
+ pill.style.color = meta.color;
628
+ pill.style.backgroundColor = `${meta.color}12`;
629
+ panel.append(pill);
630
+ panel.append(html('p', 'inspector-description', String(node.attrs.description ?? descriptions[node.kind])));
631
+ if (node.kind === 'aggregate') {
632
+ const note = html('div', 'aggregate-explainer');
633
+ note.append(html('strong', '', '什么是业务对象(聚合)?'), html('p', '', '框架把一组需要一起遵守业务规则的数据和操作作为一个整体管理,技术上称为“聚合”。它包含字段、状态和命令,不一定只对应一张数据库表。'));
634
+ panel.append(note);
635
+ }
636
+ const actions = html('div', 'inspector-actions');
637
+ const focusButton = html('button', 'button secondary', '◎ 聚焦关联');
638
+ focusButton.onclick = () => {
639
+ focus = node.key;
640
+ aggregate = null;
641
+ if (mode === 'structure') {
642
+ const card = tableOwner(cards, node.key);
643
+ const refs = model.edges.filter((edge) => edge.kind === 'refs' &&
644
+ (edge.to === card.node.key || card.fields.some((field) => field.key === edge.from)));
645
+ showSystem ||= refs.some((edge) => isSystem(byKey(edge.to)) || isSystem(byKey(edge.from)));
646
+ renderFilters();
647
+ renderGraph();
648
+ return;
649
+ }
650
+ const adjacent = neighborhood(model, node.key);
651
+ for (const n of model.nodes.filter((n) => adjacent.has(n.key))) {
652
+ enabled.add(n.kind);
653
+ showSystem ||= isSystem(n);
654
+ }
655
+ renderFilters();
656
+ renderGraph();
657
+ };
658
+ const copy = html('button', 'button secondary', '复制标识');
659
+ copy.onclick = async () => {
660
+ try {
661
+ await navigator.clipboard.writeText(node.id);
662
+ copy.textContent = '已复制';
663
+ }
664
+ catch (error) {
665
+ notify(`复制失败:${error.message}`);
666
+ }
667
+ };
668
+ actions.append(focusButton, copy);
669
+ panel.append(actions);
670
+ // 把当前节点交给编程工具时交付可追溯的文字,而不是让 AI 猜截图中的连线。
671
+ // 入口与 CLI 使用同一上下文构建器,复制失败明确报告,保留原来的节点详情。
672
+ const handoff = html('section', 'context-handoff');
673
+ const contextButton = html('button', 'button secondary', '⌘ 复制 AI 上下文');
674
+ const contextHint = html('p', '', '包含业务链路、连接原因、源码位置和验证候选。基于当前图谱,改代码后先重新抽取。');
675
+ contextButton.onclick = async () => {
676
+ contextButton.disabled = true;
677
+ contextButton.textContent = '正在整理上下文…';
678
+ try {
679
+ const response = await fetch(`/api/context?node=${encodeURIComponent(node.key)}&format=md`);
680
+ if (!response.ok)
681
+ throw new Error((await response.json()).error);
682
+ await navigator.clipboard.writeText(await response.text());
683
+ contextButton.textContent = '✓ 已复制,粘贴给编程工具';
684
+ }
685
+ catch (error) {
686
+ contextButton.textContent = '⌘ 复制 AI 上下文';
687
+ notify(`上下文复制失败:${error.message}`);
688
+ }
689
+ finally {
690
+ contextButton.disabled = false;
691
+ }
692
+ };
693
+ handoff.append(contextButton, contextHint);
694
+ panel.append(handoff);
695
+ const section = (title, count) => {
696
+ const div = html('section', 'detail-section'), h = html('h3', '', title);
697
+ if (count !== undefined)
698
+ h.append(html('span', '', String(count)));
699
+ div.append(h);
700
+ panel.append(div);
701
+ return div;
702
+ };
703
+ const info = section('声明属性');
704
+ const row = (label, value) => {
705
+ if (value === undefined || value === null)
706
+ return;
707
+ const div = html('div', 'detail-row');
708
+ div.append(html('span', '', label), html('span', '', typeof value === 'object' ? JSON.stringify(value) : String(value)));
709
+ info.append(div);
710
+ };
711
+ row('类型', meta.label);
712
+ if (node.kind === 'aggregate') {
713
+ row('数据表', node.attrs.table);
714
+ const scope = node.attrs.scope;
715
+ row('部门范围', scope?.dept ? named(`${node.id}.${scope.dept}`, 'field') : undefined);
716
+ row('归属字段', scope?.owner ? named(`${node.id}.${scope.owner}`, 'field') : undefined);
717
+ row('状态数量', model.nodes.filter((n) => n.kind === 'state' && n.id.startsWith(`${node.id}.`)).length);
718
+ row('命令数量', model.nodes.filter((n) => n.kind === 'command' && n.id.startsWith(`${node.id}.`)).length);
719
+ }
720
+ else if (node.kind === 'command') {
721
+ const agg = node.id.split('.')[0];
722
+ const stateName = (state) => (state === 'new' ? '新建' : named(`${agg}.${state}`, 'state'));
723
+ row('起始状态', Array.isArray(node.attrs.from)
724
+ ? node.attrs.from.map(stateName).join('、')
725
+ : stateName(String(node.attrs.from)));
726
+ row('目标状态', node.attrs.to ? stateName(String(node.attrs.to)) : '保持当前状态');
727
+ const actors = {
728
+ user: '用户',
729
+ process: '审批流程',
730
+ external: '外部系统',
731
+ job: '定时任务',
732
+ reaction: '事件反应',
733
+ system: '系统',
734
+ };
735
+ row('调用身份', node.attrs.invokedBy.map((kind) => actors[kind] ?? kind).join('、'));
736
+ row('约束', node.attrs.constraints.map((name) => constraintLabel(name, agg)).join(';') || '无');
737
+ }
738
+ else {
739
+ for (const [key, label] of Object.entries({
740
+ event: '事件',
741
+ command: '调用命令',
742
+ process: '审批流程',
743
+ cron: '调度表达式',
744
+ connector: '连接器',
745
+ operation: '操作',
746
+ sensitive: '敏感类型',
747
+ kind: '字段类型',
748
+ ref: '引用对象',
749
+ mappedBy: '对方外键',
750
+ through: '自动中间表',
751
+ virtual: '不占主表列',
752
+ readOnly: '只读关联',
753
+ optional: '可选字段',
754
+ acquires: '获取资源',
755
+ releases: '释放资源',
756
+ terminal: '终态',
757
+ initial: '初态',
758
+ })) {
759
+ const value = node.attrs[key];
760
+ if (value === undefined || value === null)
761
+ continue;
762
+ row(label, detailValue(key, value));
763
+ }
764
+ }
765
+ const source = section('声明位置');
766
+ // 字段、状态没有独立源码位置时只引用所属聚合,并在文案上说明来源。
767
+ const owner = model.nodes.find((n) => n.kind === 'aggregate' && node.id.startsWith(`${n.id}.`));
768
+ const location = node.file ? node : owner?.file ? owner : null;
769
+ if (location) {
770
+ source.append(html('div', 'source-location', `${location.file}:${location.line ?? 1}`));
771
+ if (location !== node)
772
+ source.append(html('p', 'source-note', '位置来自所属业务对象声明。'));
773
+ }
774
+ else
775
+ source.append(html('p', 'source-note', node.kind === 'entry' ? '由新建命令的迁移关系推导。' : '来自框架包,当前项目未记录源码位置。'));
776
+ const relations = model.edges.filter((e) => e.from === node.key || e.to === node.key);
777
+ const relationSection = section('关联关系', relations.length);
778
+ if (relations.length)
779
+ relationSection.append(html('p', 'source-note', '点击一项,查看连接原因与用途。'));
780
+ if (!relations.length)
781
+ relationSection.append(html('p', 'source-note', '当前声明没有关联关系。'));
782
+ for (const edge of relations) {
783
+ const explanation = explainEdge(edge, model, snapshot.presentation.labels);
784
+ const outgoing = edge.from === node.key;
785
+ const target = byKey(outgoing ? edge.to : edge.from);
786
+ const button = html('button', 'relation-button');
787
+ const dot = html('span', 'relation-dot');
788
+ dot.style.backgroundColor = kinds[target.kind].color;
789
+ const text = html('div');
790
+ text.append(html('strong', '', displayName(target)), html('small', '', `${outgoing ? '→' : '←'} ${explanation.label}`), html('small', 'relation-code', target.id));
791
+ button.append(dot, text, html('span', '', '解释 ›'));
792
+ button.onclick = () => selectEdge(edge);
793
+ relationSection.append(button);
794
+ }
795
+ const details = html('details');
796
+ details.append(html('summary', '', '查看声明元数据'), html('pre', 'detail-json', JSON.stringify(node.attrs, null, 2)));
797
+ panel.append(details);
798
+ }
799
+ /** 把会直接影响阅读理解的属性值翻译为业务名称;SQL 表名、cron 和折叠的
800
+ * 原始元数据仍保持技术原文,方便开发者核对实现。 */
801
+ function detailValue(key, value) {
802
+ if (typeof value === 'boolean')
803
+ return value ? '是' : '否';
804
+ if (key === 'event' || key === 'command')
805
+ return named(value, key);
806
+ if (key === 'ref')
807
+ return named(value, 'aggregate');
808
+ if (key === 'process') {
809
+ const binding = model.nodes.find((n) => n.kind === 'approval' && n.attrs.process === value);
810
+ return binding ? displayName(binding) : String(value);
811
+ }
812
+ if (key === 'connector')
813
+ return named(`connector:${value}`, 'connector');
814
+ if (key === 'acquires' || key === 'releases')
815
+ return snapshot.presentation.labels[`resource:${value}`] ?? String(value);
816
+ if (key === 'operation')
817
+ return snapshot.presentation.labels[`operation:${value}`] ?? String(value);
818
+ const types = {
819
+ money: '金额',
820
+ text: '文本',
821
+ int: '整数',
822
+ decimal: '小数',
823
+ float: '浮点数',
824
+ bool: '布尔值',
825
+ boolean: '布尔值',
826
+ id: '唯一标识',
827
+ ref: '关联引用',
828
+ hasMany: '一对多(反查)',
829
+ manyToMany: '多对多(自动中间表)',
830
+ list: '列表',
831
+ json: '结构化数据',
832
+ date: '日期',
833
+ datetime: '日期时间',
834
+ enum: '枚举',
835
+ phone: '电话号码',
836
+ email: '电子邮箱',
837
+ name: '姓名',
838
+ };
839
+ return ['kind', 'sensitive'].includes(key) ? (types[String(value)] ?? String(value)) : String(value);
840
+ }
841
+ function constraintLabel(name, aggregateId) {
842
+ const ceiling = /^ceiling\((.+)\)$/.exec(name);
843
+ if (ceiling)
844
+ return `${named(`${aggregateId}.${ceiling[1]}`, 'field')}不超过审批额度`;
845
+ const sod = /^sod\.notSameAs\((.+)\)$/.exec(name);
846
+ if (sod)
847
+ return `审批人与${named(`${aggregateId}.${sod[1]}`, 'field')}不同`;
848
+ const builtins = {
849
+ stepUp: '需要二次认证',
850
+ knownCapability: '能力点必须已声明',
851
+ noEscalation: '不能授予超出自身的能力',
852
+ noWiderScope: '不能授予超出自身的数据范围',
853
+ noEscalationViaRoles: '不能通过分配角色提权',
854
+ };
855
+ return builtins[name] ?? name;
856
+ }
857
+ const descriptions = {
858
+ aggregate: '统一管理字段、状态、业务操作和数据范围的业务对象。',
859
+ command: '受权限和状态约束的业务写入入口。',
860
+ event: '命令产生的领域事件,用于连接后续业务协作。',
861
+ reaction: '订阅领域事件并调用命令,可声明资源获取与释放。',
862
+ query: '通过已声明的读依赖查询安全视图。',
863
+ approval: '把业务事件、审批流程与结论命令连接起来。',
864
+ route: '代码中声明的审批步骤与分支条件。',
865
+ job: '按调度表达式触发、显式声明读取与调用范围的任务。',
866
+ connector: '声明第三方地址及可调用的操作。',
867
+ outbound: '将领域事件映射为第三方调用。',
868
+ inbound: '将验签后的外部回调映射为业务命令。',
869
+ field: '业务字段的类型、范围与敏感属性。',
870
+ state: '业务对象生命周期中的一个状态。',
871
+ capability: '从聚合声明派生的访问能力点。',
872
+ entry: '新建命令进入初始业务状态的虚拟起点。',
873
+ };
874
+ function renderFindings() {
875
+ const list = element('findings-list');
876
+ list.replaceChildren();
877
+ for (const f of snapshot.findings) {
878
+ const item = html('article', 'finding');
879
+ item.append(html('code', '', f.code), html('p', '', f.reason));
880
+ if (f.fix)
881
+ item.append(html('p', '', `修复:${f.fix}`));
882
+ if (f.at)
883
+ item.append(html('small', '', f.at));
884
+ list.append(item);
885
+ }
886
+ }
887
+ function searchNodes() {
888
+ const query = element('search').value.trim().toLowerCase();
889
+ const results = element('search-results');
890
+ results.replaceChildren();
891
+ results.hidden = !query;
892
+ if (!query)
893
+ return;
894
+ const matches = rankGraphNodes(model, query, snapshot.presentation.labels, {
895
+ descriptions: snapshot.presentation.descriptions,
896
+ });
897
+ for (const hit of matches.slice(0, 30)) {
898
+ const n = byKey(hit.key);
899
+ const button = html('button', 'search-result');
900
+ const names = html('span', 'search-names');
901
+ names.append(html('strong', '', displayName(n)), html('span', '', n.id));
902
+ button.append(html('small', '', kinds[n.kind].label), names);
903
+ button.title = hit.matches
904
+ .map((match) => `${match.field}:${match.value}(命中 ${match.terms.join('、')})`)
905
+ .join('\n');
906
+ button.onclick = () => {
907
+ selectNode(n.key, true);
908
+ results.hidden = true;
909
+ element('search').value = '';
910
+ };
911
+ results.append(button);
912
+ }
913
+ if (!matches.length)
914
+ results.append(html('div', 'search-empty', '没有匹配的声明,试试命令名或字段名。'));
915
+ }
916
+ function applyCamera() {
917
+ viewport.setAttribute('transform', `translate(${camera.x},${camera.y}) scale(${camera.scale})`);
918
+ element('zoom-level').textContent = `${Math.round(camera.scale * 100)}%`;
919
+ }
920
+ function fit() {
921
+ if (!visibleNodes.length)
922
+ return;
923
+ const bounds = svg.getBoundingClientRect();
924
+ const xs = [...positions.values()].map((p) => p.x), ys = [...positions.values()].map((p) => p.y);
925
+ const minX = Math.min(...xs), minY = Math.min(...ys), width = Math.max(...visibleNodes.map((node) => positions.get(node.key).x + nodeSize(node.key).width)) - minX, height = Math.max(...visibleNodes.map((node) => positions.get(node.key).y + nodeSize(node.key).height)) - minY;
926
+ camera.scale = Math.min((bounds.width - 65) / width, (bounds.height - 85) / height, mode === 'structure' ? 1 : 1.2);
927
+ // 表字段是正文,默认不把它们缩成难读的小字。长表可纵向平移,适当留白
928
+ // 比为了容纳全图而把所有字段缩到 35% 更适合阅读。
929
+ if (mode === 'structure')
930
+ camera.scale = Math.max(Math.min(0.8, (bounds.width - 40) / TABLE_WIDTH), camera.scale);
931
+ camera.x = (bounds.width - width * camera.scale) / 2 - minX * camera.scale;
932
+ camera.y =
933
+ (mode === 'structure'
934
+ ? Math.max(30, (bounds.height - height * camera.scale) / 2)
935
+ : (bounds.height - height * camera.scale) / 2 - 7) -
936
+ minY * camera.scale;
937
+ applyCamera();
938
+ if (mode === 'structure' && searchTarget)
939
+ centerSearchResult();
940
+ }
941
+ function zoom(factor, x = svg.clientWidth / 2, y = svg.clientHeight / 2) {
942
+ searchTarget = null;
943
+ const scale = Math.min(2.5, Math.max(0.15, camera.scale * factor));
944
+ camera.x = x - ((x - camera.x) * scale) / camera.scale;
945
+ camera.y = y - ((y - camera.y) * scale) / camera.scale;
946
+ camera.scale = scale;
947
+ applyCamera();
948
+ }
949
+ // 指针捕获同时支持鼠标和触控。节点拖动修改布局坐标,空白拖动修改视口;
950
+ // 拖动结束不触发选择,避免移动节点时不断切换右侧详情。
951
+ let drag = null;
952
+ svg.addEventListener('pointerdown', (event) => {
953
+ if (event.button !== 0)
954
+ return;
955
+ searchTarget = null;
956
+ // 连线由自身 click/键盘处理;不进入画布平移,否则 pointer capture 会吞掉点击。
957
+ if (event.target.closest('.edge-group'))
958
+ return;
959
+ if (event.target.closest('.table-field'))
960
+ return;
961
+ const target = event.target.closest('.node');
962
+ const key = target?.dataset.key;
963
+ drag = {
964
+ id: event.pointerId,
965
+ key,
966
+ x: event.clientX,
967
+ y: event.clientY,
968
+ initial: key ? { ...positions.get(key) } : { ...camera },
969
+ moved: false,
970
+ };
971
+ svg.setPointerCapture(event.pointerId);
972
+ svg.classList.add('dragging');
973
+ });
974
+ svg.addEventListener('pointermove', (event) => {
975
+ if (!drag || drag.id !== event.pointerId)
976
+ return;
977
+ const dx = event.clientX - drag.x, dy = event.clientY - drag.y;
978
+ drag.moved ||= Math.abs(dx) + Math.abs(dy) > 4;
979
+ if (drag.key) {
980
+ positions.set(drag.key, { x: drag.initial.x + dx / camera.scale, y: drag.initial.y + dy / camera.scale });
981
+ updatePositions();
982
+ }
983
+ else {
984
+ camera.x = drag.initial.x + dx;
985
+ camera.y = drag.initial.y + dy;
986
+ applyCamera();
987
+ }
988
+ });
989
+ const endDrag = (event) => {
990
+ if (!drag || drag.id !== event.pointerId)
991
+ return;
992
+ if (event.type === 'pointerup' && !drag.moved) {
993
+ if (drag.key)
994
+ selectNode(drag.key);
995
+ else {
996
+ selected = null;
997
+ selectedEdge = null;
998
+ highlight();
999
+ renderInspector();
1000
+ }
1001
+ }
1002
+ drag = null;
1003
+ svg.classList.remove('dragging');
1004
+ };
1005
+ svg.addEventListener('pointerup', endDrag);
1006
+ svg.addEventListener('pointercancel', endDrag);
1007
+ svg.addEventListener('wheel', (event) => {
1008
+ event.preventDefault();
1009
+ const rect = svg.getBoundingClientRect();
1010
+ zoom(Math.exp(-event.deltaY * 0.0015), event.clientX - rect.left, event.clientY - rect.top);
1011
+ }, { passive: false });
1012
+ new ResizeObserver(() => {
1013
+ if (snapshot) {
1014
+ if (mode === 'structure') {
1015
+ positions = layoutTables(visibleCards, visibleEdges, svg.clientWidth < 680 ? 1 : 2);
1016
+ updatePositions();
1017
+ }
1018
+ fit();
1019
+ }
1020
+ }).observe(svg);
1021
+ function resetView() {
1022
+ mode = 'structure';
1023
+ aggregate = null;
1024
+ focus = null;
1025
+ showSystem = false;
1026
+ enabled = new Set(businessKinds);
1027
+ renderFilters();
1028
+ renderGraph();
1029
+ }
1030
+ element('reset-view').onclick = resetView;
1031
+ element('reset-types').onclick = () => {
1032
+ enabled = new Set(mode === 'business' ? businessKinds : Object.keys(kinds));
1033
+ renderFilters();
1034
+ renderGraph();
1035
+ };
1036
+ element('exit-focus').onclick = () => {
1037
+ focus = null;
1038
+ renderGraph();
1039
+ };
1040
+ element('system-toggle').onchange = (event) => {
1041
+ showSystem = event.target.checked;
1042
+ aggregate = null;
1043
+ focus = null;
1044
+ renderFilters();
1045
+ renderGraph();
1046
+ };
1047
+ document.querySelectorAll('[data-mode]').forEach((button) => {
1048
+ button.onclick = () => {
1049
+ mode = button.dataset.mode;
1050
+ selectedEdge = null;
1051
+ enabled = new Set(mode === 'business' ? businessKinds : Object.keys(kinds));
1052
+ focus = null;
1053
+ if (mode === 'structure' && selected)
1054
+ selected =
1055
+ tableOwner(cards, selected)?.node.key ?? cards.find((card) => !isSystem(card.node))?.node.key ?? null;
1056
+ renderFilters();
1057
+ renderGraph();
1058
+ renderInspector();
1059
+ };
1060
+ });
1061
+ element('refresh').onclick = () => void load(true);
1062
+ element('search').addEventListener('input', searchNodes);
1063
+ element('search').addEventListener('focus', searchNodes);
1064
+ element('search').addEventListener('keydown', (event) => {
1065
+ if (event.key === 'Enter')
1066
+ element('search-results').querySelector('button')?.click();
1067
+ if (event.key === 'Escape')
1068
+ element('search-results').hidden = true;
1069
+ });
1070
+ document.addEventListener('pointerdown', (event) => {
1071
+ if (!event.target.closest('.search-wrap'))
1072
+ element('search-results').hidden = true;
1073
+ });
1074
+ document.addEventListener('keydown', (event) => {
1075
+ if (event.key === '/' &&
1076
+ !(event.target instanceof HTMLInputElement) &&
1077
+ !element('findings-dialog').open) {
1078
+ event.preventDefault();
1079
+ element('search').focus();
1080
+ }
1081
+ });
1082
+ element('zoom-in').onclick = () => zoom(1.2);
1083
+ element('zoom-out').onclick = () => zoom(1 / 1.2);
1084
+ element('fit').onclick = () => {
1085
+ searchTarget = null;
1086
+ fit();
1087
+ };
1088
+ element('relayout').onclick = () => renderGraph();
1089
+ element('show-findings').onclick = () => element('findings-dialog').showModal();
1090
+ element('close-findings').onclick = () => element('findings-dialog').close();
1091
+ element('toggle-sidebar').onclick = () => {
1092
+ const open = element('sidebar').classList.toggle('open');
1093
+ element('toggle-sidebar').setAttribute('aria-expanded', String(open));
1094
+ };
1095
+ element('download').onclick = () => {
1096
+ const url = URL.createObjectURL(new Blob([JSON.stringify(snapshot.graph, null, 2)], { type: 'application/json' }));
1097
+ const link = html('a');
1098
+ link.href = url;
1099
+ link.download = 'ontology.json';
1100
+ link.click();
1101
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
1102
+ };
1103
+ void load();