create-windy 0.3.19 → 0.4.1

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 (234) hide show
  1. package/README.md +28 -13
  2. package/dist/cli.js +788 -280
  3. package/package.json +1 -1
  4. package/template/.agents/skills/windy-business-module/SKILL.md +34 -10
  5. package/template/.windy-template.json +2 -2
  6. package/template/AGENTS.md +18 -8
  7. package/template/README.md +32 -22
  8. package/template/apps/server/Dockerfile +1 -0
  9. package/template/apps/server/package.json +1 -0
  10. package/template/apps/server/scripts/validate-module-composition-once.ts +4 -0
  11. package/template/apps/server/scripts/validate-module-composition.ts +28 -0
  12. package/template/apps/server/src/agent/data-scope-routes.test.ts +20 -5
  13. package/template/apps/server/src/agent/routes.test.ts +20 -7
  14. package/template/apps/server/src/agent/routes.ts +14 -0
  15. package/template/apps/server/src/agent/runtime-bootstrap.ts +22 -6
  16. package/template/apps/server/src/agent/sandbox-worker.ts +2 -2
  17. package/template/apps/server/src/agent/unavailable-routes.ts +33 -0
  18. package/template/apps/server/src/app/server-composition.ts +56 -0
  19. package/template/apps/server/src/application-services.ts +2 -0
  20. package/template/apps/server/src/bulk-data/service.ts +14 -0
  21. package/template/apps/server/src/data-governance/export/authorization-port.ts +1 -1
  22. package/template/apps/server/src/data-governance/export/postgres-api-fixture.ts +18 -2
  23. package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +10 -9
  24. package/template/apps/server/src/data-governance/export/routes-test-fixture.ts +6 -8
  25. package/template/apps/server/src/data-governance/user-plaintext-routes.test.ts +4 -2
  26. package/template/apps/server/src/feature/routes.authorization.test.ts +26 -1
  27. package/template/apps/server/src/feature/routes.ts +33 -15
  28. package/template/apps/server/src/foundation-activation.test.ts +60 -0
  29. package/template/apps/server/src/foundation.ts +64 -5
  30. package/template/apps/server/src/guards.test.ts +23 -0
  31. package/template/apps/server/src/guards.ts +12 -0
  32. package/template/apps/server/src/index.ts +28 -34
  33. package/template/apps/server/src/installed-business-modules.ts +13 -24
  34. package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +1 -0
  35. package/template/apps/server/src/jobs/legacy-adapter.test.ts +106 -0
  36. package/template/apps/server/src/jobs/legacy-service-runtime.ts +40 -0
  37. package/template/apps/server/src/license/activation.test.ts +30 -0
  38. package/template/apps/server/src/license/activation.ts +26 -0
  39. package/template/apps/server/src/license/restriction.test.ts +16 -0
  40. package/template/apps/server/src/license/routes-offline-code.test.ts +6 -2
  41. package/template/apps/server/src/license/routes.ts +20 -4
  42. package/template/apps/server/src/license/runtime-service.test.ts +10 -2
  43. package/template/apps/server/src/module-bootstrap.ts +14 -23
  44. package/template/apps/server/src/module-composition.test.ts +43 -2
  45. package/template/apps/server/src/module-composition.ts +29 -0
  46. package/template/apps/server/src/module-host/durable-context-guard.test.ts +172 -0
  47. package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +50 -1
  48. package/template/apps/server/src/module-host/durable-context.test.ts +42 -25
  49. package/template/apps/server/src/module-host/durable-context.ts +42 -2
  50. package/template/apps/server/src/module-host/durable-handler-registration.ts +96 -5
  51. package/template/apps/server/src/module-host/implementation-completeness.test.ts +185 -0
  52. package/template/apps/server/src/module-host/implementation-completeness.ts +132 -0
  53. package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
  54. package/template/apps/server/src/module-host/initialize.test.ts +78 -2
  55. package/template/apps/server/src/module-host/initialize.ts +15 -4
  56. package/template/apps/server/src/module-host/project-attachments.test.ts +92 -0
  57. package/template/apps/server/src/module-host/project-attachments.ts +58 -0
  58. package/template/apps/server/src/module-host/project-composition-package.test.ts +186 -0
  59. package/template/apps/server/src/module-host/project-composition.test.ts +260 -0
  60. package/template/apps/server/src/module-host/project-composition.ts +311 -0
  61. package/template/apps/server/src/module-host/request-context.ts +3 -0
  62. package/template/apps/server/src/module-host/route-declaration.ts +15 -6
  63. package/template/apps/server/src/module-host/route-installer.test.ts +15 -2
  64. package/template/apps/server/src/module-host/route-installer.ts +12 -2
  65. package/template/apps/server/src/module-host/route-registration.test.ts +13 -4
  66. package/template/apps/server/src/module-host/runtime-composition-compatibility.test.ts +63 -0
  67. package/template/apps/server/src/module-host/runtime-composition-compatibility.ts +46 -0
  68. package/template/apps/server/src/module-host/runtime-composition.ts +34 -0
  69. package/template/apps/server/src/module-host/search-provider-registration.test.ts +64 -0
  70. package/template/apps/server/src/module-host/search-provider-registration.ts +35 -0
  71. package/template/apps/server/src/module-host/static-route-authorization.test.ts +99 -0
  72. package/template/apps/server/src/module-host/static-route-authorization.ts +90 -0
  73. package/template/apps/server/src/module-host/task-definitions.ts +1 -0
  74. package/template/apps/server/src/module-host/user-directory-composition.test.ts +4 -1
  75. package/template/apps/server/src/module-host.test.ts +2 -0
  76. package/template/apps/server/src/module-preflight.test.ts +289 -0
  77. package/template/apps/server/src/module-preflight.ts +96 -0
  78. package/template/apps/server/src/module-runtime-validation.ts +1 -0
  79. package/template/apps/server/src/platform-app-bootstrap.ts +50 -0
  80. package/template/apps/server/src/platform-http-runtime.ts +32 -7
  81. package/template/apps/server/src/route-guards.test.ts +22 -0
  82. package/template/apps/server/src/route-guards.ts +10 -3
  83. package/template/apps/server/src/runtime-audit-query.ts +50 -0
  84. package/template/apps/server/src/runtime-feature.test.ts +46 -0
  85. package/template/apps/server/src/runtime-feature.ts +5 -0
  86. package/template/apps/server/src/runtime.ts +10 -49
  87. package/template/apps/server/src/scheduler/audit-action.test.ts +49 -0
  88. package/template/apps/server/src/scheduler/audit.ts +1 -1
  89. package/template/apps/server/src/scheduler/execution-recorder.ts +10 -5
  90. package/template/apps/server/src/scheduler/recovery-service.test.ts +4 -3
  91. package/template/apps/server/src/scheduler/service-idle.test.ts +4 -3
  92. package/template/apps/server/src/scheduler/service.test.ts +1 -0
  93. package/template/apps/server/src/scheduler/service.ts +18 -21
  94. package/template/apps/server/src/scheduler/types.ts +5 -1
  95. package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +4 -0
  96. package/template/apps/server/src/settings/drizzle-watermark-repository.ts +11 -3
  97. package/template/apps/server/src/settings/watermark-routes.test.ts +54 -1
  98. package/template/apps/server/src/settings/watermark-routes.ts +29 -1
  99. package/template/apps/server/src/startup-composition-validation.test.ts +35 -0
  100. package/template/apps/server/src/startup-composition-validation.ts +54 -0
  101. package/template/apps/server/src/startup-preflight.test.ts +36 -0
  102. package/template/apps/server/src/startup-preflight.ts +37 -0
  103. package/template/apps/server/src/system/audit-routes.test.ts +9 -3
  104. package/template/apps/server/src/system/department-routes.test.ts +5 -3
  105. package/template/apps/server/src/system/feature-routes.test.ts +4 -3
  106. package/template/apps/server/src/system/interface-catalog.test.ts +5 -3
  107. package/template/apps/server/src/system/interface-catalog.ts +3 -0
  108. package/template/apps/server/src/system/menu-routes.test.ts +5 -3
  109. package/template/apps/server/src/system/route-test-runtime.ts +24 -0
  110. package/template/apps/server/src/system/routes-validation.test.ts +7 -3
  111. package/template/apps/server/src/system/routes.test.ts +21 -11
  112. package/template/apps/server/src/system/routes.ts +11 -2
  113. package/template/apps/server/src/work-order/foundation-modules.test.ts +6 -4
  114. package/template/apps/server/src/work-order/routes.test.ts +8 -0
  115. package/template/apps/server/src/work-order/task.test.ts +6 -1
  116. package/template/apps/web/Dockerfile +5 -0
  117. package/template/apps/web/package.json +1 -0
  118. package/template/apps/web/scripts/validate-admin-ui-composition.ts +106 -0
  119. package/template/apps/web/src/app/web-composition.ts +24 -0
  120. package/template/apps/web/src/app/web-composition.webtest.ts +15 -0
  121. package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
  122. package/template/apps/web/src/composables/watermark-settings.ts +40 -11
  123. package/template/apps/web/src/composables/watermark-settings.webtest.ts +19 -6
  124. package/template/apps/web/src/layout/AppSidebar.vue +8 -1
  125. package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +2 -0
  126. package/template/apps/web/src/layout/GlobalWatermark.vue +4 -8
  127. package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +12 -5
  128. package/template/apps/web/src/layout/NavigationSearchDialog.vue +8 -1
  129. package/template/apps/web/src/layout/navigation.ts +29 -4
  130. package/template/apps/web/src/layout/navigation.webtest.ts +51 -0
  131. package/template/apps/web/src/pages/dashboard/dashboard-presenter.ts +6 -1
  132. package/template/apps/web/src/pages/settings/WatermarkPreview.vue +72 -0
  133. package/template/apps/web/src/pages/settings/WatermarkPreview.webtest.ts +55 -0
  134. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +80 -24
  135. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.webtest.ts +115 -0
  136. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +202 -0
  137. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.webtest.ts +51 -0
  138. package/template/apps/web/src/router/admin-extension-loader.ts +44 -0
  139. package/template/apps/web/src/router/admin-extension-loader.webtest.ts +49 -0
  140. package/template/apps/web/src/router/admin-ui-composition-plugin.ts +33 -0
  141. package/template/apps/web/src/router/admin-ui-composition-plugin.webtest.ts +20 -0
  142. package/template/apps/web/src/router/admin-ui-composition.ts +313 -0
  143. package/template/apps/web/src/router/admin-ui-composition.webtest.ts +225 -0
  144. package/template/apps/web/src/router/admin-ui-module-registry-plugin.ts +55 -0
  145. package/template/apps/web/src/router/admin-ui-module-registry-plugin.webtest.ts +46 -0
  146. package/template/apps/web/src/router/business-routes.ts +32 -45
  147. package/template/apps/web/src/router/business-routes.webtest.ts +40 -44
  148. package/template/apps/web/src/router/index.ts +64 -54
  149. package/template/apps/web/src/router/manifest-routes.ts +27 -7
  150. package/template/apps/web/src/router/manifest-routes.webtest.ts +10 -2
  151. package/template/apps/web/src/router/platform-admin-route-bindings.ts +58 -0
  152. package/template/apps/web/src/router/reserved-route-paths.ts +14 -0
  153. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +10 -4
  154. package/template/apps/web/src/router/web-composition-host.ts +20 -0
  155. package/template/apps/web/src/services/work-order-api.ts +16 -0
  156. package/template/apps/web/src/services/work-order-api.webtest.ts +21 -0
  157. package/template/apps/web/src/virtual-admin-ui-modules.d.ts +5 -0
  158. package/template/apps/web/vite.config.ts +22 -1
  159. package/template/docs/architecture/migration-bundle.md +10 -7
  160. package/template/docs/architecture/module-manifest-composition.md +85 -3
  161. package/template/docs/architecture/search-provider-protocol.md +6 -1
  162. package/template/docs/architecture/server-module-interface.md +70 -7
  163. package/template/docs/development/business-web-routes.md +53 -50
  164. package/template/docs/development/module-lifecycle.md +48 -54
  165. package/template/docs/platform/navigation-registry.md +5 -4
  166. package/template/docs/platform/platform-shell-settings.md +15 -10
  167. package/template/docs/platform/web-system-crud.md +1 -1
  168. package/template/package.json +1 -1
  169. package/template/packages/config/package.json +1 -1
  170. package/template/packages/database/drizzle/0044_sweet_tarantula.sql +6 -0
  171. package/template/packages/database/drizzle/meta/0044_snapshot.json +8349 -0
  172. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  173. package/template/packages/database/package.json +1 -1
  174. package/template/packages/database/src/schema/platform-settings.ts +3 -0
  175. package/template/packages/example-work-order/package.json +6 -0
  176. package/template/packages/example-work-order/src/admin-page.ts +78 -0
  177. package/template/packages/example-work-order/src/admin.ts +35 -0
  178. package/template/packages/example-work-order/src/manifest.test.ts +62 -5
  179. package/template/packages/example-work-order/src/manifest.ts +54 -7
  180. package/template/packages/example-work-order/src/migration-bundle.ts +8 -0
  181. package/template/packages/example-work-order/src/server-module.ts +2 -2
  182. package/template/packages/jobs/package.json +1 -1
  183. package/template/packages/modules/index.ts +4 -0
  184. package/template/packages/modules/package.json +3 -2
  185. package/template/packages/modules/src/admin-ui-entry.ts +8 -0
  186. package/template/packages/modules/src/admin-ui-manifest.test.ts +52 -0
  187. package/template/packages/modules/src/admin-ui-manifest.ts +53 -0
  188. package/template/packages/modules/src/capability-audit-validation.test.ts +127 -0
  189. package/template/packages/modules/src/capability-audit-validation.ts +38 -0
  190. package/template/packages/modules/src/capability-guard-validation.ts +148 -0
  191. package/template/packages/modules/src/capability-key-validation.test.ts +150 -0
  192. package/template/packages/modules/src/composition-fixtures.test-support.ts +7 -5
  193. package/template/packages/modules/src/composition.test.ts +74 -9
  194. package/template/packages/modules/src/composition.ts +8 -0
  195. package/template/packages/modules/src/manifest.ts +1 -0
  196. package/template/packages/modules/src/migration-bundle-validation.test.ts +23 -0
  197. package/template/packages/modules/src/module-host-range.test.ts +57 -0
  198. package/template/packages/modules/src/module-host-range.ts +125 -0
  199. package/template/packages/modules/src/project-module-assembly.test.ts +133 -0
  200. package/template/packages/modules/src/project-module-assembly.ts +126 -0
  201. package/template/packages/modules/src/system-admin-routes.ts +6 -5
  202. package/template/packages/modules/src/system-admin-ui-catalog.ts +72 -0
  203. package/template/packages/modules/src/system-agent-tools.ts +2 -2
  204. package/template/packages/modules/src/system-api-binding.ts +43 -9
  205. package/template/packages/modules/src/system-api-control-plane.ts +202 -0
  206. package/template/packages/modules/src/system-api-data-governance.ts +4 -2
  207. package/template/packages/modules/src/system-api-operational.ts +57 -8
  208. package/template/packages/modules/src/system-api-permissions.ts +115 -21
  209. package/template/packages/modules/src/system-api-watermark.ts +3 -0
  210. package/template/packages/modules/src/system-audit-actions.ts +7 -0
  211. package/template/packages/modules/src/system-data-governance.ts +5 -2
  212. package/template/packages/modules/src/system-features.ts +31 -0
  213. package/template/packages/modules/src/system-governed-export-bindings.ts +8 -4
  214. package/template/packages/modules/src/system-module-catalog.test.ts +39 -4
  215. package/template/packages/modules/src/system-module-catalog.ts +68 -7
  216. package/template/packages/modules/src/system-module-tasks.ts +8 -1
  217. package/template/packages/modules/src/system-modules-access.test.ts +4 -0
  218. package/template/packages/modules/src/system-modules.test.ts +10 -2
  219. package/template/packages/modules/src/system-modules.ts +6 -5
  220. package/template/packages/modules/src/system-permissions.ts +4 -0
  221. package/template/packages/modules/user/module.ts +19 -2
  222. package/template/packages/server-sdk/index.ts +8 -0
  223. package/template/packages/server-sdk/package.json +2 -1
  224. package/template/packages/server-sdk/src/durable-handler.ts +16 -1
  225. package/template/packages/server-sdk/src/module-adapter.test.ts +44 -0
  226. package/template/packages/server-sdk/src/module-adapter.ts +59 -0
  227. package/template/packages/server-sdk/src/route-definition.ts +3 -2
  228. package/template/packages/shared/package.json +1 -1
  229. package/template/packages/shared/src/audit.test.ts +7 -0
  230. package/template/packages/shared/src/audit.ts +16 -0
  231. package/template/packages/shared/src/migration-bundle-validation.ts +9 -11
  232. package/template/packages/shared/src/platform-settings.ts +17 -0
  233. package/template/packages/shared/src/rbac.ts +2 -0
  234. package/template/apps/web/src/app/business-routes.ts +0 -22
@@ -0,0 +1,127 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { fixture } from "./composition-fixtures.test-support.js";
3
+ import { validateModuleComposition } from "./composition.js";
4
+ import { systemFoundationModule } from "./system-modules.js";
5
+
6
+ describe("API 与定时任务 Audit Action 组合门禁", () => {
7
+ test("非匿名 API 缺少 Audit Action 时 fail closed", () => {
8
+ const module = fixture("api-audit");
9
+ Reflect.deleteProperty(module.apiPermissions[0]!, "auditAction");
10
+
11
+ expect(() => validateModuleComposition([module])).toThrow(
12
+ "API Route GET /api/api-audit 必须声明 Audit Action",
13
+ );
14
+ });
15
+
16
+ test("定时任务缺少 Audit Action 时 fail closed", () => {
17
+ const module = fixture("task-audit");
18
+ Reflect.deleteProperty(module.tasks[0]!, "auditAction");
19
+
20
+ expect(() => validateModuleComposition([module])).toThrow(
21
+ "定时任务 task-audit.task 必须声明 Audit Action",
22
+ );
23
+ });
24
+
25
+ test.each([
26
+ [
27
+ "API Route GET /api/api-audit",
28
+ (module: ReturnType<typeof fixture>) => {
29
+ module.apiPermissions[0]!.auditAction = "audit.unknown";
30
+ },
31
+ ],
32
+ [
33
+ "定时任务 task-audit.task",
34
+ (module: ReturnType<typeof fixture>) => {
35
+ module.tasks[0]!.auditAction = "audit.unknown";
36
+ },
37
+ ],
38
+ ] as const)("%s 引用未知 Audit Action 时 fail closed", (label, mutate) => {
39
+ const module = fixture(
40
+ label.startsWith("API") ? "api-audit" : "task-audit",
41
+ );
42
+ mutate(module);
43
+
44
+ expect(() => validateModuleComposition([module])).toThrow(
45
+ `${label} 引用了未注册审计动作:audit.unknown`,
46
+ );
47
+ });
48
+
49
+ test("显式匿名 API 可以不声明 Audit Action", () => {
50
+ const module = fixture("public");
51
+ module.apiPermissions[0]!.anonymous = true;
52
+ Reflect.deleteProperty(module.apiPermissions[0]!, "auditAction");
53
+
54
+ expect(() => validateModuleComposition([module])).not.toThrow();
55
+ });
56
+
57
+ test("Provider 不虚构执行 Audit Action", () => {
58
+ const module = fixture("provider");
59
+
60
+ expect("auditAction" in module.providers[0]!).toBe(false);
61
+ expect(() => validateModuleComposition([module])).not.toThrow();
62
+ });
63
+
64
+ test("系统 API 与任务引用的 Audit Action 均由 Manifest 注册", () => {
65
+ const manifest = systemFoundationModule();
66
+ const registered = new Set(
67
+ manifest.auditActions.map((action) =>
68
+ typeof action === "string" ? action : action.key,
69
+ ),
70
+ );
71
+ const used = [
72
+ ...manifest.apiPermissions
73
+ .filter(({ anonymous }) => anonymous !== true)
74
+ .map(({ auditAction }) => auditAction),
75
+ ...manifest.tasks.map(({ auditAction }) => auditAction),
76
+ ];
77
+
78
+ expect(used.every((action) => action && registered.has(action))).toBe(true);
79
+ });
80
+
81
+ test("系统资源 API 按读取、导入、导出和 CRUD 语义声明 Audit Action", () => {
82
+ const bindings = systemFoundationModule().apiPermissions.filter(
83
+ ({ path }) => path.startsWith("/api/system/roles"),
84
+ );
85
+
86
+ expect(bindings.map(({ auditAction }) => auditAction)).toEqual([
87
+ "data.read",
88
+ "data.export",
89
+ "data.read",
90
+ "data.import",
91
+ "crud.create",
92
+ "crud.update",
93
+ "crud.delete",
94
+ ]);
95
+ });
96
+
97
+ test.each([
98
+ // @windy-module system.search begin
99
+ ["POST", "/api/search", "search.query"],
100
+ // @windy-module system.search end
101
+ ["PUT", "/api/system/menus/placements", "menu.structure.update"],
102
+ ["PUT", "/api/system/features/:id", "feature.update"],
103
+ // @windy-module system.scheduler begin
104
+ ["POST", "/api/system/scheduled-tasks/:id/trigger", "task.trigger"],
105
+ // @windy-module system.scheduler end
106
+ // @windy-module system.bulk-data begin
107
+ [
108
+ "POST",
109
+ "/api/system/bulk-data/dictionaries/import",
110
+ "data.bulk-import.create",
111
+ ],
112
+ // @windy-module system.bulk-data end
113
+ // @windy-module system.storage begin
114
+ [
115
+ "PUT",
116
+ "/api/system/storage/uploads/:id/parts/:partNumber",
117
+ "file.upload.part.write",
118
+ ],
119
+ // @windy-module system.storage end
120
+ ] as const)("%s %s 声明真实动作 %s", (method, path, auditAction) => {
121
+ expect(
122
+ systemFoundationModule().apiPermissions.find(
123
+ (binding) => binding.method === method && binding.path === path,
124
+ ),
125
+ ).toMatchObject({ auditAction });
126
+ });
127
+ });
@@ -0,0 +1,38 @@
1
+ import type { AuditActionType } from "@southwind-ai/shared";
2
+ import type { ModuleManifest } from "./manifest.js";
3
+
4
+ export function validateCapabilityAuditActions(
5
+ modules: readonly ModuleManifest[],
6
+ auditActions: ReadonlySet<string>,
7
+ ): void {
8
+ for (const module of modules) {
9
+ for (const route of module.apiPermissions) {
10
+ if (route.anonymous === true) continue;
11
+ validateAuditAction(
12
+ `API Route ${route.method} ${route.path}`,
13
+ route.auditAction,
14
+ auditActions,
15
+ );
16
+ }
17
+ for (const task of module.tasks) {
18
+ validateAuditAction(
19
+ `定时任务 ${task.key}`,
20
+ task.auditAction,
21
+ auditActions,
22
+ );
23
+ }
24
+ }
25
+ }
26
+
27
+ function validateAuditAction(
28
+ label: string,
29
+ auditAction: AuditActionType | undefined,
30
+ auditActions: ReadonlySet<string>,
31
+ ): void {
32
+ if (!auditAction?.trim()) {
33
+ throw new Error(`${label} 必须声明 Audit Action`);
34
+ }
35
+ if (!auditActions.has(auditAction)) {
36
+ throw new Error(`${label} 引用了未注册审计动作:${auditAction}`);
37
+ }
38
+ }
@@ -0,0 +1,148 @@
1
+ import type { MenuNode } from "@southwind-ai/shared";
2
+ import type { ModuleManifest } from "./manifest.js";
3
+
4
+ interface CapabilityGuard {
5
+ label: string;
6
+ permissionKey?: string;
7
+ featureKey?: string;
8
+ }
9
+
10
+ export function validateCapabilityGuardCompleteness(
11
+ modules: readonly ModuleManifest[],
12
+ ): void {
13
+ for (const module of modules) {
14
+ const guards: CapabilityGuard[] = [
15
+ ...flattenMenus(module.menus)
16
+ .filter(({ type }) => type !== "catalog")
17
+ .map((menu) => guard("菜单", menu)),
18
+ ...module.adminRoutes.map((route) => guard("Admin Route", route)),
19
+ ...module.apiPermissions
20
+ .filter(({ anonymous }) => anonymous !== true)
21
+ .map((route) =>
22
+ guard("API Route", {
23
+ ...route,
24
+ key: `${route.method} ${route.path}`,
25
+ }),
26
+ ),
27
+ ...module.tasks.map((task) => guard("定时任务", task)),
28
+ ...module.tools.map((tool) => guard("Agent Tool", tool)),
29
+ ...(module.aiOperations || []).map((operation) =>
30
+ guard("AI Operation", operation),
31
+ ),
32
+ ...module.providers.map((provider) => guard("Provider", provider)),
33
+ ];
34
+
35
+ for (const item of guards) {
36
+ if (!item.permissionKey?.trim() || !item.featureKey?.trim()) {
37
+ throw new Error(`${item.label} 必须同时声明 Permission 与 Feature`);
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ export function validateCapabilityKeyAlignment(
44
+ modules: readonly ModuleManifest[],
45
+ ): void {
46
+ const featureKeys = new Set<string>(
47
+ modules.flatMap((module) => module.features.map(({ key }) => key)),
48
+ );
49
+ const permissionKeys = new Set(
50
+ modules.flatMap((module) => module.permissions.map(({ key }) => key)),
51
+ );
52
+
53
+ for (const module of modules) {
54
+ for (const permission of module.permissions) {
55
+ const expectedFeatureKey = permissionFeatureKey(permission.key);
56
+ if (!featureKeys.has(expectedFeatureKey)) {
57
+ throw new Error(
58
+ `权限 ${permission.key} 缺少同基础 Key Feature:${expectedFeatureKey}`,
59
+ );
60
+ }
61
+ }
62
+
63
+ const guards: CapabilityGuard[] = [
64
+ ...flattenMenus(module.menus)
65
+ .filter(({ type }) => type !== "catalog")
66
+ .map((menu) => guard("菜单", menu)),
67
+ ...module.adminRoutes.map((route) => guard("Admin Route", route)),
68
+ ...module.apiPermissions
69
+ .filter(({ anonymous }) => anonymous !== true)
70
+ .map((route) =>
71
+ guard("API Route", {
72
+ ...route,
73
+ key: `${route.method} ${route.path}`,
74
+ }),
75
+ ),
76
+ ...module.tasks.map((task) => guard("定时任务", task)),
77
+ ...module.tools.map((tool) => guard("Agent Tool", tool)),
78
+ ...(module.aiOperations || []).map((operation) =>
79
+ guard("AI Operation", operation),
80
+ ),
81
+ ...module.providers.map((provider) => guard("Provider", provider)),
82
+ ...(module.dataPolicies || []).flatMap((policy) =>
83
+ [
84
+ policy.readPermissionKey,
85
+ policy.revealPermissionKey,
86
+ policy.exportPermissionKey,
87
+ ].map((permissionKey) =>
88
+ guard("数据治理策略", {
89
+ key: policy.key,
90
+ permissionKey,
91
+ featureKey: policy.featureKey,
92
+ }),
93
+ ),
94
+ ),
95
+ ];
96
+
97
+ for (const item of guards) {
98
+ validateKeyAlignment(item.label, item, permissionKeys, featureKeys);
99
+ }
100
+ }
101
+ }
102
+
103
+ function validateKeyAlignment(
104
+ label: string,
105
+ input: { permissionKey?: string; featureKey?: string },
106
+ permissionKeys: ReadonlySet<string>,
107
+ featureKeys: ReadonlySet<string>,
108
+ ): void {
109
+ if (
110
+ !input.permissionKey ||
111
+ !input.featureKey ||
112
+ !permissionKeys.has(input.permissionKey) ||
113
+ !featureKeys.has(input.featureKey)
114
+ ) {
115
+ return;
116
+ }
117
+ const expectedFeatureKey = permissionFeatureKey(input.permissionKey);
118
+ if (input.featureKey !== expectedFeatureKey) {
119
+ throw new Error(
120
+ `${label} 的 Permission 基础 Key ${expectedFeatureKey} 与 Feature ${input.featureKey} 不一致`,
121
+ );
122
+ }
123
+ }
124
+
125
+ function permissionFeatureKey(permissionKey: string): string {
126
+ const separator = permissionKey.lastIndexOf(".");
127
+ return separator > 0 ? permissionKey.slice(0, separator) : "";
128
+ }
129
+
130
+ function guard(
131
+ kind: string,
132
+ input: {
133
+ key?: string;
134
+ path?: string;
135
+ permissionKey?: string;
136
+ featureKey?: string;
137
+ },
138
+ ): CapabilityGuard {
139
+ return {
140
+ label: `${kind} ${input.key || input.path || "unknown"}`,
141
+ permissionKey: input.permissionKey,
142
+ featureKey: input.featureKey,
143
+ };
144
+ }
145
+
146
+ function flattenMenus(menus: readonly MenuNode[]): MenuNode[] {
147
+ return menus.flatMap((menu) => [menu, ...flattenMenus(menu.children || [])]);
148
+ }
@@ -0,0 +1,150 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { featureKey } from "@southwind-ai/shared";
3
+ import {
4
+ dataPolicy,
5
+ fixture,
6
+ permission,
7
+ } from "./composition-fixtures.test-support.js";
8
+ import { validateModuleComposition } from "./composition.js";
9
+ import type { ModuleManifest } from "./manifest.js";
10
+ import userModule from "../user/module.js";
11
+
12
+ describe("Capability 基础 Key 组合门禁", () => {
13
+ test("Permission 缺少同基础 Key Feature 时 fail closed", () => {
14
+ const module = fixture("permission");
15
+ module.permissions.push(
16
+ permission("permission", "permission.orphan.read", "read"),
17
+ );
18
+
19
+ expect(() => validateModuleComposition([module])).toThrow(
20
+ "权限 permission.orphan.read 缺少同基础 Key Feature:permission.orphan",
21
+ );
22
+ });
23
+
24
+ test.each([
25
+ [
26
+ "菜单 binding.page",
27
+ (module: ModuleManifest) => {
28
+ module.menus[0]!.featureKey = featureKey("binding.other");
29
+ },
30
+ ],
31
+ [
32
+ "Admin Route binding.page",
33
+ (module: ModuleManifest) => {
34
+ module.adminRoutes[0]!.featureKey = featureKey("binding.other");
35
+ },
36
+ ],
37
+ [
38
+ "API Route GET /api/binding",
39
+ (module: ModuleManifest) => {
40
+ module.apiPermissions[0]!.featureKey = featureKey("binding.other");
41
+ },
42
+ ],
43
+ [
44
+ "定时任务 binding.task",
45
+ (module: ModuleManifest) => {
46
+ module.tasks[0]!.featureKey = featureKey("binding.other");
47
+ },
48
+ ],
49
+ [
50
+ "Agent Tool binding.tool",
51
+ (module: ModuleManifest) => {
52
+ module.tools[0]!.featureKey = featureKey("binding.other");
53
+ },
54
+ ],
55
+ [
56
+ "AI Operation binding.ai-operation",
57
+ (module: ModuleManifest) => {
58
+ module.aiOperations = [aiOperation()];
59
+ },
60
+ ],
61
+ [
62
+ "Provider binding.provider",
63
+ (module: ModuleManifest) => {
64
+ module.providers[0]!.featureKey = featureKey("binding.other");
65
+ },
66
+ ],
67
+ [
68
+ "数据治理策略 binding.feature.fields",
69
+ (module: ModuleManifest) => {
70
+ addDataPolicy(module);
71
+ },
72
+ ],
73
+ ] as const)("%s 的 Guard 基础 Key 漂移时 fail closed", (label, mutate) => {
74
+ const module = fixture("binding");
75
+ module.features.push({
76
+ ...module.features[0]!,
77
+ key: featureKey("binding.other"),
78
+ });
79
+ mutate(module);
80
+
81
+ expect(() => validateModuleComposition([module])).toThrow(
82
+ `${label} 的 Permission 基础 Key binding.feature 与 Feature binding.other 不一致`,
83
+ );
84
+ });
85
+
86
+ test("显式匿名 API 不要求 Permission 与 Feature 使用同一基础 Key", () => {
87
+ const module = fixture("public");
88
+ module.features.push({
89
+ ...module.features[0]!,
90
+ key: featureKey("public.other"),
91
+ });
92
+ module.apiPermissions[0]!.anonymous = true;
93
+ module.apiPermissions[0]!.featureKey = featureKey("public.other");
94
+
95
+ expect(() => validateModuleComposition([module])).not.toThrow();
96
+ });
97
+
98
+ test("示例用户模块通过完整 Capability Guard 门禁", () => {
99
+ expect(() => validateModuleComposition([userModule()])).not.toThrow();
100
+ });
101
+ });
102
+
103
+ function aiOperation(): NonNullable<ModuleManifest["aiOperations"]>[number] {
104
+ return {
105
+ key: "binding.ai-operation",
106
+ kind: "agent",
107
+ featureKey: featureKey("binding.other"),
108
+ permissionKey: "binding.feature.read",
109
+ auditAction: "agent.tool.execute",
110
+ inputSchemaKey: "binding.tool.input.v1",
111
+ outputSchemaKey: "binding.tool.output.v1",
112
+ requiredCapabilities: ["text-generation"],
113
+ allowedToolKeys: [],
114
+ dataClassification: "internal",
115
+ execution: "interactive",
116
+ providerEgress: { mode: "forbidden" },
117
+ toolNetworkPolicy: { mode: "forbidden" },
118
+ limits: {
119
+ maxDurationSeconds: 10,
120
+ maxInputBytes: 4_096,
121
+ maxOutputBytes: 4_096,
122
+ },
123
+ };
124
+ }
125
+
126
+ function addDataPolicy(module: ModuleManifest): void {
127
+ module.permissions.push(
128
+ {
129
+ ...permission("binding", "binding.feature.reveal", "manage"),
130
+ action: "reveal",
131
+ accessRisk: "sensitive-disclosure",
132
+ },
133
+ {
134
+ ...permission("binding", "binding.feature.export", "manage"),
135
+ action: "export",
136
+ accessRisk: "governed-export",
137
+ },
138
+ );
139
+ module.dataPolicies = [
140
+ {
141
+ ...dataPolicy(),
142
+ key: "binding.feature.fields",
143
+ resourceType: "binding.feature",
144
+ featureKey: featureKey("binding.other"),
145
+ readPermissionKey: "binding.feature.read",
146
+ revealPermissionKey: "binding.feature.reveal",
147
+ exportPermissionKey: "binding.feature.export",
148
+ },
149
+ ];
150
+ }
@@ -6,8 +6,8 @@ export function fixture(
6
6
  dependencies: string[] = [],
7
7
  ): ModuleManifest {
8
8
  const featureKey = `${name}.feature`;
9
- const readPermission = `${name}.read`;
10
- const managePermission = `${name}.manage`;
9
+ const readPermission = `${featureKey}.read`;
10
+ const managePermission = `${featureKey}.manage`;
11
11
  return {
12
12
  name,
13
13
  version: "0.1.0",
@@ -46,6 +46,7 @@ export function fixture(
46
46
  path: `/api/${name}`,
47
47
  permissionKey: readPermission,
48
48
  featureKey,
49
+ auditAction: "data.search",
49
50
  },
50
51
  ],
51
52
  features: [
@@ -83,6 +84,7 @@ export function fixture(
83
84
  description: name,
84
85
  permissionKey: managePermission,
85
86
  featureKey,
87
+ auditAction: "task.execute",
86
88
  intervalSeconds: 60,
87
89
  maxAttempts: 3,
88
90
  retryDelaySeconds: 10,
@@ -131,9 +133,9 @@ export function dataPolicy(): NonNullable<
131
133
  resourceType: "system.user",
132
134
  version: "1",
133
135
  featureKey: featureKey("system.feature"),
134
- readPermissionKey: "system.read",
135
- revealPermissionKey: "system.reveal",
136
- exportPermissionKey: "system.export",
136
+ readPermissionKey: "system.feature.read",
137
+ revealPermissionKey: "system.feature.reveal",
138
+ exportPermissionKey: "system.feature.export",
137
139
  display: {
138
140
  read: "masked",
139
141
  reveal: "plaintext",
@@ -35,11 +35,11 @@ describe("Module Composition 契约", () => {
35
35
  const duplicate = fixture("duplicate");
36
36
  duplicate.permissions[0] = {
37
37
  ...duplicate.permissions[0]!,
38
- key: "system.read",
38
+ key: "system.feature.read",
39
39
  };
40
40
  expect(() =>
41
41
  validateModuleComposition([fixture("system"), duplicate]),
42
- ).toThrow("权限 Key 重复:system.read");
42
+ ).toThrow("权限 Key 重复:system.feature.read");
43
43
  });
44
44
 
45
45
  test("未知 Permission、Feature 与 Audit 引用会被拒绝", () => {
@@ -58,6 +58,71 @@ describe("Module Composition 契约", () => {
58
58
  expect(() => validateModuleComposition([audit])).toThrow("未注册审计动作");
59
59
  });
60
60
 
61
+ test.each([
62
+ [
63
+ "菜单",
64
+ (module: ReturnType<typeof fixture>) => {
65
+ module.menus[0]!.path = undefined;
66
+ module.menus[0]!.featureKey = undefined;
67
+ },
68
+ ],
69
+ [
70
+ "Admin Route",
71
+ (module: ReturnType<typeof fixture>) => {
72
+ module.menus = [];
73
+ module.adminRoutes[0]!.featureKey = undefined;
74
+ },
75
+ ],
76
+ [
77
+ "API Route",
78
+ (module: ReturnType<typeof fixture>) => {
79
+ module.apiPermissions[0]!.featureKey = undefined;
80
+ },
81
+ ],
82
+ [
83
+ "定时任务",
84
+ (module: ReturnType<typeof fixture>) => {
85
+ Reflect.deleteProperty(module.tasks[0]!, "featureKey");
86
+ },
87
+ ],
88
+ [
89
+ "Agent Tool",
90
+ (module: ReturnType<typeof fixture>) => {
91
+ Reflect.deleteProperty(module.tools[0]!, "featureKey");
92
+ },
93
+ ],
94
+ [
95
+ "Provider",
96
+ (module: ReturnType<typeof fixture>) => {
97
+ module.providers[0]!.featureKey = undefined;
98
+ },
99
+ ],
100
+ ] as const)("%s 缺少 Feature Guard 时 fail closed", (_, removeFeature) => {
101
+ const module = fixture("guard");
102
+ removeFeature(module);
103
+
104
+ expect(() => validateModuleComposition([module])).toThrow(
105
+ "必须同时声明 Permission 与 Feature",
106
+ );
107
+ });
108
+
109
+ test("业务入口缺少 Permission Guard 时 fail closed", () => {
110
+ const module = fixture("guard");
111
+ Reflect.deleteProperty(module.apiPermissions[0]!, "permissionKey");
112
+
113
+ expect(() => validateModuleComposition([module])).toThrow(
114
+ "必须同时声明 Permission 与 Feature",
115
+ );
116
+ });
117
+
118
+ test("显式匿名 API 可以不声明 Feature Guard", () => {
119
+ const module = fixture("public");
120
+ module.apiPermissions[0]!.anonymous = true;
121
+ module.apiPermissions[0]!.featureKey = undefined;
122
+
123
+ expect(() => validateModuleComposition([module])).not.toThrow();
124
+ });
125
+
61
126
  test("业务模块可注册带展示信息的审计动作目录", () => {
62
127
  const module = fixture("ticket");
63
128
  module.auditActions.push({
@@ -133,7 +198,7 @@ describe("Module Composition 契约", () => {
133
198
 
134
199
  test("菜单 Guard 必须与 Admin Route 一致", () => {
135
200
  const module = fixture("system");
136
- module.adminRoutes[0]!.permissionKey = "system.manage";
201
+ module.adminRoutes[0]!.permissionKey = "system.feature.manage";
137
202
  expect(() => validateModuleComposition([module])).toThrow(
138
203
  "Admin Route Guard 不一致",
139
204
  );
@@ -144,12 +209,12 @@ describe("Module Composition 契约", () => {
144
209
  module.adminRoutes[0] = {
145
210
  ...module.adminRoutes[0]!,
146
211
  directAccess: "admin-admission",
147
- permissionKey: "system.manage",
212
+ permissionKey: "system.feature.manage",
148
213
  };
149
214
 
150
215
  expect(() => validateModuleComposition([module])).not.toThrow();
151
216
  expect(module.menus[0]).toMatchObject({
152
- permissionKey: "system.read",
217
+ permissionKey: "system.feature.read",
153
218
  featureKey: "system.feature",
154
219
  });
155
220
  });
@@ -197,12 +262,12 @@ describe("Module Composition 契约", () => {
197
262
  test("字段治理策略校验唯一资源、权限风险和已注册字段", () => {
198
263
  const module = fixture("system");
199
264
  module.permissions.push({
200
- ...permission("system", "system.reveal", "manage"),
265
+ ...permission("system", "system.feature.reveal", "manage"),
201
266
  action: "reveal",
202
267
  accessRisk: "sensitive-disclosure",
203
268
  });
204
269
  module.permissions.push({
205
- ...permission("system", "system.export", "manage"),
270
+ ...permission("system", "system.feature.export", "manage"),
206
271
  action: "export",
207
272
  accessRisk: "governed-export",
208
273
  });
@@ -220,8 +285,8 @@ describe("Module Composition 契约", () => {
220
285
  module.dataPolicies = [dataPolicy()];
221
286
  expect(() => validateModuleComposition([module])).toThrow("未注册权限");
222
287
 
223
- module.dataPolicies[0]!.revealPermissionKey = "system.manage";
224
- module.dataPolicies[0]!.exportPermissionKey = "system.manage";
288
+ module.dataPolicies[0]!.revealPermissionKey = "system.feature.manage";
289
+ module.dataPolicies[0]!.exportPermissionKey = "system.feature.manage";
225
290
  expect(() => validateModuleComposition([module])).toThrow(
226
291
  "明文权限必须标记 sensitive-disclosure",
227
292
  );
@@ -4,6 +4,11 @@ import type {
4
4
  ModuleManifest,
5
5
  } from "./manifest.js";
6
6
  import { validateAiOperations } from "./ai-operation-composition.js";
7
+ import { validateCapabilityAuditActions } from "./capability-audit-validation.js";
8
+ import {
9
+ validateCapabilityGuardCompleteness,
10
+ validateCapabilityKeyAlignment,
11
+ } from "./capability-guard-validation.js";
7
12
  import { validateImplementationBindings } from "./implementation-bindings.js";
8
13
  import { composeModuleMenus } from "./menu-composition.js";
9
14
  import { validateCapabilityCatalogs } from "./catalog-validation.js";
@@ -27,6 +32,7 @@ export function validateModuleComposition(
27
32
  modules.map(({ name }) => name),
28
33
  );
29
34
  const moduleOrder = validateDependencies(modules, moduleNames);
35
+ validateCapabilityGuardCompleteness(modules);
30
36
  const permissions = unique(
31
37
  "权限",
32
38
  modules.flatMap((module) => module.permissions.map(({ key }) => key)),
@@ -35,9 +41,11 @@ export function validateModuleComposition(
35
41
  "Feature",
36
42
  modules.flatMap((module) => module.features.map(({ key }) => key)),
37
43
  );
44
+ validateCapabilityKeyAlignment(modules);
38
45
  const catalogs = validateCapabilityCatalogs(modules);
39
46
  const jsonSchemas = validateToolContracts(modules);
40
47
  const audits = new Set(catalogs.auditActions.map(({ key }) => key));
48
+ validateCapabilityAuditActions(modules, audits);
41
49
  const menus = composeModuleMenus(modules).flatMap((menu) =>
42
50
  flattenMenus([menu]),
43
51
  );
@@ -80,6 +80,7 @@ export interface ModuleScheduledTaskDefinition {
80
80
  description: string;
81
81
  permissionKey: string;
82
82
  featureKey: string;
83
+ auditAction: AuditActionType;
83
84
  intervalSeconds: number;
84
85
  maxAttempts: number;
85
86
  retryDelaySeconds: number;