openxiangda 1.0.264 → 2.0.0-alpha.2

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 (537) hide show
  1. package/README.md +11 -666
  2. package/bin/run.js +11 -0
  3. package/dist/browser/AuthoritativeSelector.d.ts +37 -0
  4. package/dist/browser/AuthoritativeSelector.d.ts.map +1 -0
  5. package/dist/browser/AuthoritativeSelector.js +357 -0
  6. package/dist/browser/AuthoritativeSelector.js.map +1 -0
  7. package/dist/browser/FilePreviewPage.d.ts +2 -0
  8. package/dist/browser/FilePreviewPage.d.ts.map +1 -0
  9. package/dist/browser/FilePreviewPage.js +191 -0
  10. package/dist/browser/FilePreviewPage.js.map +1 -0
  11. package/dist/browser/Shell.d.ts +6 -0
  12. package/dist/browser/Shell.d.ts.map +1 -0
  13. package/dist/browser/Shell.js +198 -0
  14. package/dist/browser/Shell.js.map +1 -0
  15. package/dist/browser/appearance.d.ts +12 -0
  16. package/dist/browser/appearance.d.ts.map +1 -0
  17. package/dist/browser/appearance.js +100 -0
  18. package/dist/browser/appearance.js.map +1 -0
  19. package/dist/browser/application.d.ts +8 -0
  20. package/dist/browser/application.d.ts.map +1 -0
  21. package/dist/browser/application.js +42 -0
  22. package/dist/browser/application.js.map +1 -0
  23. package/dist/browser/components/platform-fields/AddressField.d.ts +12 -0
  24. package/dist/browser/components/platform-fields/AddressField.d.ts.map +1 -0
  25. package/dist/browser/components/platform-fields/AddressField.js +179 -0
  26. package/dist/browser/components/platform-fields/AddressField.js.map +1 -0
  27. package/dist/browser/components/platform-fields/AttachmentFileList.d.ts +9 -0
  28. package/dist/browser/components/platform-fields/AttachmentFileList.d.ts.map +1 -0
  29. package/dist/browser/components/platform-fields/AttachmentFileList.js +164 -0
  30. package/dist/browser/components/platform-fields/AttachmentFileList.js.map +1 -0
  31. package/dist/browser/components/platform-fields/CascadeField.d.ts +13 -0
  32. package/dist/browser/components/platform-fields/CascadeField.d.ts.map +1 -0
  33. package/dist/browser/components/platform-fields/CascadeField.js +14 -0
  34. package/dist/browser/components/platform-fields/CascadeField.js.map +1 -0
  35. package/dist/browser/components/platform-fields/DateTimeField.d.ts +16 -0
  36. package/dist/browser/components/platform-fields/DateTimeField.d.ts.map +1 -0
  37. package/dist/browser/components/platform-fields/DateTimeField.js +51 -0
  38. package/dist/browser/components/platform-fields/DateTimeField.js.map +1 -0
  39. package/dist/browser/components/platform-fields/JsonField.d.ts +11 -0
  40. package/dist/browser/components/platform-fields/JsonField.d.ts.map +1 -0
  41. package/dist/browser/components/platform-fields/JsonField.js +40 -0
  42. package/dist/browser/components/platform-fields/JsonField.js.map +1 -0
  43. package/dist/browser/components/platform-fields/LocationField.d.ts +11 -0
  44. package/dist/browser/components/platform-fields/LocationField.d.ts.map +1 -0
  45. package/dist/browser/components/platform-fields/LocationField.js +73 -0
  46. package/dist/browser/components/platform-fields/LocationField.js.map +1 -0
  47. package/dist/browser/components/platform-fields/PlatformDirectoryPicker.d.ts +12 -0
  48. package/dist/browser/components/platform-fields/PlatformDirectoryPicker.d.ts.map +1 -0
  49. package/dist/browser/components/platform-fields/PlatformDirectoryPicker.js +269 -0
  50. package/dist/browser/components/platform-fields/PlatformDirectoryPicker.js.map +1 -0
  51. package/dist/browser/components/platform-fields/ResourceReferenceField.d.ts +12 -0
  52. package/dist/browser/components/platform-fields/ResourceReferenceField.d.ts.map +1 -0
  53. package/dist/browser/components/platform-fields/ResourceReferenceField.js +22 -0
  54. package/dist/browser/components/platform-fields/ResourceReferenceField.js.map +1 -0
  55. package/dist/browser/components/platform-fields/RichTextField.d.ts +14 -0
  56. package/dist/browser/components/platform-fields/RichTextField.d.ts.map +1 -0
  57. package/dist/browser/components/platform-fields/RichTextField.js +214 -0
  58. package/dist/browser/components/platform-fields/RichTextField.js.map +1 -0
  59. package/dist/browser/components/platform-fields/SignatureField.d.ts +15 -0
  60. package/dist/browser/components/platform-fields/SignatureField.d.ts.map +1 -0
  61. package/dist/browser/components/platform-fields/SignatureField.js +179 -0
  62. package/dist/browser/components/platform-fields/SignatureField.js.map +1 -0
  63. package/dist/browser/components/platform-fields/SubtableField.d.ts +18 -0
  64. package/dist/browser/components/platform-fields/SubtableField.d.ts.map +1 -0
  65. package/dist/browser/components/platform-fields/SubtableField.js +236 -0
  66. package/dist/browser/components/platform-fields/SubtableField.js.map +1 -0
  67. package/dist/browser/components/platform-fields/address-value.d.ts +13 -0
  68. package/dist/browser/components/platform-fields/address-value.d.ts.map +1 -0
  69. package/dist/browser/components/platform-fields/address-value.js +70 -0
  70. package/dist/browser/components/platform-fields/address-value.js.map +1 -0
  71. package/dist/browser/components/platform-fields/cascade-value.d.ts +6 -0
  72. package/dist/browser/components/platform-fields/cascade-value.d.ts.map +1 -0
  73. package/dist/browser/components/platform-fields/cascade-value.js +47 -0
  74. package/dist/browser/components/platform-fields/cascade-value.js.map +1 -0
  75. package/dist/browser/components/platform-fields/directory-value.d.ts +7 -0
  76. package/dist/browser/components/platform-fields/directory-value.d.ts.map +1 -0
  77. package/dist/browser/components/platform-fields/directory-value.js +35 -0
  78. package/dist/browser/components/platform-fields/directory-value.js.map +1 -0
  79. package/dist/browser/components/platform-fields/field-form-codec.d.ts +10 -0
  80. package/dist/browser/components/platform-fields/field-form-codec.d.ts.map +1 -0
  81. package/dist/browser/components/platform-fields/field-form-codec.js +82 -0
  82. package/dist/browser/components/platform-fields/field-form-codec.js.map +1 -0
  83. package/dist/browser/components/platform-fields/location-value.d.ts +20 -0
  84. package/dist/browser/components/platform-fields/location-value.d.ts.map +1 -0
  85. package/dist/browser/components/platform-fields/location-value.js +57 -0
  86. package/dist/browser/components/platform-fields/location-value.js.map +1 -0
  87. package/dist/browser/components/platform-fields/resource-query.d.ts +14 -0
  88. package/dist/browser/components/platform-fields/resource-query.d.ts.map +1 -0
  89. package/dist/browser/components/platform-fields/resource-query.js +102 -0
  90. package/dist/browser/components/platform-fields/resource-query.js.map +1 -0
  91. package/dist/browser/components/platform-fields/rich-text-value.d.ts +12 -0
  92. package/dist/browser/components/platform-fields/rich-text-value.d.ts.map +1 -0
  93. package/dist/browser/components/platform-fields/rich-text-value.js +83 -0
  94. package/dist/browser/components/platform-fields/rich-text-value.js.map +1 -0
  95. package/dist/browser/components/platform-fields/subtable-value.d.ts +31 -0
  96. package/dist/browser/components/platform-fields/subtable-value.d.ts.map +1 -0
  97. package/dist/browser/components/platform-fields/subtable-value.js +120 -0
  98. package/dist/browser/components/platform-fields/subtable-value.js.map +1 -0
  99. package/dist/browser/components/resource/GeneratedResourceCrud.d.ts +8 -0
  100. package/dist/browser/components/resource/GeneratedResourceCrud.d.ts.map +1 -0
  101. package/dist/browser/components/resource/GeneratedResourceCrud.js +580 -0
  102. package/dist/browser/components/resource/GeneratedResourceCrud.js.map +1 -0
  103. package/dist/browser/components/resource/ResourceBatchActions.d.ts +25 -0
  104. package/dist/browser/components/resource/ResourceBatchActions.d.ts.map +1 -0
  105. package/dist/browser/components/resource/ResourceBatchActions.js +168 -0
  106. package/dist/browser/components/resource/ResourceBatchActions.js.map +1 -0
  107. package/dist/browser/components/resource/StandardResourcePages.d.ts +57 -0
  108. package/dist/browser/components/resource/StandardResourcePages.d.ts.map +1 -0
  109. package/dist/browser/components/resource/StandardResourcePages.js +89 -0
  110. package/dist/browser/components/resource/StandardResourcePages.js.map +1 -0
  111. package/dist/browser/components/resource/SurfaceFields.d.ts +88 -0
  112. package/dist/browser/components/resource/SurfaceFields.d.ts.map +1 -0
  113. package/dist/browser/components/resource/SurfaceFields.js +497 -0
  114. package/dist/browser/components/resource/SurfaceFields.js.map +1 -0
  115. package/dist/browser/components/resource/generated-resource-definition.d.ts +19 -0
  116. package/dist/browser/components/resource/generated-resource-definition.d.ts.map +1 -0
  117. package/dist/browser/components/resource/generated-resource-definition.js +2 -0
  118. package/dist/browser/components/resource/generated-resource-definition.js.map +1 -0
  119. package/dist/browser/components/resource/resource-import.d.ts +15 -0
  120. package/dist/browser/components/resource/resource-import.d.ts.map +1 -0
  121. package/dist/browser/components/resource/resource-import.js +197 -0
  122. package/dist/browser/components/resource/resource-import.js.map +1 -0
  123. package/dist/browser/data-provider.d.ts +4 -0
  124. package/dist/browser/data-provider.d.ts.map +1 -0
  125. package/dist/browser/data-provider.js +45 -0
  126. package/dist/browser/data-provider.js.map +1 -0
  127. package/dist/browser/platform-client.d.ts +81 -0
  128. package/dist/browser/platform-client.d.ts.map +1 -0
  129. package/dist/browser/platform-client.js +422 -0
  130. package/dist/browser/platform-client.js.map +1 -0
  131. package/dist/browser/resource-definitions.d.ts +8 -0
  132. package/dist/browser/resource-definitions.d.ts.map +1 -0
  133. package/dist/browser/resource-definitions.js +14 -0
  134. package/dist/browser/resource-definitions.js.map +1 -0
  135. package/dist/browser/runtime-meta.d.ts +22 -0
  136. package/dist/browser/runtime-meta.d.ts.map +1 -0
  137. package/dist/browser/runtime-meta.js +79 -0
  138. package/dist/browser/runtime-meta.js.map +1 -0
  139. package/dist/browser/runtime.d.ts +13 -0
  140. package/dist/browser/runtime.d.ts.map +1 -0
  141. package/dist/browser/runtime.js +47 -0
  142. package/dist/browser/runtime.js.map +1 -0
  143. package/dist/browser/styles.css +1941 -0
  144. package/dist/config.d.ts +2 -0
  145. package/dist/config.d.ts.map +1 -0
  146. package/dist/config.js +2 -0
  147. package/dist/config.js.map +1 -0
  148. package/dist/core.d.ts +6 -0
  149. package/dist/core.d.ts.map +1 -0
  150. package/dist/core.js +4 -0
  151. package/dist/core.js.map +1 -0
  152. package/dist/field-kit.d.ts +22 -0
  153. package/dist/field-kit.d.ts.map +1 -0
  154. package/dist/field-kit.js +22 -0
  155. package/dist/field-kit.js.map +1 -0
  156. package/dist/index.d.ts +2 -0
  157. package/dist/index.d.ts.map +1 -0
  158. package/dist/index.js +2 -0
  159. package/dist/index.js.map +1 -0
  160. package/dist/nest.d.ts +10 -0
  161. package/dist/nest.d.ts.map +1 -0
  162. package/dist/nest.js +9 -0
  163. package/dist/nest.js.map +1 -0
  164. package/dist/react.d.ts +12 -0
  165. package/dist/react.d.ts.map +1 -0
  166. package/dist/react.js +11 -0
  167. package/dist/react.js.map +1 -0
  168. package/dist/testing.d.ts +4 -0
  169. package/dist/testing.d.ts.map +1 -0
  170. package/dist/testing.js +4 -0
  171. package/dist/testing.js.map +1 -0
  172. package/package.json +95 -200
  173. package/skills/manifest.json +10 -0
  174. package/skills/openxiangda-v2/SKILL.md +55 -0
  175. package/skills/openxiangda-v2/references/architecture.md +7 -0
  176. package/skills/openxiangda-v2/references/backend.md +116 -0
  177. package/skills/openxiangda-v2/references/data-authz.md +93 -0
  178. package/skills/openxiangda-v2/references/delivery.md +11 -0
  179. package/skills/openxiangda-v2/references/discovery.md +15 -0
  180. package/skills/openxiangda-v2/references/frontend.md +10 -0
  181. package/skills/openxiangda-v2/references/testing.md +16 -0
  182. package/skills/openxiangda-v2/references/workflow-notification.md +69 -0
  183. package/skills/openxiangda-v2/references/workspace.md +21 -0
  184. package/bin/openxiangda.js +0 -26
  185. package/lib/app-function-invoke.js +0 -28
  186. package/lib/application-environments.js +0 -1134
  187. package/lib/change-baseline.js +0 -1359
  188. package/lib/cli.js +0 -31084
  189. package/lib/config.js +0 -450
  190. package/lib/content-cache.js +0 -97
  191. package/lib/delivery-v2-cli.js +0 -329
  192. package/lib/delivery-v2-executor.js +0 -1703
  193. package/lib/delivery-v2-package.js +0 -1176
  194. package/lib/dependency-capsule.js +0 -246
  195. package/lib/design-gates.js +0 -668
  196. package/lib/design-review.js +0 -220
  197. package/lib/developer-center.js +0 -160
  198. package/lib/form-field-contract.js +0 -79
  199. package/lib/form-schema-loader.js +0 -181
  200. package/lib/git-remote-identity.js +0 -89
  201. package/lib/http.js +0 -196
  202. package/lib/integration-bundle.js +0 -225
  203. package/lib/js-code-build.js +0 -175
  204. package/lib/open-api.js +0 -256
  205. package/lib/policy.js +0 -137
  206. package/lib/publish-context.js +0 -98
  207. package/lib/publish-lease-heartbeat.js +0 -182
  208. package/lib/publish-lease.js +0 -221
  209. package/lib/release-error-classification.js +0 -41
  210. package/lib/release-explain.js +0 -75
  211. package/lib/release-mainline.js +0 -654
  212. package/lib/release-plan.js +0 -550
  213. package/lib/release-telemetry.js +0 -182
  214. package/lib/resource-binding-contract.js +0 -210
  215. package/lib/role-resolution.js +0 -57
  216. package/lib/runtime-lineage.js +0 -326
  217. package/lib/sdd.js +0 -3306
  218. package/lib/skills.js +0 -453
  219. package/lib/source-dependencies.js +0 -921
  220. package/lib/task-status.js +0 -330
  221. package/lib/utils.js +0 -178
  222. package/lib/workspace-bootstrap.js +0 -268
  223. package/lib/workspace-init.js +0 -202
  224. package/lib/worktree-cleanup.js +0 -631
  225. package/lib/worktree-owner.js +0 -354
  226. package/openxiangda-skills/SKILL.md +0 -226
  227. package/openxiangda-skills/references/architecture-design.md +0 -409
  228. package/openxiangda-skills/references/architecture-patterns.md +0 -264
  229. package/openxiangda-skills/references/automation-v3.md +0 -349
  230. package/openxiangda-skills/references/best-practices.md +0 -272
  231. package/openxiangda-skills/references/component-guide.md +0 -287
  232. package/openxiangda-skills/references/connector-resources.md +0 -80
  233. package/openxiangda-skills/references/data-views.md +0 -360
  234. package/openxiangda-skills/references/forms/component-registry.md +0 -90
  235. package/openxiangda-skills/references/forms/form-schema.md +0 -146
  236. package/openxiangda-skills/references/forms/layout-and-rules.md +0 -24
  237. package/openxiangda-skills/references/notifications.md +0 -227
  238. package/openxiangda-skills/references/openxiangda-api.md +0 -971
  239. package/openxiangda-skills/references/pages/app-shell.md +0 -66
  240. package/openxiangda-skills/references/pages/page-sdk.md +0 -401
  241. package/openxiangda-skills/references/pages/publish-flow.md +0 -84
  242. package/openxiangda-skills/references/pages/workspace-structure.md +0 -41
  243. package/openxiangda-skills/references/permission-design-patterns.md +0 -313
  244. package/openxiangda-skills/references/permissions-settings.md +0 -468
  245. package/openxiangda-skills/references/platform-data-model.md +0 -340
  246. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +0 -936
  247. package/openxiangda-skills/references/school-contact-relations.md +0 -224
  248. package/openxiangda-skills/references/style-system.md +0 -218
  249. package/openxiangda-skills/references/troubleshooting.md +0 -271
  250. package/openxiangda-skills/references/workflow-v3.md +0 -441
  251. package/openxiangda-skills/references/workspace-state.md +0 -115
  252. package/openxiangda-skills/skills/openxiangda-app/SKILL.md +0 -118
  253. package/openxiangda-skills/skills/openxiangda-architecture-design/SKILL.md +0 -137
  254. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +0 -222
  255. package/openxiangda-skills/skills/openxiangda-form/SKILL.md +0 -184
  256. package/openxiangda-skills/skills/openxiangda-inspect/SKILL.md +0 -63
  257. package/openxiangda-skills/skills/openxiangda-open-api/SKILL.md +0 -57
  258. package/openxiangda-skills/skills/openxiangda-open-api/agents/openai.yaml +0 -4
  259. package/openxiangda-skills/skills/openxiangda-open-api/references/backend-authentication.md +0 -63
  260. package/openxiangda-skills/skills/openxiangda-open-api/references/dingtalk-api.openapi.json +0 -15653
  261. package/openxiangda-skills/skills/openxiangda-page/SKILL.md +0 -154
  262. package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +0 -209
  263. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +0 -201
  264. package/packages/sdk/bin/lowcode-workspace.mjs +0 -4
  265. package/packages/sdk/dist/ProcessPreview-DSUIJi5V.d.mts +0 -1292
  266. package/packages/sdk/dist/ProcessPreview-DSUIJi5V.d.ts +0 -1292
  267. package/packages/sdk/dist/build/index.cjs +0 -1
  268. package/packages/sdk/dist/build/index.d.mts +0 -60
  269. package/packages/sdk/dist/build/index.d.ts +0 -60
  270. package/packages/sdk/dist/build/index.mjs +0 -1
  271. package/packages/sdk/dist/components/index.cjs +0 -148
  272. package/packages/sdk/dist/components/index.d.mts +0 -970
  273. package/packages/sdk/dist/components/index.d.ts +0 -970
  274. package/packages/sdk/dist/components/index.mjs +0 -148
  275. package/packages/sdk/dist/dataManagementApi-Ro9itwm8.d.ts +0 -142
  276. package/packages/sdk/dist/dataManagementApi-p_HOhQnA.d.mts +0 -142
  277. package/packages/sdk/dist/runtime/index.cjs +0 -148
  278. package/packages/sdk/dist/runtime/index.d.mts +0 -171
  279. package/packages/sdk/dist/runtime/index.d.ts +0 -171
  280. package/packages/sdk/dist/runtime/index.mjs +0 -148
  281. package/packages/sdk/dist/runtime/react.cjs +0 -148
  282. package/packages/sdk/dist/runtime/react.d.mts +0 -3202
  283. package/packages/sdk/dist/runtime/react.d.ts +0 -3202
  284. package/packages/sdk/dist/runtime/react.mjs +0 -148
  285. package/packages/sdk/dist/styles/antd-theme.cjs +0 -1
  286. package/packages/sdk/dist/styles/antd-theme.d.mts +0 -6
  287. package/packages/sdk/dist/styles/antd-theme.d.ts +0 -6
  288. package/packages/sdk/dist/styles/antd-theme.mjs +0 -1
  289. package/packages/sdk/dist/styles/tailwind-preset.cjs +0 -1
  290. package/packages/sdk/dist/styles/tailwind-preset.d.mts +0 -74
  291. package/packages/sdk/dist/styles/tailwind-preset.d.ts +0 -74
  292. package/packages/sdk/dist/styles/tailwind-preset.mjs +0 -1
  293. package/packages/sdk/dist/styles/tokens.css +0 -74
  294. package/packages/sdk/dist/workflow/index.cjs +0 -3
  295. package/packages/sdk/dist/workflow/index.d.mts +0 -291
  296. package/packages/sdk/dist/workflow/index.d.ts +0 -291
  297. package/packages/sdk/dist/workflow/index.mjs +0 -3
  298. package/packages/sdk/source/admin-list/AdminList.tsx +0 -1734
  299. package/packages/sdk/source/admin-list/README.md +0 -12
  300. package/packages/sdk/source/admin-list/dataSources.ts +0 -345
  301. package/packages/sdk/source/admin-list/index.ts +0 -42
  302. package/packages/sdk/source/admin-list/preferences.ts +0 -174
  303. package/packages/sdk/source/admin-list/types.ts +0 -273
  304. package/packages/sdk/src/build-source/README.md +0 -9
  305. package/packages/sdk/src/build-source/bin/lowcode-workspace.mjs +0 -7
  306. package/packages/sdk/src/build-source/package.json +0 -34
  307. package/packages/sdk/src/build-source/scripts/build-forms.mjs +0 -1129
  308. package/packages/sdk/src/build-source/scripts/build-pages.mjs +0 -897
  309. package/packages/sdk/src/build-source/scripts/build-workspace.mjs +0 -89
  310. package/packages/sdk/src/build-source/scripts/publish-all.mjs +0 -190
  311. package/packages/sdk/src/build-source/scripts/publish-oss.mjs +0 -144
  312. package/packages/sdk/src/build-source/scripts/register-bundle.mjs +0 -1
  313. package/packages/sdk/src/build-source/scripts/register.mjs +0 -362
  314. package/packages/sdk/src/build-source/scripts/sync-schema.mjs +0 -381
  315. package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +0 -1058
  316. package/packages/sdk/src/build-source/scripts/utils/form-runtime-assets.mjs +0 -57
  317. package/packages/sdk/src/build-source/scripts/utils/incremental.mjs +0 -348
  318. package/packages/sdk/src/build-source/scripts/utils/load-config.mjs +0 -331
  319. package/packages/sdk/src/build-source/scripts/utils/mime-types.mjs +0 -70
  320. package/packages/sdk/src/build-source/scripts/utils/namespace-css.mjs +0 -77
  321. package/packages/sdk/src/build-source/scripts/utils/oss-client.mjs +0 -128
  322. package/packages/sdk/src/build-source/scripts/utils/pages.mjs +0 -150
  323. package/packages/sdk/src/build-source/scripts/utils/progress.mjs +0 -57
  324. package/packages/sdk/src/build-source/scripts/utils/publish-oss-patterns.mjs +0 -20
  325. package/packages/sdk/src/build-source/scripts/utils/register-payload.mjs +0 -90
  326. package/packages/sdk/src/build-source/scripts/utils/runtime-css-check.mjs +0 -44
  327. package/packages/sdk/src/build-source/scripts/utils/schema-transform.mjs +0 -142
  328. package/packages/sdk/src/build-source/scripts/utils/static-assets.mjs +0 -165
  329. package/packages/sdk/src/build-source/scripts/utils/tailwind-config.mjs +0 -283
  330. package/packages/sdk/src/build-source/scripts/utils/tailwind-token-warnings.mjs +0 -146
  331. package/packages/sdk/src/build-source/scripts/utils/workspace-style-entry.mjs +0 -29
  332. package/packages/sdk/src/build-source/src/cli.mjs +0 -735
  333. package/policy/openxiangda-policy.json +0 -72
  334. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-form.mdc +0 -24
  335. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-page.mdc +0 -25
  336. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +0 -32
  337. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-workflow-automation.mdc +0 -26
  338. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +0 -68
  339. package/templates/openxiangda-react-spa/.env.example +0 -4
  340. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-form.md +0 -37
  341. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-page.md +0 -29
  342. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +0 -32
  343. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-workflow-automation.md +0 -34
  344. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +0 -66
  345. package/templates/openxiangda-react-spa/AGENTS.md +0 -192
  346. package/templates/openxiangda-react-spa/DELIVERY.md +0 -43
  347. package/templates/openxiangda-react-spa/app-workspace.config.ts +0 -50
  348. package/templates/openxiangda-react-spa/index.html +0 -12
  349. package/templates/openxiangda-react-spa/package.json +0 -53
  350. package/templates/openxiangda-react-spa/postcss.config.cjs +0 -6
  351. package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +0 -600
  352. package/templates/openxiangda-react-spa/scripts/deploy.mjs +0 -18
  353. package/templates/openxiangda-react-spa/scripts/guard-publish.mjs +0 -21
  354. package/templates/openxiangda-react-spa/src/app/navigation.ts +0 -73
  355. package/templates/openxiangda-react-spa/src/app/router.tsx +0 -146
  356. package/templates/openxiangda-react-spa/src/app/starter-content.ts +0 -144
  357. package/templates/openxiangda-react-spa/src/forms/.gitkeep +0 -1
  358. package/templates/openxiangda-react-spa/src/layouts/AdminShell.tsx +0 -570
  359. package/templates/openxiangda-react-spa/src/main.tsx +0 -21
  360. package/templates/openxiangda-react-spa/src/pages/admin/AdminDashboardPage.tsx +0 -170
  361. package/templates/openxiangda-react-spa/src/pages/admin/LoginLogPage.tsx +0 -425
  362. package/templates/openxiangda-react-spa/src/pages/defaults/DataRoutePage.tsx +0 -36
  363. package/templates/openxiangda-react-spa/src/pages/defaults/FilePreviewRoutePage.tsx +0 -65
  364. package/templates/openxiangda-react-spa/src/pages/defaults/FormRoutePage.tsx +0 -235
  365. package/templates/openxiangda-react-spa/src/pages/public/PublicRegisterPage.tsx +0 -15
  366. package/templates/openxiangda-react-spa/src/pages/states/NotFoundPage.tsx +0 -31
  367. package/templates/openxiangda-react-spa/src/resources/menus/menus.json +0 -22
  368. package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/app-admin.json +0 -8
  369. package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/public-visitor.json +0 -8
  370. package/templates/openxiangda-react-spa/src/resources/public-access/public-register.json +0 -14
  371. package/templates/openxiangda-react-spa/src/resources/roles/roles.json +0 -15
  372. package/templates/openxiangda-react-spa/src/resources/routes/public-register.json +0 -8
  373. package/templates/openxiangda-react-spa/src/runtime/default-page-overrides.tsx +0 -35
  374. package/templates/openxiangda-react-spa/src/runtime/default-routes.ts +0 -16
  375. package/templates/openxiangda-react-spa/src/shared/form-schema.ts +0 -135
  376. package/templates/openxiangda-react-spa/src/shared/mac-admin.tsx +0 -651
  377. package/templates/openxiangda-react-spa/src/shared/ui.tsx +0 -22
  378. package/templates/openxiangda-react-spa/src/styles/index.css +0 -105
  379. package/templates/openxiangda-react-spa/tailwind.config.cjs +0 -37
  380. package/templates/openxiangda-react-spa/tsconfig.app.json +0 -36
  381. package/templates/openxiangda-react-spa/tsconfig.js-code-nodes.json +0 -19
  382. package/templates/openxiangda-react-spa/tsconfig.json +0 -7
  383. package/templates/openxiangda-react-spa/tsconfig.node.json +0 -10
  384. package/templates/openxiangda-react-spa/vite.config.ts +0 -209
  385. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-form.mdc +0 -24
  386. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-page.mdc +0 -21
  387. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +0 -40
  388. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-workflow-automation.mdc +0 -21
  389. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +0 -81
  390. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-form.md +0 -38
  391. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-page.md +0 -39
  392. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +0 -59
  393. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-workflow-automation.md +0 -46
  394. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +0 -82
  395. package/templates/sy-lowcode-app-workspace/AGENTS.md +0 -158
  396. package/templates/sy-lowcode-app-workspace/DELIVERY.md +0 -43
  397. package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +0 -51
  398. package/templates/sy-lowcode-app-workspace/examples/best-practices/README.md +0 -43
  399. package/templates/sy-lowcode-app-workspace/examples/best-practices/access-governance.md +0 -239
  400. package/templates/sy-lowcode-app-workspace/examples/best-practices/catalog.json +0 -99
  401. package/templates/sy-lowcode-app-workspace/examples/best-practices/decision-guide.md +0 -94
  402. package/templates/sy-lowcode-app-workspace/examples/best-practices/design-style.md +0 -91
  403. package/templates/sy-lowcode-app-workspace/examples/best-practices/module-structure.md +0 -48
  404. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/index.ts +0 -2
  405. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.test.ts +0 -52
  406. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.ts +0 -37
  407. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/types.ts +0 -19
  408. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/index.ts +0 -4
  409. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.test.ts +0 -42
  410. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.ts +0 -25
  411. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.test.ts +0 -63
  412. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.ts +0 -73
  413. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.test.ts +0 -34
  414. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.ts +0 -73
  415. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/types.ts +0 -66
  416. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/page.tsx +0 -1
  417. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/schema.ts +0 -109
  418. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/page.tsx +0 -1
  419. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/schema.ts +0 -92
  420. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/page.tsx +0 -1
  421. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/schema.ts +0 -153
  422. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/page.tsx +0 -1
  423. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/schema.ts +0 -65
  424. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/daily_ticket_digest/index.ts +0 -44
  425. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/sync_roles_to_platform/index.ts +0 -33
  426. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/glass-home-dashboard/App.tsx +0 -8
  427. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/glass-home-dashboard/GlassHomeDashboard.tsx +0 -232
  428. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/glass-home-dashboard/index.tsx +0 -10
  429. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/glass-home-dashboard/page.config.ts +0 -14
  430. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/glass-home-dashboard/styles.css +0 -196
  431. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/App.tsx +0 -7
  432. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/WorkbenchPage.tsx +0 -36
  433. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/ConfigPanel.tsx +0 -34
  434. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/PreviewPanel.tsx +0 -17
  435. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/index.tsx +0 -10
  436. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/page.config.ts +0 -9
  437. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/reducer.ts +0 -29
  438. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/styles.css +0 -24
  439. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mint-analytics-dashboard/App.tsx +0 -8
  440. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mint-analytics-dashboard/MintAnalyticsDashboard.tsx +0 -279
  441. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mint-analytics-dashboard/index.tsx +0 -10
  442. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mint-analytics-dashboard/page.config.ts +0 -14
  443. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mint-analytics-dashboard/styles.css +0 -163
  444. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/App.tsx +0 -7
  445. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/MobilePortalShell.tsx +0 -31
  446. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/index.tsx +0 -10
  447. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/modules/MobileHome.tsx +0 -13
  448. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/page.config.ts +0 -14
  449. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/routes.ts +0 -13
  450. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/styles.css +0 -11
  451. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/ops-monitor-dashboard/App.tsx +0 -8
  452. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/ops-monitor-dashboard/OpsMonitorDashboard.tsx +0 -306
  453. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/ops-monitor-dashboard/index.tsx +0 -10
  454. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/ops-monitor-dashboard/page.config.ts +0 -14
  455. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/ops-monitor-dashboard/styles.css +0 -248
  456. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/App.tsx +0 -7
  457. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/PcPortalShell.tsx +0 -35
  458. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/components/PortalMetric.tsx +0 -11
  459. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/index.tsx +0 -10
  460. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/HomeModule.tsx +0 -25
  461. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/TicketsModule.tsx +0 -14
  462. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/page.config.ts +0 -14
  463. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/routes.ts +0 -19
  464. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/styles.css +0 -35
  465. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/App.tsx +0 -7
  466. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/TicketOpsPage.tsx +0 -105
  467. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketActionTimeline.tsx +0 -22
  468. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketDetailDrawer.tsx +0 -44
  469. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketTableActions.tsx +0 -57
  470. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/index.tsx +0 -10
  471. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/page.config.ts +0 -9
  472. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/styles.css +0 -35
  473. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/work-order-list-drawer/App.tsx +0 -8
  474. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/work-order-list-drawer/WorkOrderListDrawerPage.tsx +0 -371
  475. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/work-order-list-drawer/index.tsx +0 -10
  476. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/work-order-list-drawer/page.config.ts +0 -14
  477. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/work-order-list-drawer/styles.css +0 -182
  478. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/automation.json +0 -25
  479. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/trigger.json +0 -9
  480. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/notifications/daily-ticket-digest.json +0 -24
  481. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/form-groups/service-ticket-college.json +0 -21
  482. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/roles.json +0 -17
  483. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/workflows/expense-approval-workflow.json +0 -48
  484. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/ConfirmAction.tsx +0 -22
  485. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/QueryState.tsx +0 -37
  486. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/StatusTag.tsx +0 -20
  487. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/admin-ui-templates/DashboardPrimitives.tsx +0 -832
  488. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/admin-ui-templates/chartOptions.ts +0 -140
  489. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/admin-ui-templates/sampleData.ts +0 -466
  490. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/admin-ui-templates/styles.css +0 -874
  491. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/admin-ui-templates/types.ts +0 -150
  492. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/hooks/useTicketOps.ts +0 -96
  493. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/role-governance.ts +0 -48
  494. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/service-ticket.ts +0 -113
  495. package/templates/sy-lowcode-app-workspace/examples/forms/customer/page.tsx +0 -1
  496. package/templates/sy-lowcode-app-workspace/examples/forms/customer/schema.ts +0 -36
  497. package/templates/sy-lowcode-app-workspace/index.html +0 -12
  498. package/templates/sy-lowcode-app-workspace/package.json +0 -58
  499. package/templates/sy-lowcode-app-workspace/postcss.config.cjs +0 -9
  500. package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +0 -600
  501. package/templates/sy-lowcode-app-workspace/scripts/guard-publish.mjs +0 -29
  502. package/templates/sy-lowcode-app-workspace/src/dev/App.tsx +0 -820
  503. package/templates/sy-lowcode-app-workspace/src/forms/.gitkeep +0 -1
  504. package/templates/sy-lowcode-app-workspace/src/forms/README.md +0 -52
  505. package/templates/sy-lowcode-app-workspace/src/index.css +0 -26
  506. package/templates/sy-lowcode-app-workspace/src/js-code-nodes/.gitkeep +0 -1
  507. package/templates/sy-lowcode-app-workspace/src/js-code-nodes/types.d.ts +0 -3
  508. package/templates/sy-lowcode-app-workspace/src/main.tsx +0 -29
  509. package/templates/sy-lowcode-app-workspace/src/pages/.gitkeep +0 -1
  510. package/templates/sy-lowcode-app-workspace/src/runtime/builtin-overrides.tsx +0 -8
  511. package/templates/sy-lowcode-app-workspace/src/shared/form-schema.ts +0 -135
  512. package/templates/sy-lowcode-app-workspace/src/types/app-workspace.types.ts +0 -88
  513. package/templates/sy-lowcode-app-workspace/tailwind.config.cjs +0 -31
  514. package/templates/sy-lowcode-app-workspace/tsconfig.app.json +0 -40
  515. package/templates/sy-lowcode-app-workspace/tsconfig.examples.json +0 -24
  516. package/templates/sy-lowcode-app-workspace/tsconfig.js-code-nodes.json +0 -19
  517. package/templates/sy-lowcode-app-workspace/tsconfig.json +0 -7
  518. package/templates/sy-lowcode-app-workspace/tsconfig.node.json +0 -10
  519. package/templates/sy-lowcode-app-workspace/vite.config.ts +0 -85
  520. package/v2/README.md +0 -35
  521. package/v2/skills/manifest.json +0 -45
  522. package/v2/skills/openxiangda-v1-maintenance/SKILL.md +0 -21
  523. package/v2/skills/openxiangda-v1-maintenance/agents/openai.yaml +0 -4
  524. package/v2/skills/openxiangda-v2/SKILL.md +0 -38
  525. package/v2/skills/openxiangda-v2-architecture/SKILL.md +0 -29
  526. package/v2/skills/openxiangda-v2-architecture/agents/openai.yaml +0 -4
  527. package/v2/skills/openxiangda-v2-backend/SKILL.md +0 -28
  528. package/v2/skills/openxiangda-v2-backend/agents/openai.yaml +0 -4
  529. package/v2/skills/openxiangda-v2-data-authz/SKILL.md +0 -28
  530. package/v2/skills/openxiangda-v2-data-authz/agents/openai.yaml +0 -4
  531. package/v2/skills/openxiangda-v2-delivery/SKILL.md +0 -30
  532. package/v2/skills/openxiangda-v2-delivery/agents/openai.yaml +0 -4
  533. package/v2/skills/openxiangda-v2-frontend/SKILL.md +0 -21
  534. package/v2/skills/openxiangda-v2-frontend/agents/openai.yaml +0 -4
  535. package/v2/skills/openxiangda-v2-workflow-events/SKILL.md +0 -28
  536. package/v2/skills/openxiangda-v2-workflow-events/agents/openai.yaml +0 -4
  537. /package/{v2/skills → skills}/openxiangda-v2/agents/openai.yaml +0 -0
package/lib/sdd.js DELETED
@@ -1,3306 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
- const { spawnSync } = require('child_process');
5
- const { readGitSourceBase } = require('./change-baseline');
6
- const { assertReleaseSourceIntegrated } = require('./release-mainline');
7
- const {
8
- buildWorkspaceReleaseCommands,
9
- normalizeReleaseTargets,
10
- releasePlanHash,
11
- } = require('./release-plan');
12
-
13
- const DEFAULT_SDD_DIR = 'openspec';
14
- const SDD_CONFIG_FILE = 'config.yaml';
15
- const CHANGE_META_FILE = 'change.json';
16
- const COVERAGE_FILE = 'coverage.json';
17
- const RELEASE_FILE = 'release.json';
18
- const BYPASS_LOG_FILE = path.join('changes', 'bypass-log.md');
19
- const SDD_SCHEMA_VERSION = 'openxiangda-sdd-v2';
20
- const LEGACY_SDD_SCHEMA_VERSION = 'openxiangda-sdd-v1';
21
- const SDD_SCOPE_MODEL_VERSION = 'openxiangda-sdd-scopes-v1';
22
- const CODE_RESOURCE_TARGETS = ['functions', 'automations', 'workflows', 'jsCodeNodes'];
23
- const SDD_VERIFY_STAGES = ['implementation', 'prepublish', 'postpublish', 'archive'];
24
- const QUICK_SDD_RISK_TIER = 'L1';
25
- const QUICK_SDD_ALLOWED_KINDS = ['copy', 'style', 'binding', 'metadata', 'narrow-fix'];
26
- const QUICK_FUNCTION_SAFETY_FIELDS = [
27
- 'schema',
28
- 'permission',
29
- 'auth',
30
- 'publicAccess',
31
- 'dataMigration',
32
- 'destructive',
33
- 'crossResource',
34
- ];
35
- const QUICK_SDD_MAX_TARGETS = 3;
36
- const QUICK_SDD_MAX_FILES = 12;
37
-
38
- const DEFAULT_SPEC_CONTENT = `# Application Specification
39
-
40
- This directory is the source of truth for this application's agreed behavior.
41
-
42
- ## Purpose
43
-
44
- Record user-facing behavior, data rules, permissions, workflows, automations, and runtime contracts for this OpenXiangda application. Platform governance rules live in OpenXiangda skills and references, not in this application spec.
45
-
46
- ## Requirements
47
-
48
- `;
49
-
50
- const PLACEHOLDER_PATTERNS = [
51
- /\bTODO\b/i,
52
- /\bTBD\b/i,
53
- /Describe the behavior/i,
54
- /填写|补充|待确认/,
55
- /-\s*\*\*(Given|When|Then)\*\*\s*$/im,
56
- ];
57
-
58
- function nowIso() {
59
- return new Date().toISOString();
60
- }
61
-
62
- function normalizeRelPath(value) {
63
- const input = String(value || '').trim();
64
- if (!input) return DEFAULT_SDD_DIR;
65
- const normalized = input.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '');
66
- if (!normalized || normalized.includes('..') || path.isAbsolute(normalized)) {
67
- throw new Error(`非法 SDD 目录: ${value}`);
68
- }
69
- return normalized;
70
- }
71
-
72
- function normalizeChangeId(value) {
73
- const raw = String(value || '').trim();
74
- if (!raw) throw new Error('缺少 change id');
75
- const id = raw
76
- .toLowerCase()
77
- .replace(/[^a-z0-9._-]+/g, '-')
78
- .replace(/^-+|-+$/g, '');
79
- if (!id) throw new Error(`非法 change id: ${value}`);
80
- return id;
81
- }
82
-
83
- function slug(value, fallback = 'item') {
84
- const normalized = String(value || '')
85
- .trim()
86
- .toLowerCase()
87
- .replace(/[^a-z0-9]+/g, '-')
88
- .replace(/^-+|-+$/g, '');
89
- return normalized || fallback;
90
- }
91
-
92
- function ensureDir(dir) {
93
- fs.mkdirSync(dir, { recursive: true });
94
- }
95
-
96
- function writeFileIfMissing(file, content, operations, force = false) {
97
- const existed = fs.existsSync(file);
98
- if (existed && !force) {
99
- operations.push({ path: file, action: 'unchanged' });
100
- return;
101
- }
102
- ensureDir(path.dirname(file));
103
- fs.writeFileSync(file, content);
104
- operations.push({ path: file, action: existed && force ? 'overwrite' : 'create' });
105
- }
106
-
107
- function readJsonFile(file, fallback = null) {
108
- if (!fs.existsSync(file)) return fallback;
109
- try {
110
- return JSON.parse(fs.readFileSync(file, 'utf8'));
111
- } catch (error) {
112
- return {
113
- __invalid: true,
114
- error: error.message,
115
- file,
116
- };
117
- }
118
- }
119
-
120
- function writeJsonFile(file, value, options = {}) {
121
- ensureDir(path.dirname(file));
122
- fs.writeFileSync(
123
- file,
124
- `${JSON.stringify(value, null, options.compact ? 0 : 2)}\n`
125
- );
126
- }
127
-
128
- function extractSddBlock(configText) {
129
- const governanceIndex = String(configText || '').indexOf('governance');
130
- const sddIndex = String(configText || '').indexOf('sdd', governanceIndex >= 0 ? governanceIndex : 0);
131
- if (sddIndex < 0) return '';
132
- const start = String(configText).indexOf('{', sddIndex);
133
- if (start < 0) return '';
134
- let depth = 0;
135
- for (let index = start; index < configText.length; index += 1) {
136
- const char = configText[index];
137
- if (char === '{') depth += 1;
138
- if (char === '}') {
139
- depth -= 1;
140
- if (depth === 0) return configText.slice(start, index + 1);
141
- }
142
- }
143
- return '';
144
- }
145
-
146
- function readConfigYamlSchema(rootDir) {
147
- const file = path.join(rootDir, SDD_CONFIG_FILE);
148
- if (!fs.existsSync(file)) return null;
149
- const content = fs.readFileSync(file, 'utf8');
150
- return content.match(/^\s*schema\s*:\s*([^\s#]+)/m)?.[1] || null;
151
- }
152
-
153
- function getSddGovernanceConfig(options = {}) {
154
- const cwd = options.cwd || process.cwd();
155
- const configText = options.configText || '';
156
- const block = extractSddBlock(configText);
157
- const configured = Boolean(block);
158
- const enabled =
159
- configured && /enabled\s*:\s*false/.test(block)
160
- ? false
161
- : configured
162
- ? true
163
- : fs.existsSync(path.join(cwd, DEFAULT_SDD_DIR));
164
- const strictHighRisk =
165
- configured && /strictHighRisk\s*:\s*false/.test(block)
166
- ? false
167
- : enabled;
168
- const strictDocumentation = Boolean(
169
- configured && /strictDocumentation\s*:\s*true/.test(block)
170
- );
171
- const documentationModeMatch = block.match(
172
- /documentationMode\s*:\s*['"]?(structured|full)['"]?/
173
- );
174
- const documentationMode = strictDocumentation
175
- ? 'full'
176
- : documentationModeMatch?.[1] || 'structured';
177
- const pathMatch = block.match(/path\s*:\s*['"]([^'"]+)['"]/);
178
- const schemaMatch = block.match(/schemaVersion\s*:\s*['"]([^'"]+)['"]/);
179
- const sddPath = normalizeRelPath(pathMatch?.[1] || DEFAULT_SDD_DIR);
180
- const rootDir = path.join(cwd, sddPath);
181
- const initialized =
182
- fs.existsSync(path.join(rootDir, SDD_CONFIG_FILE)) ||
183
- fs.existsSync(path.join(rootDir, 'specs')) ||
184
- fs.existsSync(path.join(rootDir, 'changes'));
185
- const schemaVersion =
186
- schemaMatch?.[1] ||
187
- readConfigYamlSchema(rootDir) ||
188
- (initialized ? LEGACY_SDD_SCHEMA_VERSION : SDD_SCHEMA_VERSION);
189
- return {
190
- enabled,
191
- strictHighRisk,
192
- strictDocumentation,
193
- documentationMode,
194
- path: sddPath,
195
- rootDir,
196
- initialized,
197
- configured,
198
- schemaVersion,
199
- source: configured ? 'app-workspace.config.ts' : initialized ? 'openspec' : 'none',
200
- };
201
- }
202
-
203
- function buildDefaultConfigYaml() {
204
- return [
205
- `schema: ${SDD_SCHEMA_VERSION}`,
206
- 'description: OpenXiangda native SDD workspace records inspired by OpenSpec.',
207
- 'artifacts:',
208
- ' proposal: proposal.md',
209
- ' specs: specs/**/*.md',
210
- ' design: design.md',
211
- ' tasks: tasks.md',
212
- ' evidence: evidence.md',
213
- ' coverage: coverage.json',
214
- ' release: release.json',
215
- ' metadata: change.json',
216
- ' documentationMode: structured',
217
- 'rules:',
218
- ' requireApprovalForHighRisk: true',
219
- ' requireAffectedCoverage: true',
220
- ' requireCoverageManifest: true',
221
- ' requireScenarioEvidence: false',
222
- ' requireChecklistCompletion: false',
223
- ' releaseModel: publish-from-main-v1',
224
- ' archiveMergesDeltaSpecs: true',
225
- '',
226
- ].join('\n');
227
- }
228
-
229
- function initSddWorkspace(options = {}) {
230
- const cwd = options.cwd || process.cwd();
231
- const sddPath = normalizeRelPath(options.path || DEFAULT_SDD_DIR);
232
- const rootDir = path.join(cwd, sddPath);
233
- const operations = [];
234
- const force = Boolean(options.force || options.upgrade);
235
-
236
- ensureDir(rootDir);
237
- ensureDir(path.join(rootDir, 'changes'));
238
- ensureDir(path.join(rootDir, 'changes', 'archive'));
239
- ensureDir(path.join(rootDir, 'specs', 'app'));
240
- ensureDir(path.join(rootDir, 'templates', 'change'));
241
-
242
- writeFileIfMissing(path.join(rootDir, SDD_CONFIG_FILE), buildDefaultConfigYaml(), operations, force);
243
- writeFileIfMissing(path.join(rootDir, 'specs', 'app', 'spec.md'), DEFAULT_SPEC_CONTENT, operations, false);
244
- writeFileIfMissing(path.join(rootDir, 'changes', '.gitkeep'), '', operations, false);
245
- writeFileIfMissing(path.join(rootDir, 'changes', 'archive', '.gitkeep'), '', operations, false);
246
- writeFileIfMissing(
247
- path.join(rootDir, 'templates', 'change', 'README.md'),
248
- [
249
- '# Change Artifact Template',
250
- '',
251
- 'Create changes with `openxiangda sdd propose <change-id>`.',
252
- 'High-risk implementation starts only after `openxiangda sdd approve <change-id>`.',
253
- 'Before release, `coverage.json`, `release.json`, `tasks.md`, delta specs, and `evidence.md` must pass `openxiangda sdd verify <change> --changed`.',
254
- '',
255
- ].join('\n'),
256
- operations,
257
- force
258
- );
259
-
260
- return {
261
- cwd,
262
- path: sddPath,
263
- rootDir,
264
- initialized: true,
265
- schemaVersion: SDD_SCHEMA_VERSION,
266
- operations: operations.map(item => ({
267
- path: path.relative(cwd, item.path).replace(/\\/g, '/'),
268
- action: item.action,
269
- })),
270
- };
271
- }
272
-
273
- function listMarkdownFiles(dir) {
274
- if (!fs.existsSync(dir)) return [];
275
- const result = [];
276
- const visit = current => {
277
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
278
- const entryPath = path.join(current, entry.name);
279
- if (entry.isDirectory()) {
280
- visit(entryPath);
281
- } else if (entry.isFile() && entry.name.endsWith('.md')) {
282
- result.push(entryPath);
283
- }
284
- }
285
- };
286
- visit(dir);
287
- return result.sort();
288
- }
289
-
290
- function listChangeDirs(rootDir, includeArchive = false) {
291
- const changesDir = path.join(rootDir, 'changes');
292
- if (!fs.existsSync(changesDir)) return [];
293
- const dirs = [];
294
- for (const entry of fs.readdirSync(changesDir, { withFileTypes: true })) {
295
- if (!entry.isDirectory()) continue;
296
- if (entry.name === 'archive') {
297
- if (!includeArchive) continue;
298
- const archiveDir = path.join(changesDir, entry.name);
299
- for (const archived of fs.readdirSync(archiveDir, { withFileTypes: true })) {
300
- if (archived.isDirectory()) dirs.push(path.join(archiveDir, archived.name));
301
- }
302
- continue;
303
- }
304
- dirs.push(path.join(changesDir, entry.name));
305
- }
306
- return dirs.sort();
307
- }
308
-
309
- function readChange(changeDir) {
310
- const meta = readJsonFile(path.join(changeDir, CHANGE_META_FILE), null);
311
- const id = meta?.id || path.basename(changeDir);
312
- const coverage = readJsonFile(path.join(changeDir, COVERAGE_FILE), null);
313
- return {
314
- id,
315
- dir: changeDir,
316
- relativeDir: changeDir.split(path.sep).slice(-2).join('/'),
317
- metadata: meta,
318
- coverage,
319
- valid: Boolean(meta && !meta.__invalid),
320
- };
321
- }
322
-
323
- function listChanges(rootDir, options = {}) {
324
- return listChangeDirs(rootDir, Boolean(options.includeArchive)).map(readChange);
325
- }
326
-
327
- function splitList(value) {
328
- if (value === undefined || value === null || value === false) return [];
329
- if (Array.isArray(value)) return value.flatMap(splitList);
330
- if (value === true) return ['*'];
331
- return String(value)
332
- .split(',')
333
- .map(item => item.trim())
334
- .filter(Boolean);
335
- }
336
-
337
- function normalizeResourceSelectors(value = {}) {
338
- if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
339
- return Object.fromEntries(
340
- Object.entries(value)
341
- .map(([key, selectors]) => [
342
- String(key || '').trim(),
343
- Array.from(new Set(splitList(selectors))).sort(),
344
- ])
345
- .filter(([key, selectors]) => key && selectors.length > 0)
346
- );
347
- }
348
-
349
- function normalizeAffected(input = {}) {
350
- const affected = input.affected || input;
351
- return {
352
- forms: splitList(affected.forms || affected.form),
353
- pages: splitList(affected.pages || affected.page),
354
- functions: splitList(affected.functions || affected.function),
355
- automations: splitList(affected.automations || affected.automation),
356
- workflows: splitList(affected.workflows || affected.workflow),
357
- jsCodeNodes: splitList(affected.jsCodeNodes || affected.jsCode || affected['js-code-nodes']),
358
- files: splitList(affected.files || affected.file),
359
- resourceSelectors: normalizeResourceSelectors(
360
- affected.resourceSelectors || affected.resourceTargets
361
- ),
362
- resources: affected.resources === true || affected.resources === 'true' || splitList(affected.resources).includes('*'),
363
- runtime: affected.runtime === true || affected.runtime === 'true',
364
- backend: affected.backend === true || affected.backend === 'true',
365
- contracts: affected.contracts === true || affected.contracts === 'true',
366
- };
367
- }
368
-
369
- function mergeAffectedScopes(...inputs) {
370
- const scopes = inputs.filter(Boolean).map(input => normalizeAffected(input));
371
- const mergeList = key =>
372
- Array.from(new Set(scopes.flatMap(scope => scope[key] || []))).sort();
373
- const hasFlag = key => scopes.some(scope => Boolean(scope[key]));
374
- const resourceSelectorKeys = Array.from(
375
- new Set(scopes.flatMap(scope => Object.keys(scope.resourceSelectors || {})))
376
- ).sort();
377
- return {
378
- forms: mergeList('forms'),
379
- pages: mergeList('pages'),
380
- functions: mergeList('functions'),
381
- automations: mergeList('automations'),
382
- workflows: mergeList('workflows'),
383
- jsCodeNodes: mergeList('jsCodeNodes'),
384
- files: mergeList('files'),
385
- resourceSelectors: Object.fromEntries(
386
- resourceSelectorKeys.map(key => [
387
- key,
388
- Array.from(
389
- new Set(
390
- scopes.flatMap(scope => scope.resourceSelectors?.[key] || [])
391
- )
392
- ).sort(),
393
- ])
394
- ),
395
- resources: hasFlag('resources'),
396
- runtime: hasFlag('runtime'),
397
- backend: hasFlag('backend'),
398
- contracts: hasFlag('contracts'),
399
- };
400
- }
401
-
402
- function normalizeSddScopeModel(input = {}, fallbackChangedResources = {}) {
403
- const source = input.scopeModel || input.scopes || input;
404
- const changedResources = normalizeAffected(
405
- source.changedResources ||
406
- source.changed ||
407
- fallbackChangedResources
408
- );
409
- const deployTargets = normalizeAffected(
410
- source.deployTargets ||
411
- source.targets ||
412
- changedResources
413
- );
414
- deployTargets.files = [];
415
- return {
416
- schemaVersion: SDD_SCOPE_MODEL_VERSION,
417
- changedResources,
418
- runtimeDependencies: normalizeAffected(
419
- source.runtimeDependencies || source.dependencies || {}
420
- ),
421
- deployTargets,
422
- };
423
- }
424
-
425
- function hasExplicitSddScopeModel(value) {
426
- return Boolean(value?.scopes || value?.scopeModel);
427
- }
428
-
429
- function normalizeQuickScopeFile(value) {
430
- const file = String(value || '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
431
- if (
432
- !file ||
433
- path.isAbsolute(file) ||
434
- file.split('/').includes('..') ||
435
- file.endsWith('/') ||
436
- /[*?\[\]{}]/.test(file)
437
- ) {
438
- return null;
439
- }
440
- return file;
441
- }
442
-
443
- function normalizeQuickChangeKind(value) {
444
- return String(value || '')
445
- .trim()
446
- .toLowerCase()
447
- .replace(/[\s_]+/g, '-');
448
- }
449
-
450
- function normalizeQuickRiskAssessment(value) {
451
- if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
452
- return {
453
- confirmed: value.confirmed === true,
454
- schema: value.schema,
455
- permission: value.permission ?? value.permissions,
456
- auth: value.auth ?? value.authentication,
457
- publicAccess: value.publicAccess ?? value.public,
458
- dataMigration: value.dataMigration ?? value.migration,
459
- destructive: value.destructive,
460
- crossResource: value.crossResource ?? value.crossResources,
461
- };
462
- }
463
-
464
- function validateQuickSddScope(options = {}) {
465
- const affected = normalizeAffected(options);
466
- affected.pages = Array.from(new Set(affected.pages));
467
- affected.functions = Array.from(new Set(affected.functions));
468
- affected.files = Array.from(new Set(affected.files.map(normalizeQuickScopeFile).filter(Boolean))).sort();
469
- if (affected.pages.length > 0) affected.runtime = true;
470
- const errors = [];
471
- const requestedRisk = String(
472
- options.riskTier || options.riskLevel || options.risk || QUICK_SDD_RISK_TIER
473
- ).trim().toUpperCase();
474
- if (!['L1', 'LOW'].includes(requestedRisk)) {
475
- errors.push(`quick change 仅允许 ${QUICK_SDD_RISK_TIER}/low,收到 ${requestedRisk || '(empty)'}`);
476
- }
477
-
478
- const changeKind = normalizeQuickChangeKind(options.changeKind || options.kind);
479
- if (!QUICK_SDD_ALLOWED_KINDS.includes(changeKind)) {
480
- errors.push(
481
- `quick change 必须声明可解释的 L1 changeKind:${QUICK_SDD_ALLOWED_KINDS.join(', ')};收到 ${changeKind || '(empty)'}`
482
- );
483
- }
484
- if (
485
- affected.forms.length > 0 ||
486
- affected.automations.length > 0 ||
487
- affected.workflows.length > 0 ||
488
- affected.jsCodeNodes.length > 0 ||
489
- affected.resources ||
490
- affected.backend ||
491
- affected.contracts
492
- ) {
493
- errors.push('quick change 不能覆盖表单、Automation、Workflow、JS_CODE、通用资源、后端或契约变更');
494
- }
495
- if (affected.runtime && affected.pages.length === 0) {
496
- errors.push('quick change 仅允许由精确 page target 引起的 Runtime 发布');
497
- }
498
-
499
- const logicalTargets = [...affected.pages, ...affected.functions];
500
- if (logicalTargets.length === 0) {
501
- errors.push('quick change 至少需要一个精确 page 或 function target');
502
- }
503
- if (logicalTargets.length > QUICK_SDD_MAX_TARGETS) {
504
- errors.push(`quick change 最多允许 ${QUICK_SDD_MAX_TARGETS} 个逻辑 target`);
505
- }
506
- if (logicalTargets.some(item => !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(item))) {
507
- errors.push('quick change target 必须是精确安全 code,不能包含路径或通配符');
508
- }
509
- if (affected.files.length === 0) {
510
- errors.push('quick change 必须声明精确 files');
511
- }
512
- if (affected.files.length > QUICK_SDD_MAX_FILES) {
513
- errors.push(`quick change 最多允许 ${QUICK_SDD_MAX_FILES} 个文件`);
514
- }
515
-
516
- const selectedPages = new Set(affected.pages);
517
- const selectedFunctions = new Set(affected.functions);
518
- const riskAssessment = normalizeQuickRiskAssessment(options.riskAssessment);
519
- if (selectedFunctions.size > 0) {
520
- if (!riskAssessment?.confirmed) {
521
- errors.push('Function quick change 必须提供 riskAssessment.confirmed=true 并逐项排除高风险行为');
522
- }
523
- for (const field of QUICK_FUNCTION_SAFETY_FIELDS) {
524
- if (riskAssessment?.[field] !== false) {
525
- errors.push(`Function quick change 必须明确确认 riskAssessment.${field}=false`);
526
- }
527
- }
528
- if (!['metadata', 'narrow-fix'].includes(changeKind)) {
529
- errors.push('Function quick change 仅允许 changeKind=metadata 或 narrow-fix');
530
- }
531
- } else if (changeKind === 'binding' && riskAssessment?.crossResource !== false) {
532
- errors.push('binding quick change 必须明确确认 riskAssessment.crossResource=false');
533
- }
534
- for (const rawFile of normalizeAffected(options).files) {
535
- if (!normalizeQuickScopeFile(rawFile)) {
536
- errors.push(`quick change file 不是精确安全路径: ${rawFile}`);
537
- }
538
- }
539
- for (const file of affected.files) {
540
- const pageCode = file.match(/^src\/pages\/([^/]+)\//)?.[1];
541
- const functionCode = file.match(/^src\/functions\/([^/]+)\//)?.[1];
542
- const functionManifestCode = file.match(/^src\/resources\/functions\/([^/]+)\.json$/)?.[1];
543
- if (pageCode && selectedPages.has(pageCode)) continue;
544
- if (functionCode && selectedFunctions.has(functionCode)) continue;
545
- if (functionManifestCode && selectedFunctions.has(functionManifestCode)) continue;
546
- errors.push(`quick change file 超出精确 page/function scope: ${file}`);
547
- }
548
- for (const pageCode of selectedPages) {
549
- if (!affected.files.some(file => file.startsWith(`src/pages/${pageCode}/`))) {
550
- errors.push(`page ${pageCode} 缺少对应精确 file`);
551
- }
552
- }
553
- for (const functionCode of selectedFunctions) {
554
- const hasSource = affected.files.some(file => file.startsWith(`src/functions/${functionCode}/`));
555
- const hasManifest = affected.files.includes(`src/resources/functions/${functionCode}.json`);
556
- if (!hasSource && !hasManifest) {
557
- errors.push(`function ${functionCode} 缺少对应源码或 function manifest file`);
558
- }
559
- }
560
-
561
- return {
562
- valid: errors.length === 0,
563
- errors: Array.from(new Set(errors)),
564
- affected,
565
- risk: {
566
- tier: QUICK_SDD_RISK_TIER,
567
- level: 'low',
568
- mode: 'quick',
569
- changeKind,
570
- assessment: riskAssessment,
571
- prohibitedFunctionImpacts: QUICK_FUNCTION_SAFETY_FIELDS,
572
- exactScope: true,
573
- maxTargets: QUICK_SDD_MAX_TARGETS,
574
- maxFiles: QUICK_SDD_MAX_FILES,
575
- },
576
- };
577
- }
578
-
579
- function assertQuickSddScope(options = {}) {
580
- const validation = validateQuickSddScope(options);
581
- if (validation.valid) return validation;
582
- const error = new Error(`不能创建 quick SDD change: ${validation.errors[0]}`);
583
- error.code = 'SDD_QUICK_SCOPE_REJECTED';
584
- error.details = validation.errors;
585
- throw error;
586
- }
587
-
588
- function extractCodeFromPath(file, prefix) {
589
- if (!file.startsWith(prefix)) return null;
590
- const rest = file.slice(prefix.length);
591
- const first = rest.split('/').filter(Boolean)[0] || '';
592
- return first.replace(/\.json$/i, '') || null;
593
- }
594
-
595
- function coversCodeTarget(normalized, key, code) {
596
- return Boolean(normalized.resources || normalized.backend || includesAll(normalized[key], [code]));
597
- }
598
-
599
- function createChangeMetadata(changeId, options = {}) {
600
- const affected = normalizeAffected(options);
601
- const scopes = normalizeSddScopeModel(
602
- options.scopes || options.scopeModel || {},
603
- affected
604
- );
605
- const metadata = {
606
- schemaVersion: SDD_SCHEMA_VERSION,
607
- id: changeId,
608
- title: options.title || changeId,
609
- status: 'proposed',
610
- riskLevel: options.riskLevel || options.risk || 'medium',
611
- createdAt: nowIso(),
612
- updatedAt: nowIso(),
613
- approvedAt: null,
614
- approvedBy: null,
615
- approvalSummary: null,
616
- // affected remains as a backwards-compatible alias for the authored
617
- // change facts. It must never be used to infer deployment closure.
618
- affected: scopes.changedResources,
619
- scopes,
620
- release: {
621
- plannedCommands: [],
622
- lastVerifiedAt: null,
623
- },
624
- evidence: {
625
- files: ['evidence.md'],
626
- bypasses: [],
627
- },
628
- };
629
- if (options.changeMode) metadata.changeMode = options.changeMode;
630
- if (options.riskTier) metadata.riskTier = options.riskTier;
631
- if (options.changeKind) metadata.changeKind = options.changeKind;
632
- if (options.riskMetadata) metadata.riskMetadata = options.riskMetadata;
633
- if (options.scopePolicy) metadata.scopePolicy = options.scopePolicy;
634
- try {
635
- const sourceBase =
636
- options.sourceBase ||
637
- readGitSourceBase(options.cwd || process.cwd(), options.sourceBaseRef || 'HEAD');
638
- if (sourceBase?.baseCommit && sourceBase?.treeHash) {
639
- metadata.sourceBase = {
640
- repo: sourceBase.repo,
641
- baseCommit: sourceBase.baseCommit,
642
- treeHash: sourceBase.treeHash,
643
- };
644
- metadata.baseRevision = sourceBase.baseCommit;
645
- }
646
- } catch {
647
- // SDD can still be used before a workspace is initialized as a Git repo.
648
- // Release begin will require a reproducible source base for stale-worktree protection.
649
- }
650
- return metadata;
651
- }
652
-
653
- function formatAffectedList(items) {
654
- return items.length ? items.join(', ') : '(none)';
655
- }
656
-
657
- function createProposalContent(meta) {
658
- if (meta.changeMode === 'quick') {
659
- return [
660
- `# Quick Change: ${meta.title}`,
661
- '',
662
- `User intent and approval are recorded in \`${CHANGE_META_FILE}\`.`,
663
- `Exact resource/file scope is recorded in \`${COVERAGE_FILE}\`; no additional proposal prose is required.`,
664
- '',
665
- ].join('\n');
666
- }
667
- return `# Proposal: ${meta.title}
668
-
669
- ## Intent
670
-
671
- Deliver "${meta.title}" as one focused OpenXiangda application change.
672
-
673
- ## Scope
674
-
675
- - Forms: ${formatAffectedList(meta.affected.forms)}
676
- - Pages: ${formatAffectedList(meta.affected.pages)}
677
- - Resources: ${meta.affected.resources ? 'yes' : 'no'}
678
- - Runtime: ${meta.affected.runtime ? 'yes' : 'no'}
679
- - Backend logic: ${meta.affected.backend ? 'yes' : 'no'}
680
-
681
- ## Out Of Scope
682
-
683
- - Platform-level governance changes.
684
- - Unrelated application behavior outside the affected resources above.
685
-
686
- ## Acceptance
687
-
688
- - Requirements in \`specs/**/spec.md\` describe the user-visible behavior.
689
- - \`coverage.json\` maps requirements and scenarios to affected resources.
690
- - \`evidence.md\` records verification commands before release.
691
- `;
692
- }
693
-
694
- function createDesignContent(meta) {
695
- if (meta.changeMode === 'quick') {
696
- return [
697
- '# Design',
698
- '',
699
- 'L1 exact-scope change; implementation stays inside coverage.json and uses the generated atomic release plan.',
700
- '',
701
- ].join('\n');
702
- }
703
- return `# Design: ${meta.title}
704
-
705
- ## OpenXiangda Resource Impact
706
-
707
- - Forms: ${formatAffectedList(meta.affected.forms)}
708
- - Pages: ${formatAffectedList(meta.affected.pages)}
709
- - Functions: ${formatAffectedList(meta.affected.functions)}
710
- - Automations: ${formatAffectedList(meta.affected.automations)}
711
- - Workflows: ${formatAffectedList(meta.affected.workflows)}
712
- - JS_CODE nodes: ${formatAffectedList(meta.affected.jsCodeNodes)}
713
- - Resources: ${meta.affected.resources ? 'yes' : 'no'}
714
- - Runtime: ${meta.affected.runtime ? 'yes' : 'no'}
715
- - Contracts/config: ${meta.affected.contracts ? 'yes' : 'no'}
716
-
717
- ## Data And Contract Notes
718
-
719
- - Keep application specs focused on observable behavior; implementation details stay here or in source code.
720
- - Resource IDs remain profile-local under \`.openxiangda/state.json\`.
721
-
722
- ## Publish Plan
723
-
724
- - Run \`openxiangda sdd verify ${meta.id} --changed\`.
725
- - Run \`openxiangda workspace check --changed\`.
726
- - Publish with the commands recorded in \`release.json\`.
727
- `;
728
- }
729
-
730
- function createTasksContent(meta = {}) {
731
- if (meta.changeMode === 'quick') {
732
- return [
733
- '# Tasks',
734
- '',
735
- '## Implementation',
736
- '',
737
- '- [ ] Implement the exact covered change',
738
- '',
739
- '## Prepublish',
740
- '',
741
- '- [ ] Run the focused check and confirm the generated release plan',
742
- '',
743
- '## Postpublish',
744
- '',
745
- '- [ ] Record atomic publish and mainline integration status',
746
- '',
747
- '## Archive',
748
- '',
749
- '- [ ] Archive the released change',
750
- '',
751
- ].join('\n');
752
- }
753
- return [
754
- '# Tasks',
755
- '',
756
- '## Implementation',
757
- '',
758
- '- [ ] Confirm proposal and affected scope',
759
- '- [ ] Sharpen delta specs into behavior-first requirements and scenarios',
760
- '- [ ] Implement changes',
761
- '- [ ] Update coverage.json if affected resources change',
762
- '',
763
- '## Prepublish',
764
- '',
765
- '- [ ] Record verification evidence',
766
- '- [ ] Run release plan/check commands',
767
- '',
768
- '## Postpublish',
769
- '',
770
- '- [ ] Record publish/deploy result',
771
- '- [ ] Merge/fast-forward the frozen release SHA and push the authoritative default branch',
772
- '',
773
- '## Archive',
774
- '',
775
- '- [ ] Confirm the released change is ready to archive',
776
- '',
777
- ].join('\n');
778
- }
779
-
780
- function createEvidenceContent(meta = {}) {
781
- if (meta.changeMode === 'quick') {
782
- return [
783
- '# Evidence',
784
- '',
785
- '## Verification',
786
- '',
787
- '- [ ] Record the focused check result',
788
- '',
789
- '## Release',
790
- '',
791
- '- [ ] Record the atomic publish result',
792
- '',
793
- ].join('\n');
794
- }
795
- return [
796
- '# Evidence',
797
- '',
798
- '## Verification',
799
- '',
800
- '- [ ] Record implementation test/check output',
801
- '- [ ] Record workspace/resource/runtime check or dry-run output',
802
- '',
803
- '## Release',
804
- '',
805
- '- [ ] Record publish/deploy command output or explain why this change was not released',
806
- '- [ ] Record `openxiangda release integration-status` proving the frozen SHA is on the authoritative default branch',
807
- '',
808
- '## Bypass Log',
809
- '',
810
- '- None',
811
- '',
812
- ].join('\n');
813
- }
814
-
815
- function createDeltaSpecContent(meta, domain) {
816
- if (meta.changeMode === 'quick') {
817
- return [
818
- `# ${domain} Delta Spec`,
819
- '',
820
- '## ADDED Requirements',
821
- '',
822
- `### Requirement: ${meta.title}`,
823
- 'The application SHALL preserve existing behavior except for the exact approved L1 scope.',
824
- '',
825
- '#### Scenario: Focused change',
826
- '- **Given** the existing application behavior',
827
- `- **When** ${meta.title} is applied`,
828
- '- **Then** only the covered target changes and unrelated behavior remains unchanged',
829
- '',
830
- ].join('\n');
831
- }
832
- return `# ${domain} Delta Spec
833
-
834
- ## ADDED Requirements
835
-
836
- ### Requirement: ${meta.title}
837
- The application SHALL provide the approved "${meta.title}" behavior for the affected OpenXiangda resources.
838
-
839
- #### Scenario: Main path
840
- - **Given** an authorized user is using the affected application area
841
- - **When** the user follows the approved ${meta.title} workflow
842
- - **Then** the application produces the expected business result without relying on mock data or frontend-only authorization
843
- `;
844
- }
845
-
846
- function renderSddDocumentation(options = {}) {
847
- const loaded = options.loaded || loadSddChange(options);
848
- const meta = loaded.meta;
849
- const requirementSpec = loaded.coverage?.requirements?.find(
850
- item => typeof item?.spec === 'string' && item.spec.trim()
851
- )?.spec;
852
- const specRelativePath =
853
- requirementSpec && !requirementSpec.includes('..')
854
- ? requirementSpec.replace(/\\/g, '/')
855
- : 'specs/app/spec.md';
856
- const specMatch = specRelativePath.match(/^specs\/([^/]+)\/spec\.md$/);
857
- const domain = options.domain || specMatch?.[1] || 'app';
858
- const files = {
859
- proposal: path.join(loaded.changeDir, 'proposal.md'),
860
- design: path.join(loaded.changeDir, 'design.md'),
861
- tasks: path.join(loaded.changeDir, 'tasks.md'),
862
- evidence: path.join(loaded.changeDir, 'evidence.md'),
863
- spec: path.join(loaded.changeDir, 'specs', domain, 'spec.md'),
864
- };
865
- const contents = {
866
- proposal: createProposalContent(meta),
867
- design: createDesignContent(meta),
868
- tasks: createTasksContent(meta),
869
- evidence: createEvidenceContent(meta),
870
- spec: createDeltaSpecContent(meta, domain),
871
- };
872
- const rendered = [];
873
- const skipped = [];
874
- for (const [kind, file] of Object.entries(files)) {
875
- if (fs.existsSync(file) && !options.force) {
876
- skipped.push(path.relative(options.cwd || process.cwd(), file).replace(/\\/g, '/'));
877
- continue;
878
- }
879
- ensureDir(path.dirname(file));
880
- fs.writeFileSync(file, contents[kind]);
881
- rendered.push(path.relative(options.cwd || process.cwd(), file).replace(/\\/g, '/'));
882
- }
883
- return {
884
- schemaVersion: meta.schemaVersion || SDD_SCHEMA_VERSION,
885
- changeId: loaded.changeId,
886
- mode: 'full',
887
- rendered,
888
- skipped,
889
- };
890
- }
891
-
892
- function createCoverageManifest(meta, domain) {
893
- const reqId = `req-${slug(meta.title || meta.id, meta.id)}`;
894
- const scopes = normalizeSddScopeModel(meta, meta.affected);
895
- const changedResources = scopes.changedResources;
896
- return {
897
- schemaVersion: SDD_SCHEMA_VERSION,
898
- changeId: meta.id,
899
- generatedAt: nowIso(),
900
- scopeModel: scopes,
901
- resources: changedResources,
902
- requirements: [
903
- {
904
- id: reqId,
905
- title: meta.title,
906
- spec: `specs/${domain}/spec.md`,
907
- scenarios: [
908
- {
909
- id: 'main-path',
910
- title: 'Main path',
911
- evidence: [],
912
- },
913
- ],
914
- covers: changedResources,
915
- },
916
- ],
917
- };
918
- }
919
-
920
- function createReleaseManifest(meta) {
921
- const scopes = normalizeSddScopeModel(meta, meta.affected);
922
- return {
923
- schemaVersion: SDD_SCHEMA_VERSION,
924
- changeId: meta.id,
925
- scopeModel: scopes,
926
- targets: {
927
- ...scopes.deployTargets,
928
- other: [],
929
- },
930
- changedFiles: [],
931
- commands: [],
932
- verifiedAt: null,
933
- };
934
- }
935
-
936
- function proposeSddChange(options = {}) {
937
- const governance = getSddGovernanceConfig(options);
938
- if (!governance.initialized) initSddWorkspace({ cwd: options.cwd, path: governance.path });
939
- const changeId = normalizeChangeId(options.changeId);
940
- const rootDir = governance.rootDir;
941
- const domain = options.domain || 'app';
942
- const changeDir = path.join(rootDir, 'changes', changeId);
943
- if (fs.existsSync(changeDir) && !options.force) {
944
- throw new Error(`change 已存在: ${changeId}`);
945
- }
946
- ensureDir(changeDir);
947
- ensureDir(path.join(changeDir, 'specs', domain));
948
- const meta = createChangeMetadata(changeId, options);
949
- const coverage = createCoverageManifest(meta, domain);
950
- const release = createReleaseManifest(meta);
951
- const jsonWriteOptions = {
952
- compact: meta.changeMode === 'quick',
953
- };
954
- writeJsonFile(path.join(changeDir, CHANGE_META_FILE), meta, jsonWriteOptions);
955
- writeJsonFile(path.join(changeDir, COVERAGE_FILE), coverage, jsonWriteOptions);
956
- writeJsonFile(path.join(changeDir, RELEASE_FILE), release, jsonWriteOptions);
957
- const loaded = {
958
- governance,
959
- changeId,
960
- changeDir,
961
- meta,
962
- coverage,
963
- };
964
- const documentation =
965
- governance.documentationMode === 'full' || options.renderDocumentation
966
- ? renderSddDocumentation({
967
- cwd: options.cwd,
968
- loaded,
969
- domain,
970
- force: true,
971
- })
972
- : {
973
- mode: 'structured',
974
- rendered: [],
975
- skipped: [],
976
- };
977
- return {
978
- schemaVersion: SDD_SCHEMA_VERSION,
979
- change: meta,
980
- coverage,
981
- release,
982
- documentation,
983
- dir: path.relative(options.cwd || process.cwd(), changeDir).replace(/\\/g, '/'),
984
- nextActions:
985
- documentation.mode === 'full'
986
- ? [
987
- `Review openspec/changes/${changeId}/proposal.md`,
988
- `Sharpen openspec/changes/${changeId}/specs/${domain}/spec.md`,
989
- `Run openxiangda sdd approve ${changeId} after user confirmation`,
990
- ]
991
- : [
992
- `Review openspec/changes/${changeId}/change.json and coverage.json`,
993
- `Run openxiangda sdd approve ${changeId} after user confirmation`,
994
- `Optional: openxiangda sdd render ${changeId}`,
995
- ],
996
- };
997
- }
998
-
999
- function createMainlineSddBundle(options = {}) {
1000
- const governance = getSddGovernanceConfig(options);
1001
- if (!governance.initialized) {
1002
- throw new Error('SDD 尚未初始化,不能创建 mainline bundle');
1003
- }
1004
- const changeId = normalizeChangeId(options.changeId);
1005
- const sourceIds = Array.from(
1006
- new Set(splitList(options.changes || options.changeIds).map(normalizeChangeId))
1007
- ).filter(id => id !== changeId);
1008
- if (sourceIds.length === 0) {
1009
- throw new Error('mainline bundle 至少需要一个 --changes <change-a,change-b>');
1010
- }
1011
- const sourceChanges = sourceIds.map(id =>
1012
- loadSddChange({ cwd: options.cwd, configText: options.configText, changeId: id })
1013
- );
1014
- const invalid = sourceChanges.find(
1015
- item => item.meta.status !== 'approved' || !item.meta.approvedAt
1016
- );
1017
- if (invalid) {
1018
- const error = new Error(
1019
- `SDD_MAINLINE_BUNDLE_CHANGE_NOT_APPROVED: ${invalid.changeId} 尚未 approved`
1020
- );
1021
- error.code = 'SDD_MAINLINE_BUNDLE_CHANGE_NOT_APPROVED';
1022
- throw error;
1023
- }
1024
- const nested = sourceChanges.find(item => item.meta.changeMode === 'mainline-bundle');
1025
- if (nested) {
1026
- throw new Error(`mainline bundle 不允许嵌套: ${nested.changeId}`);
1027
- }
1028
- const bundleSourceBase = resolveMainlineBundleSourceBase(
1029
- sourceChanges,
1030
- options
1031
- );
1032
- const integratedChanges = sourceChanges.map(item => {
1033
- const sourceBase =
1034
- normalizeSddSourceBase(item.meta.sourceBase) || bundleSourceBase;
1035
- const task = options.integration?.requiredCommits?.find(
1036
- candidate => candidate.changeId === item.changeId
1037
- );
1038
- return {
1039
- changeId: item.changeId,
1040
- sourceBase,
1041
- baseRevision:
1042
- item.meta.baseRevision || sourceBase.baseCommit,
1043
- ...(task
1044
- ? {
1045
- commit: task.commit,
1046
- treeHash: task.treeHash,
1047
- sourceBranch: task.sourceBranch,
1048
- }
1049
- : {}),
1050
- };
1051
- });
1052
-
1053
- const sourceScopeModels = sourceChanges.map(item => {
1054
- const explicit = loadedSddScopeModel(item);
1055
- if (explicit) return explicit;
1056
- const changedResources = mergeAffectedScopes(
1057
- item.meta.affected,
1058
- normalizeCoverageResources(item.coverage, {}),
1059
- { files: item.release?.changedFiles || [] }
1060
- );
1061
- return {
1062
- schemaVersion: SDD_SCOPE_MODEL_VERSION,
1063
- changedResources,
1064
- runtimeDependencies: normalizeAffected({}),
1065
- deployTargets: mergeAffectedScopes(
1066
- item.meta.affected,
1067
- item.release?.targets
1068
- ),
1069
- };
1070
- });
1071
- const scopes = {
1072
- schemaVersion: SDD_SCOPE_MODEL_VERSION,
1073
- changedResources: mergeAffectedScopes(
1074
- ...sourceScopeModels.map(item => item.changedResources)
1075
- ),
1076
- runtimeDependencies: mergeAffectedScopes(
1077
- ...sourceScopeModels.map(item => item.runtimeDependencies)
1078
- ),
1079
- deployTargets: mergeAffectedScopes(
1080
- ...sourceScopeModels.map(item => item.deployTargets)
1081
- ),
1082
- };
1083
- if (
1084
- scopes.changedResources.functions.length > 0 ||
1085
- scopes.changedResources.automations.length > 0
1086
- ) {
1087
- scopes.changedResources.backend = true;
1088
- }
1089
- const runtimeMode = /runtimeMode\s*:\s*['"]react-spa['"]/.test(
1090
- options.configText || ''
1091
- )
1092
- ? 'react-spa'
1093
- : 'legacy';
1094
- const normalizedTargets = normalizeReleaseTargets(
1095
- scopes.deployTargets,
1096
- runtimeMode
1097
- );
1098
- const profile = String(options.profile || '<profile>').trim() || '<profile>';
1099
- const commands = buildWorkspaceReleaseCommands(
1100
- normalizedTargets.logicalTargets,
1101
- runtimeMode,
1102
- profile,
1103
- changeId
1104
- );
1105
- const createdAt = nowIso();
1106
- const meta = {
1107
- ...createChangeMetadata(changeId, {
1108
- cwd: options.cwd,
1109
- title: options.title || `Mainline release: ${sourceIds.join(', ')}`,
1110
- affected: scopes.changedResources,
1111
- scopes,
1112
- changeMode: 'mainline-bundle',
1113
- riskLevel: 'medium',
1114
- sourceBase: bundleSourceBase,
1115
- }),
1116
- status: 'approved',
1117
- approvedAt: createdAt,
1118
- approvedBy: 'mainline-release-coordinator',
1119
- approvalSummary: `Aggregate approved changes on authoritative mainline: ${sourceIds.join(', ')}`,
1120
- bundledChanges: sourceIds,
1121
- integratedChanges,
1122
- createdAt,
1123
- updatedAt: createdAt,
1124
- };
1125
- const coverage = {
1126
- schemaVersion: SDD_SCHEMA_VERSION,
1127
- changeId,
1128
- generatedAt: createdAt,
1129
- scopeModel: scopes,
1130
- resources: scopes.changedResources,
1131
- requirements: sourceChanges.map((item, index) => ({
1132
- id: `bundle-${slug(item.changeId, 'change')}`,
1133
- title: item.meta.title || item.changeId,
1134
- spec: 'specs/app/spec.md',
1135
- scenarios: [
1136
- {
1137
- id: `${slug(item.changeId, 'change')}-merged`,
1138
- title: `${item.changeId} is included in the mainline release`,
1139
- evidence: [],
1140
- },
1141
- ],
1142
- covers: sourceScopeModels[index].changedResources,
1143
- })),
1144
- };
1145
- const release = {
1146
- schemaVersion: SDD_SCHEMA_VERSION,
1147
- changeId,
1148
- mainlinePolicy: 'publish-from-main-v1',
1149
- bundledChanges: sourceIds,
1150
- sourceBase: bundleSourceBase,
1151
- baseRevision: bundleSourceBase.baseCommit,
1152
- integratedChanges,
1153
- ...(options.integration ? { integration: options.integration } : {}),
1154
- scopeModel: scopes,
1155
- targets: normalizedTargets.activationTargets,
1156
- logicalTargets: normalizedTargets.logicalTargets,
1157
- changedFiles: scopes.changedResources.files,
1158
- commands,
1159
- entryCommand:
1160
- `openxiangda release publish --change ${changeId} --profile ${profile}`,
1161
- planHash: releasePlanHash({
1162
- changeId,
1163
- baseRevision: bundleSourceBase.baseCommit,
1164
- integratedChanges,
1165
- runtimeMode,
1166
- targets: normalizedTargets.activationTargets,
1167
- commands,
1168
- }),
1169
- verifiedAt: null,
1170
- };
1171
- const changeDir = path.join(governance.rootDir, 'changes', changeId);
1172
- if (fs.existsSync(changeDir) && !options.force) {
1173
- throw new Error(`change 已存在: ${changeId}`);
1174
- }
1175
- if (fs.existsSync(changeDir)) fs.rmSync(changeDir, { recursive: true, force: true });
1176
- ensureDir(path.join(changeDir, 'specs', 'app'));
1177
- writeJsonFile(path.join(changeDir, CHANGE_META_FILE), meta);
1178
- writeJsonFile(path.join(changeDir, COVERAGE_FILE), coverage);
1179
- writeJsonFile(path.join(changeDir, RELEASE_FILE), release);
1180
- fs.writeFileSync(
1181
- path.join(changeDir, 'proposal.md'),
1182
- `# Mainline Release Bundle: ${changeId}\n\nApproved changes: ${sourceIds.join(', ')}.\n\nThe exact structured scope in coverage.json is authoritative.\n`
1183
- );
1184
- fs.writeFileSync(
1185
- path.join(changeDir, 'design.md'),
1186
- '# Design\n\nAll bundled changes are merged and pushed before one staged, atomic application activation.\n'
1187
- );
1188
- fs.writeFileSync(
1189
- path.join(changeDir, 'tasks.md'),
1190
- '# Tasks\n\n## Implementation\n\n- [x] Aggregate approved changes\n\n## Prepublish\n\n- [x] Generate exact structured release scope\n\n## Postpublish\n\n- [ ] Record publish result\n'
1191
- );
1192
- fs.writeFileSync(
1193
- path.join(changeDir, 'evidence.md'),
1194
- '# Evidence\n\n## Verification\n\n- [x] Source changes were approved before bundling\n\n## Release\n\n- [ ] Record atomic activation result\n'
1195
- );
1196
- fs.writeFileSync(
1197
- path.join(changeDir, 'specs', 'app', 'spec.md'),
1198
- `# Mainline Bundle Spec\n\n## ADDED Requirements\n\n### Requirement: Publish ${sourceIds.length} approved changes together\nThe application SHALL activate the exact bundled scope from one authoritative mainline commit.\n\n#### Scenario: Atomic mainline activation\n- **Given** all bundled changes are approved and merged to the remote default branch\n- **When** the release coordinator publishes bundle ${changeId}\n- **Then** changed child releases activate together and unrelated resources remain unchanged\n`
1199
- );
1200
- return {
1201
- schemaVersion: SDD_SCHEMA_VERSION,
1202
- change: meta,
1203
- coverage,
1204
- release,
1205
- bundledChanges: sourceIds,
1206
- dir: path.relative(options.cwd || process.cwd(), changeDir).replace(/\\/g, '/'),
1207
- };
1208
- }
1209
-
1210
- function normalizeSddSourceBase(value) {
1211
- if (!value?.baseCommit || !value?.treeHash) return null;
1212
- return {
1213
- repo: value.repo,
1214
- baseCommit: String(value.baseCommit),
1215
- treeHash: String(value.treeHash),
1216
- };
1217
- }
1218
-
1219
- function runSddGit(cwd, args, options = {}) {
1220
- const result = spawnSync('git', args, {
1221
- cwd: cwd || process.cwd(),
1222
- encoding: 'utf8',
1223
- stdio: ['ignore', 'pipe', 'pipe'],
1224
- });
1225
- if (result.status !== 0 && !options.allowFailure) {
1226
- const error = new Error(
1227
- String(result.stderr || result.stdout || `git ${args.join(' ')} failed`).trim()
1228
- );
1229
- error.code = 'SDD_SOURCE_BASE_GIT_FAILED';
1230
- throw error;
1231
- }
1232
- return {
1233
- ok: result.status === 0,
1234
- stdout: String(result.stdout || '').trim(),
1235
- };
1236
- }
1237
-
1238
- function resolveMainlineBundleSourceBase(sourceChanges, options = {}) {
1239
- const cwd = options.cwd || process.cwd();
1240
- if (options.sourceBaseRef) {
1241
- return readGitSourceBase(cwd, options.sourceBaseRef);
1242
- }
1243
- const missing = sourceChanges.filter(
1244
- item => !normalizeSddSourceBase(item.meta.sourceBase)
1245
- );
1246
- if (missing.length > 0) {
1247
- const error = new Error(
1248
- `SDD_SOURCE_BASE_REQUIRED: ${missing
1249
- .map(item => item.changeId)
1250
- .join(', ')} 缺少创建变更时冻结的 sourceBase;请用 --source-base-ref <commit> 明确指定共同基线`
1251
- );
1252
- error.code = 'SDD_SOURCE_BASE_REQUIRED';
1253
- error.changes = missing.map(item => item.changeId);
1254
- throw error;
1255
- }
1256
- const sourceBases = sourceChanges.map(item =>
1257
- normalizeSddSourceBase(item.meta.sourceBase)
1258
- );
1259
- const repos = Array.from(
1260
- new Set(sourceBases.map(item => item.repo).filter(Boolean))
1261
- );
1262
- if (repos.length > 1) {
1263
- const error = new Error(
1264
- 'SDD_SOURCE_BASE_REPOSITORY_MISMATCH: mainline bundle 的来源变更不属于同一 Git 仓库'
1265
- );
1266
- error.code = 'SDD_SOURCE_BASE_REPOSITORY_MISMATCH';
1267
- throw error;
1268
- }
1269
- for (const item of sourceChanges) {
1270
- const task = options.integration?.requiredCommits?.find(
1271
- candidate => candidate.changeId === item.changeId
1272
- );
1273
- if (
1274
- task?.commit &&
1275
- !runSddGit(cwd, [
1276
- 'merge-base',
1277
- '--is-ancestor',
1278
- item.meta.sourceBase.baseCommit,
1279
- task.commit,
1280
- ], { allowFailure: true }).ok
1281
- ) {
1282
- const error = new Error(
1283
- `SDD_SOURCE_BASE_NOT_ANCESTOR: ${item.changeId} 的 sourceBase 不是任务提交 ${String(task.commit).slice(0, 12)} 的祖先`
1284
- );
1285
- error.code = 'SDD_SOURCE_BASE_NOT_ANCESTOR';
1286
- error.changeId = item.changeId;
1287
- throw error;
1288
- }
1289
- }
1290
- const commits = Array.from(
1291
- new Set(sourceBases.map(item => item.baseCommit))
1292
- );
1293
- const commonCommit =
1294
- commits.length === 1
1295
- ? commits[0]
1296
- : runSddGit(cwd, ['merge-base', '--octopus', ...commits]).stdout;
1297
- if (!commonCommit) {
1298
- const error = new Error(
1299
- 'SDD_SOURCE_BASE_COMMON_ANCESTOR_REQUIRED: 来源变更没有可验证的共同 Git 基线'
1300
- );
1301
- error.code = 'SDD_SOURCE_BASE_COMMON_ANCESTOR_REQUIRED';
1302
- throw error;
1303
- }
1304
- return readGitSourceBase(cwd, commonCommit);
1305
- }
1306
-
1307
- function loadSddChange(options = {}) {
1308
- const governance = getSddGovernanceConfig(options);
1309
- const changeId = normalizeChangeId(options.changeId);
1310
- const changeDir = path.join(governance.rootDir, 'changes', changeId);
1311
- if (!fs.existsSync(changeDir)) throw new Error(`change 不存在: ${changeId}`);
1312
- const metaFile = path.join(changeDir, CHANGE_META_FILE);
1313
- const meta = readJsonFile(metaFile, null);
1314
- if (!meta || meta.__invalid) {
1315
- throw new Error(`change metadata 无效: ${metaFile}`);
1316
- }
1317
- const coverageFile = path.join(changeDir, COVERAGE_FILE);
1318
- const releaseFile = path.join(changeDir, RELEASE_FILE);
1319
- const coverage = readJsonFile(coverageFile, null);
1320
- const release = readJsonFile(releaseFile, null);
1321
- return {
1322
- governance,
1323
- changeId,
1324
- changeDir,
1325
- meta,
1326
- metaFile,
1327
- coverage,
1328
- coverageFile,
1329
- release,
1330
- releaseFile,
1331
- };
1332
- }
1333
-
1334
- function approveSddChange(options = {}) {
1335
- const loaded = loadSddChange(options);
1336
- const meta = {
1337
- ...loaded.meta,
1338
- schemaVersion: loaded.meta.schemaVersion || loaded.governance.schemaVersion || LEGACY_SDD_SCHEMA_VERSION,
1339
- status: 'approved',
1340
- approvedAt: nowIso(),
1341
- approvedBy: options.approvedBy || os.userInfo().username || 'unknown',
1342
- approvalSummary: options.summary || options.note || loaded.meta.approvalSummary || '',
1343
- ...(options.approvalMode ? { approvalMode: options.approvalMode } : {}),
1344
- ...(options.approvalBasis ? { approvalBasis: options.approvalBasis } : {}),
1345
- updatedAt: nowIso(),
1346
- };
1347
- writeJsonFile(loaded.metaFile, meta, {
1348
- compact: meta.changeMode === 'quick',
1349
- });
1350
- return {
1351
- schemaVersion: meta.schemaVersion,
1352
- change: meta,
1353
- coverage: loaded.coverage,
1354
- dir: path.relative(options.cwd || process.cwd(), loaded.changeDir).replace(/\\/g, '/'),
1355
- nextActions: [
1356
- `Implement only the approved scope in openspec/changes/${loaded.changeId}`,
1357
- `Complete tasks.md and evidence.md`,
1358
- `Run openxiangda sdd verify ${loaded.changeId} --changed before release`,
1359
- ],
1360
- };
1361
- }
1362
-
1363
- function proposeQuickSddChange(options = {}) {
1364
- const validation = assertQuickSddScope(options);
1365
- return proposeSddChange({
1366
- ...options,
1367
- affected: validation.affected,
1368
- riskLevel: validation.risk.level,
1369
- riskTier: validation.risk.tier,
1370
- changeMode: validation.risk.mode,
1371
- changeKind: validation.risk.changeKind,
1372
- riskMetadata: {
1373
- tier: validation.risk.tier,
1374
- level: validation.risk.level,
1375
- classification: 'explicit-l1-quick',
1376
- changeKind: validation.risk.changeKind,
1377
- exactScope: true,
1378
- assessment: validation.risk.assessment,
1379
- prohibitedFunctionImpacts: validation.risk.prohibitedFunctionImpacts,
1380
- },
1381
- scopePolicy: {
1382
- exact: true,
1383
- allowedTargets: ['pages', 'functions'],
1384
- maxTargets: validation.risk.maxTargets,
1385
- maxFiles: validation.risk.maxFiles,
1386
- },
1387
- });
1388
- }
1389
-
1390
- function approveQuickSddChange(options = {}) {
1391
- const loaded = loadSddChange(options);
1392
- if (loaded.meta.changeMode !== 'quick' || loaded.meta.riskTier !== QUICK_SDD_RISK_TIER) {
1393
- throw new Error(`change ${loaded.changeId} 不是可 quick approve 的 L1 change`);
1394
- }
1395
- assertQuickSddScope({
1396
- affected: loaded.meta.affected,
1397
- riskTier: loaded.meta.riskTier,
1398
- changeKind: loaded.meta.changeKind,
1399
- riskAssessment: loaded.meta.riskMetadata?.assessment,
1400
- });
1401
- const summary = String(
1402
- options.summary || options.note || options.userIntent || ''
1403
- ).trim();
1404
- if (!summary) {
1405
- throw new Error('quick approve 需要 summary/userIntent 记录用户的明确变更意图');
1406
- }
1407
- return approveSddChange({
1408
- ...options,
1409
- summary,
1410
- approvalMode: 'quick-explicit-intent',
1411
- approvalBasis: 'explicit-user-request',
1412
- });
1413
- }
1414
-
1415
- function createQuickSddChange(options = {}) {
1416
- const summary = String(
1417
- options.approvalSummary || options.summary || options.note || options.userIntent || ''
1418
- ).trim();
1419
- if (!summary) {
1420
- throw new Error('quick change 需要 approvalSummary/summary/userIntent 才能创建并批准');
1421
- }
1422
- assertQuickSddScope(options);
1423
- const proposed = proposeQuickSddChange(options);
1424
- const approved = approveQuickSddChange({
1425
- cwd: options.cwd,
1426
- configText: options.configText,
1427
- changeId: proposed.change.id,
1428
- summary,
1429
- approvedBy: options.approvedBy,
1430
- });
1431
- return {
1432
- ...proposed,
1433
- change: approved.change,
1434
- approved: true,
1435
- nextActions: [
1436
- `Implement only the exact L1 scope in openspec/changes/${proposed.change.id}`,
1437
- `Run staged verification: implementation -> prepublish -> postpublish -> archive`,
1438
- ],
1439
- };
1440
- }
1441
-
1442
- function includesAll(list, required) {
1443
- if (!required || required.length === 0) return true;
1444
- if ((list || []).includes('*')) return true;
1445
- return required.every(item => (list || []).includes(item));
1446
- }
1447
-
1448
- function hasHighRiskTargets(targets = {}) {
1449
- return (
1450
- (targets.forms || []).length > 0 ||
1451
- (targets.pages || []).length > 0 ||
1452
- CODE_RESOURCE_TARGETS.some(key => (targets[key] || []).length > 0) ||
1453
- Boolean(targets.resources) ||
1454
- Boolean(targets.runtime)
1455
- );
1456
- }
1457
-
1458
- function classifyHighRiskFiles(files = []) {
1459
- const highRisk = [];
1460
- for (const file of files || []) {
1461
- if (
1462
- file === 'app-workspace.config.ts' ||
1463
- file.startsWith('src/forms/') ||
1464
- file.startsWith('src/pages/') ||
1465
- file.startsWith('src/resources/') ||
1466
- file.startsWith('src/functions/') ||
1467
- file.startsWith('src/automations/') ||
1468
- file.startsWith('src/workflows/') ||
1469
- file.startsWith('src/js-code-nodes/') ||
1470
- file.startsWith('src/runtime/')
1471
- ) {
1472
- highRisk.push(file);
1473
- }
1474
- }
1475
- return highRisk;
1476
- }
1477
-
1478
- function affectedCoversTargets(affected, targets = {}) {
1479
- const normalized = normalizeAffected(affected);
1480
- const missing = [];
1481
- if (!includesAll(normalized.forms, targets.forms || [])) {
1482
- missing.push(`forms:${(targets.forms || []).filter(item => !normalized.forms.includes(item)).join(',')}`);
1483
- }
1484
- if (!includesAll(normalized.pages, targets.pages || [])) {
1485
- missing.push(`pages:${(targets.pages || []).filter(item => !normalized.pages.includes(item)).join(',')}`);
1486
- }
1487
- for (const key of CODE_RESOURCE_TARGETS) {
1488
- const required = targets[key] || [];
1489
- if (required.length === 0) continue;
1490
- if (normalized.resources || normalized.backend || includesAll(normalized[key], required)) continue;
1491
- missing.push(`${key}:${required.filter(item => !normalized[key].includes(item)).join(',')}`);
1492
- }
1493
- if (targets.resources && !normalized.resources && !normalized.backend) missing.push('resources');
1494
- for (const [key, required] of Object.entries(targets.resourceSelectors || {})) {
1495
- if (normalized.resources || includesAll(normalized.resourceSelectors?.[key], required)) {
1496
- continue;
1497
- }
1498
- missing.push(
1499
- `resourceSelectors.${key}:${required
1500
- .filter(item => !(normalized.resourceSelectors?.[key] || []).includes(item))
1501
- .join(',')}`
1502
- );
1503
- }
1504
- if (targets.runtime && !normalized.runtime) missing.push('runtime');
1505
- return {
1506
- covered: missing.length === 0,
1507
- missing,
1508
- };
1509
- }
1510
-
1511
- function affectedCoversHighRiskFiles(affected, files = []) {
1512
- const normalized = normalizeAffected(affected);
1513
- const missing = [];
1514
- for (const file of files) {
1515
- if (includesAll(normalized.files, [file])) continue;
1516
- if (file === 'app-workspace.config.ts' && !normalized.contracts && !normalized.runtime) {
1517
- missing.push(file);
1518
- } else if (file.startsWith('src/forms/')) {
1519
- const formCode = file.split('/')[2];
1520
- if (!includesAll(normalized.forms, [formCode])) missing.push(file);
1521
- } else if (file.startsWith('src/pages/')) {
1522
- const pageCode = file.split('/')[2];
1523
- if (!normalized.runtime && !includesAll(normalized.pages, [pageCode])) missing.push(file);
1524
- } else if (file.startsWith('src/resources/functions/')) {
1525
- const code = extractCodeFromPath(file, 'src/resources/functions/');
1526
- const isScopedContainer =
1527
- normalized.resources || normalized.backend || normalized.functions.length > 0;
1528
- if (code && !isScopedContainer && !coversCodeTarget(normalized, 'functions', code)) {
1529
- missing.push(file);
1530
- }
1531
- } else if (file.startsWith('src/resources/automations/')) {
1532
- const code = extractCodeFromPath(file, 'src/resources/automations/');
1533
- const isScopedContainer =
1534
- normalized.resources || normalized.backend || normalized.automations.length > 0;
1535
- if (code && !isScopedContainer && !coversCodeTarget(normalized, 'automations', code)) {
1536
- missing.push(file);
1537
- }
1538
- } else if (file.startsWith('src/resources/workflows/')) {
1539
- const code = extractCodeFromPath(file, 'src/resources/workflows/');
1540
- const isScopedContainer =
1541
- normalized.resources || normalized.backend || normalized.workflows.length > 0;
1542
- if (code && !isScopedContainer && !coversCodeTarget(normalized, 'workflows', code)) {
1543
- missing.push(file);
1544
- }
1545
- } else if (file.startsWith('src/resources/js-code-nodes/')) {
1546
- const code = extractCodeFromPath(file, 'src/resources/js-code-nodes/');
1547
- if (code && !coversCodeTarget(normalized, 'jsCodeNodes', code)) missing.push(file);
1548
- } else if (file.startsWith('src/resources/') && !normalized.resources) {
1549
- missing.push(file);
1550
- } else if (file.startsWith('src/functions/')) {
1551
- const code = file.split('/')[2];
1552
- if (!normalized.backend && !normalized.resources && !includesAll(normalized.functions, [code])) missing.push(file);
1553
- } else if (file.startsWith('src/automations/')) {
1554
- const code = file.split('/')[2];
1555
- if (!normalized.backend && !normalized.resources && !includesAll(normalized.automations, [code])) missing.push(file);
1556
- } else if (file.startsWith('src/workflows/')) {
1557
- const code = file.split('/')[2];
1558
- if (!normalized.backend && !normalized.resources && !includesAll(normalized.workflows, [code])) missing.push(file);
1559
- } else if (file.startsWith('src/js-code-nodes/')) {
1560
- const code = file.split('/')[2];
1561
- if (!normalized.backend && !normalized.resources && !includesAll(normalized.jsCodeNodes, [code])) missing.push(file);
1562
- } else if (file.startsWith('src/runtime/') && !normalized.runtime) {
1563
- missing.push(file);
1564
- }
1565
- }
1566
- return {
1567
- covered: missing.length === 0,
1568
- missing,
1569
- };
1570
- }
1571
-
1572
- function normalizeCoverageResources(coverage, fallbackAffected = {}, options = {}) {
1573
- const fallback = options.allowFallback ? normalizeAffected(fallbackAffected) : normalizeAffected({});
1574
- const resources = coverage?.resources || {};
1575
- const requirementCovers = Array.isArray(coverage?.requirements)
1576
- ? coverage.requirements.map(item => item.covers || {})
1577
- : [];
1578
- const mergeList = key => [
1579
- ...splitList(resources[key]),
1580
- ...requirementCovers.flatMap(item => splitList(item[key])),
1581
- ...fallback[key],
1582
- ];
1583
- const hasBool = key =>
1584
- resources[key] === true ||
1585
- resources[key] === 'true' ||
1586
- requirementCovers.some(item => item[key] === true || item[key] === 'true') ||
1587
- Boolean(fallback[key]);
1588
- const selectorSources = [
1589
- resources.resourceSelectors,
1590
- ...requirementCovers.map(item => item.resourceSelectors),
1591
- fallback.resourceSelectors,
1592
- ].map(normalizeResourceSelectors);
1593
- const selectorKeys = Array.from(
1594
- new Set(selectorSources.flatMap(item => Object.keys(item)))
1595
- ).sort();
1596
- return {
1597
- forms: Array.from(new Set(mergeList('forms'))),
1598
- pages: Array.from(new Set(mergeList('pages'))),
1599
- functions: Array.from(new Set(mergeList('functions'))),
1600
- automations: Array.from(new Set(mergeList('automations'))),
1601
- workflows: Array.from(new Set(mergeList('workflows'))),
1602
- jsCodeNodes: Array.from(new Set(mergeList('jsCodeNodes'))),
1603
- files: Array.from(new Set(mergeList('files'))),
1604
- resourceSelectors: Object.fromEntries(
1605
- selectorKeys.map(key => [
1606
- key,
1607
- Array.from(
1608
- new Set(selectorSources.flatMap(item => item[key] || []))
1609
- ).sort(),
1610
- ])
1611
- ),
1612
- resources: hasBool('resources'),
1613
- runtime: hasBool('runtime'),
1614
- backend: hasBool('backend'),
1615
- contracts: hasBool('contracts'),
1616
- };
1617
- }
1618
-
1619
- function coverageCoversTargets(coverage, affected, targets = {}, options = {}) {
1620
- return affectedCoversTargets(normalizeCoverageResources(coverage, affected, options), targets);
1621
- }
1622
-
1623
- function coverageCoversHighRiskFiles(coverage, affected, files = [], options = {}) {
1624
- return affectedCoversHighRiskFiles(normalizeCoverageResources(coverage, affected, options), files);
1625
- }
1626
-
1627
- function getApprovedActiveChanges(rootDir) {
1628
- return listChanges(rootDir)
1629
- .filter(item => item.valid && item.metadata.status === 'approved' && item.metadata.approvedAt)
1630
- .map(item => ({
1631
- ...item.metadata,
1632
- coverage: item.coverage,
1633
- }));
1634
- }
1635
-
1636
- function summarizeSpecFile(rootDir, file) {
1637
- const content = fs.readFileSync(file, 'utf8');
1638
- const rel = path.relative(path.join(rootDir, 'specs'), file).replace(/\\/g, '/');
1639
- const title = content.match(/^#\s+(.+)$/m)?.[1] || rel;
1640
- const purposeIndex = content.indexOf('## Purpose');
1641
- let purpose = '';
1642
- if (purposeIndex >= 0) {
1643
- purpose = content
1644
- .slice(purposeIndex)
1645
- .split(/\r?\n/)
1646
- .slice(1)
1647
- .find(line => line.trim() && !line.startsWith('#')) || '';
1648
- }
1649
- return {
1650
- path: `specs/${rel}`,
1651
- title,
1652
- purpose: purpose.trim(),
1653
- requirementCount: (content.match(/^### Requirement:/gm) || []).length,
1654
- };
1655
- }
1656
-
1657
- function summarizeChange(cwd, item) {
1658
- return {
1659
- id: item.id,
1660
- status: item.metadata?.status || 'invalid',
1661
- approvedAt: item.metadata?.approvedAt || null,
1662
- schemaVersion: item.metadata?.schemaVersion || LEGACY_SDD_SCHEMA_VERSION,
1663
- affected: item.metadata?.affected || null,
1664
- coverage: summarizeCoverage(item.coverage),
1665
- dir: path.relative(cwd, item.dir).replace(/\\/g, '/'),
1666
- };
1667
- }
1668
-
1669
- function summarizeCoverage(coverage) {
1670
- if (!coverage || coverage.__invalid) return null;
1671
- const resources = normalizeCoverageResources(coverage, {});
1672
- return {
1673
- requirements: Array.isArray(coverage.requirements) ? coverage.requirements.length : 0,
1674
- forms: resources.forms,
1675
- pages: resources.pages,
1676
- functions: resources.functions,
1677
- automations: resources.automations,
1678
- workflows: resources.workflows,
1679
- jsCodeNodes: resources.jsCodeNodes,
1680
- resources: resources.resources,
1681
- runtime: resources.runtime,
1682
- };
1683
- }
1684
-
1685
- function chooseActiveChange(changes, targets = {}, highRiskFiles = [], options = {}) {
1686
- const requestedChangeId = options.changeId
1687
- ? normalizeChangeId(options.changeId)
1688
- : null;
1689
- const candidates = changes
1690
- .filter(item => item.valid && !['archived', 'rejected'].includes(item.metadata.status))
1691
- .filter(item => item.metadata.status === 'approved' && item.metadata.approvedAt)
1692
- .filter(item => !requestedChangeId || item.id === requestedChangeId);
1693
- return (
1694
- candidates.find(item => {
1695
- const legacyMode = (item.metadata?.schemaVersion || LEGACY_SDD_SCHEMA_VERSION) !== SDD_SCHEMA_VERSION;
1696
- const allowFallback = legacyMode && (!item.coverage || item.coverage.__invalid);
1697
- const explicitScopes =
1698
- hasExplicitSddScopeModel(item.metadata) ||
1699
- hasExplicitSddScopeModel(item.coverage);
1700
- const scopeModel = explicitScopes
1701
- ? normalizeSddScopeModel(
1702
- item.metadata.scopes || item.coverage?.scopeModel,
1703
- item.metadata.affected
1704
- )
1705
- : null;
1706
- return (
1707
- (scopeModel
1708
- ? affectedCoversTargets(scopeModel.deployTargets, targets)
1709
- : coverageCoversTargets(
1710
- item.coverage,
1711
- item.metadata.affected,
1712
- targets,
1713
- { allowFallback }
1714
- )
1715
- ).covered &&
1716
- coverageCoversHighRiskFiles(
1717
- item.coverage,
1718
- scopeModel?.changedResources || item.metadata.affected,
1719
- highRiskFiles,
1720
- { allowFallback }
1721
- ).covered
1722
- );
1723
- }) || null
1724
- );
1725
- }
1726
-
1727
- function loadedSddScopeModel(loaded) {
1728
- const explicit =
1729
- hasExplicitSddScopeModel(loaded.meta) ||
1730
- hasExplicitSddScopeModel(loaded.coverage) ||
1731
- hasExplicitSddScopeModel(loaded.release);
1732
- if (!explicit) return null;
1733
- const meta = normalizeSddScopeModel(
1734
- loaded.meta.scopes || {},
1735
- loaded.meta.affected
1736
- );
1737
- const emptyScope = normalizeAffected({});
1738
- const coverage = hasExplicitSddScopeModel(loaded.coverage)
1739
- ? normalizeSddScopeModel(
1740
- loaded.coverage.scopeModel,
1741
- loaded.coverage?.resources
1742
- )
1743
- : {
1744
- changedResources: normalizeAffected(
1745
- loaded.coverage?.resources || {}
1746
- ),
1747
- runtimeDependencies: emptyScope,
1748
- deployTargets: emptyScope,
1749
- };
1750
- const release = hasExplicitSddScopeModel(loaded.release)
1751
- ? normalizeSddScopeModel(
1752
- loaded.release.scopeModel,
1753
- loaded.release?.targets
1754
- )
1755
- : {
1756
- changedResources: emptyScope,
1757
- runtimeDependencies: emptyScope,
1758
- deployTargets: emptyScope,
1759
- };
1760
- return {
1761
- schemaVersion: SDD_SCOPE_MODEL_VERSION,
1762
- changedResources: mergeAffectedScopes(
1763
- meta.changedResources,
1764
- coverage.changedResources,
1765
- loaded.meta.affected,
1766
- loaded.coverage?.resources
1767
- ),
1768
- runtimeDependencies: mergeAffectedScopes(
1769
- meta.runtimeDependencies,
1770
- coverage.runtimeDependencies,
1771
- release.runtimeDependencies
1772
- ),
1773
- deployTargets: mergeAffectedScopes(
1774
- meta.deployTargets,
1775
- coverage.deployTargets,
1776
- release.deployTargets,
1777
- loaded.release?.targets
1778
- ),
1779
- };
1780
- }
1781
-
1782
- function getSddChangeScope(options = {}) {
1783
- const loaded = loadSddChange(options);
1784
- const affected = normalizeAffected(loaded.meta.affected || {});
1785
- const coverage = normalizeCoverageResources(loaded.coverage, {});
1786
- const releaseTargets = loaded.release && !loaded.release.__invalid
1787
- ? loaded.release.targets || {}
1788
- : {};
1789
- const scopeModel = loadedSddScopeModel(loaded);
1790
- const targetSources = scopeModel
1791
- ? [scopeModel.deployTargets, releaseTargets]
1792
- : [releaseTargets, coverage, affected];
1793
- const mergeList = key => Array.from(new Set([
1794
- ...targetSources.flatMap(source => splitList(source?.[key])),
1795
- ])).sort();
1796
- const targets = {
1797
- forms: mergeList('forms'),
1798
- pages: mergeList('pages'),
1799
- functions: mergeList('functions'),
1800
- automations: mergeList('automations'),
1801
- workflows: mergeList('workflows'),
1802
- jsCodeNodes: mergeList('jsCodeNodes'),
1803
- resourceSelectors: Object.fromEntries(
1804
- Array.from(
1805
- new Set([
1806
- ...targetSources.flatMap(source =>
1807
- Object.keys(source?.resourceSelectors || {})
1808
- ),
1809
- ])
1810
- )
1811
- .sort()
1812
- .map(key => [
1813
- key,
1814
- Array.from(
1815
- new Set([
1816
- ...targetSources.flatMap(source =>
1817
- splitList(source?.resourceSelectors?.[key])
1818
- ),
1819
- ])
1820
- ).sort(),
1821
- ])
1822
- ),
1823
- resources: targetSources.some(source => Boolean(source?.resources)),
1824
- runtime: targetSources.some(source => Boolean(source?.runtime)),
1825
- other: Array.from(new Set(splitList(releaseTargets.other))).sort(),
1826
- };
1827
- const recordedChangedFiles = splitList(loaded.release?.changedFiles);
1828
- const changedResourceScope =
1829
- scopeModel?.changedResources ||
1830
- mergeAffectedScopes(coverage, affected);
1831
- const ownedFiles = Array.from(new Set([
1832
- ...splitList(changedResourceScope.files),
1833
- ])).sort();
1834
- const changedFiles = Array.from(new Set([
1835
- ...recordedChangedFiles,
1836
- ...ownedFiles,
1837
- ])).sort();
1838
- const releaseCommands = Array.isArray(loaded.release?.commands)
1839
- ? loaded.release.commands.filter(command => String(command || '').trim())
1840
- : [];
1841
- const plannedCommands = Array.isArray(loaded.meta?.release?.plannedCommands)
1842
- ? loaded.meta.release.plannedCommands.filter(command => String(command || '').trim())
1843
- : [];
1844
- return {
1845
- changeId: loaded.changeId,
1846
- change: loaded.meta,
1847
- coverage: loaded.coverage,
1848
- release: loaded.release,
1849
- scopeModel,
1850
- scopeModelExplicit: Boolean(scopeModel),
1851
- targets,
1852
- changedFiles,
1853
- recordedChangedFiles,
1854
- ownedFiles,
1855
- commands: releaseCommands.length > 0 ? releaseCommands : plannedCommands,
1856
- releaseCommands,
1857
- plannedCommands,
1858
- };
1859
- }
1860
-
1861
- function buildNextActions(governance, activeChanges, activeChange, targets, highRiskFiles) {
1862
- if (!governance.enabled) return ['Enable SDD governance or run without SDD for low-risk work.'];
1863
- if (!governance.initialized) return ['Run `openxiangda sdd init --upgrade`.'];
1864
- if (hasHighRiskTargets(targets) || highRiskFiles.length > 0) {
1865
- if (!activeChanges.length) return ['Run `openxiangda sdd propose <change>` before editing high-risk files.'];
1866
- if (!activeChange) return ['Approve or create an SDD change whose coverage matches the changed resources.'];
1867
- if (activeChange.metadata?.status !== 'approved') {
1868
- return [`Run \`openxiangda sdd approve ${activeChange.id}\` after user confirmation.`];
1869
- }
1870
- return [
1871
- `Complete openspec/changes/${activeChange.id}/tasks.md and evidence.md`,
1872
- `Run openxiangda sdd verify ${activeChange.id} --changed`,
1873
- `Archive with openxiangda sdd archive ${activeChange.id} after release`,
1874
- ];
1875
- }
1876
- return activeChanges.length
1877
- ? ['Continue or archive active SDD changes when finished.']
1878
- : ['No high-risk SDD action required for the current release target.'];
1879
- }
1880
-
1881
- function buildSddGovernanceReport(options = {}) {
1882
- const governance = getSddGovernanceConfig(options);
1883
- const targets = options.targets || {};
1884
- const files = options.files || [];
1885
- const highRiskFiles = classifyHighRiskFiles(files);
1886
- const checks = [];
1887
- const warnings = [];
1888
- const errors = [];
1889
- let activeChanges = [];
1890
- let activeChange = null;
1891
- let coverage = null;
1892
-
1893
- if (!governance.enabled) {
1894
- warnings.push('未启用 SDD governance;运行 openxiangda sdd init 可开启高风险变更记录。');
1895
- checks.push({ name: 'sdd', status: 'warn', message: warnings[0] });
1896
- return {
1897
- schemaVersion: governance.schemaVersion,
1898
- governance,
1899
- releaseTargets: targets,
1900
- highRiskFiles,
1901
- activeChange,
1902
- coverage,
1903
- activeChanges,
1904
- checks,
1905
- warnings,
1906
- errors,
1907
- blockingErrors: errors,
1908
- nextActions: buildNextActions(governance, activeChanges, activeChange, targets, highRiskFiles),
1909
- };
1910
- }
1911
-
1912
- if (!governance.initialized) {
1913
- const message = `SDD 已配置但未初始化: ${governance.path}。运行 openxiangda sdd init --upgrade`;
1914
- if (governance.strictHighRisk && (hasHighRiskTargets(targets) || highRiskFiles.length > 0)) {
1915
- errors.push({ name: 'sdd-not-initialized', message });
1916
- checks.push({ name: 'sdd', status: 'error', message });
1917
- } else {
1918
- warnings.push(message);
1919
- checks.push({ name: 'sdd', status: 'warn', message });
1920
- }
1921
- return {
1922
- schemaVersion: governance.schemaVersion,
1923
- governance,
1924
- releaseTargets: targets,
1925
- highRiskFiles,
1926
- activeChange,
1927
- coverage,
1928
- activeChanges,
1929
- checks,
1930
- warnings,
1931
- errors,
1932
- blockingErrors: errors,
1933
- nextActions: buildNextActions(governance, activeChanges, activeChange, targets, highRiskFiles),
1934
- };
1935
- }
1936
-
1937
- const requestedChangeId = options.changeId
1938
- ? normalizeChangeId(options.changeId)
1939
- : null;
1940
- const changes = listChanges(governance.rootDir);
1941
- activeChanges = changes.filter(
1942
- item => item.valid && !['archived', 'rejected'].includes(item.metadata.status)
1943
- );
1944
- activeChange = chooseActiveChange(activeChanges, targets, highRiskFiles, {
1945
- changeId: requestedChangeId,
1946
- });
1947
- coverage = activeChange?.coverage || null;
1948
- const approvedAll = getApprovedActiveChanges(governance.rootDir);
1949
- const approved = requestedChangeId
1950
- ? approvedAll.filter(item => item.id === requestedChangeId)
1951
- : approvedAll;
1952
- if (!governance.strictHighRisk || (!hasHighRiskTargets(targets) && highRiskFiles.length === 0)) {
1953
- checks.push({ name: 'sdd', status: 'ok', message: `${activeChanges.length} active changes` });
1954
- return {
1955
- schemaVersion: governance.schemaVersion,
1956
- governance,
1957
- releaseTargets: targets,
1958
- highRiskFiles,
1959
- activeChange: activeChange ? summarizeChange(options.cwd || process.cwd(), activeChange) : null,
1960
- coverage: summarizeCoverage(coverage),
1961
- activeChanges: activeChanges.map(item => summarizeChange(options.cwd || process.cwd(), item)),
1962
- checks,
1963
- warnings,
1964
- errors,
1965
- blockingErrors: errors,
1966
- nextActions: buildNextActions(governance, activeChanges, activeChange, targets, highRiskFiles),
1967
- };
1968
- }
1969
-
1970
- const requestedChange = requestedChangeId
1971
- ? activeChanges.find(item => item.id === requestedChangeId)
1972
- : null;
1973
- if (requestedChangeId && !requestedChange) {
1974
- errors.push({
1975
- name: 'sdd-change-not-found',
1976
- message: `指定的 SDD change 不存在或已归档: ${requestedChangeId}`,
1977
- });
1978
- } else if (requestedChangeId && approved.length === 0) {
1979
- errors.push({
1980
- name: 'sdd-change-not-approved',
1981
- message: `指定的 SDD change 尚未 approve: ${requestedChangeId}`,
1982
- });
1983
- } else if (approved.length === 0) {
1984
- errors.push({
1985
- name: 'sdd-approved-change-required',
1986
- message: '高风险变更发布前必须存在 approved SDD change;先运行 openxiangda sdd propose <change> 并经 openxiangda sdd approve <change> 确认。',
1987
- });
1988
- } else if (!activeChange) {
1989
- errors.push({
1990
- name: 'sdd-coverage-required',
1991
- message: requestedChangeId
1992
- ? `指定 change ${requestedChangeId} 的 coverage.json 未覆盖本次发布目标或高风险文件。`
1993
- : 'approved SDD change 的 coverage.json 未覆盖本次发布目标或高风险文件。',
1994
- });
1995
- } else {
1996
- const verify = verifySddChange({
1997
- cwd: options.cwd,
1998
- configText: options.configText,
1999
- changeId: activeChange.id,
2000
- targets,
2001
- files,
2002
- stage: options.stage || options.verifyStage,
2003
- releasePlan: {
2004
- targets,
2005
- changedFiles: files,
2006
- commands: options.commands || [],
2007
- actualCommands: options.actualCommands === true,
2008
- },
2009
- });
2010
- if (!verify.passed) {
2011
- errors.push(...verify.errors);
2012
- warnings.push(...verify.warnings.map(item => `${item.name}: ${item.message}`));
2013
- }
2014
- }
2015
-
2016
- checks.push({
2017
- name: 'sdd',
2018
- status: errors.length > 0 ? 'error' : 'ok',
2019
- message: errors.length > 0 ? `${errors.length} 个 SDD gate 错误` : `${approved.length} approved active changes`,
2020
- });
2021
- return {
2022
- schemaVersion: governance.schemaVersion,
2023
- governance,
2024
- releaseTargets: targets,
2025
- highRiskFiles,
2026
- activeChange: activeChange ? summarizeChange(options.cwd || process.cwd(), activeChange) : null,
2027
- coverage: summarizeCoverage(coverage),
2028
- activeChanges: activeChanges.map(item => summarizeChange(options.cwd || process.cwd(), item)),
2029
- checks,
2030
- warnings,
2031
- errors,
2032
- blockingErrors: errors,
2033
- nextActions: buildNextActions(governance, activeChanges, activeChange, targets, highRiskFiles),
2034
- };
2035
- }
2036
-
2037
- function readUncheckedChecklist(file, missingMessage) {
2038
- if (!fs.existsSync(file)) return [missingMessage || `${path.basename(file)} 缺失`];
2039
- return fs
2040
- .readFileSync(file, 'utf8')
2041
- .split(/\r?\n/)
2042
- .map((line, index) => ({ line, index: index + 1 }))
2043
- .filter(item => /^-\s*\[\s\]/.test(item.line))
2044
- .map(item => `${path.basename(file)}:${item.index} ${item.line}`);
2045
- }
2046
-
2047
- function normalizeSddVerifyStage(value) {
2048
- const stage = String(value || 'default').trim().toLowerCase();
2049
- if (['default', 'legacy', 'all'].includes(stage)) return 'default';
2050
- if (stage === 'pre-publish') return 'prepublish';
2051
- if (stage === 'post-publish') return 'postpublish';
2052
- if (!SDD_VERIFY_STAGES.includes(stage)) {
2053
- throw new Error(
2054
- `未知 SDD verify stage: ${value}。可选 implementation, prepublish, postpublish, archive`
2055
- );
2056
- }
2057
- return stage;
2058
- }
2059
-
2060
- function getSddVerifyStagePolicy(stageValue) {
2061
- const stage = normalizeSddVerifyStage(stageValue);
2062
- if (stage === 'default') {
2063
- return {
2064
- stage,
2065
- // An unspecified verify is a prepublish check. Postpublish/archive
2066
- // evidence cannot exist yet and must never block ordinary development.
2067
- taskStages: new Set(['implementation', 'prepublish']),
2068
- evidenceStages: new Set(['prepublish']),
2069
- requireRelease: true,
2070
- };
2071
- }
2072
- const rank = SDD_VERIFY_STAGES.indexOf(stage);
2073
- return {
2074
- stage,
2075
- taskStages:
2076
- stage === 'implementation'
2077
- ? new Set()
2078
- : new Set(SDD_VERIFY_STAGES.slice(0, rank + 1)),
2079
- evidenceStages:
2080
- stage === 'implementation'
2081
- ? new Set()
2082
- : new Set(SDD_VERIFY_STAGES.slice(1, rank + 1)),
2083
- requireRelease: stage !== 'implementation',
2084
- };
2085
- }
2086
-
2087
- function inferChecklistStage(kind, section, line) {
2088
- const explicit = line.match(/sdd-stage\s*:\s*(implementation|prepublish|postpublish|archive)/i)?.[1];
2089
- if (explicit) return explicit.toLowerCase();
2090
- const normalizedSection = String(section || '').toLowerCase().replace(/[^a-z]/g, '');
2091
- if (normalizedSection.includes('implementation')) return 'implementation';
2092
- if (normalizedSection.includes('prepublish') || normalizedSection.includes('verification')) {
2093
- return 'prepublish';
2094
- }
2095
- if (normalizedSection.includes('postpublish') || normalizedSection === 'release') {
2096
- return 'postpublish';
2097
- }
2098
- if (normalizedSection.includes('archive')) return 'archive';
2099
- if (/archive|归档/i.test(line)) return 'archive';
2100
- if (/publish\/deploy result|publish output|release output|发布结果|上线结果/i.test(line)) {
2101
- return 'postpublish';
2102
- }
2103
- if (
2104
- kind === 'evidence' ||
2105
- /verification evidence|release plan|workspace\/resource\/runtime check|dry-run|发布计划|验证证据/i.test(line)
2106
- ) {
2107
- return 'prepublish';
2108
- }
2109
- return 'implementation';
2110
- }
2111
-
2112
- function readUncheckedChecklistForStages(file, options = {}) {
2113
- const requiredStages = options.requiredStages || new Set(SDD_VERIFY_STAGES);
2114
- if (requiredStages.size === 0) return [];
2115
- if (!fs.existsSync(file)) {
2116
- return [options.missingMessage || `${path.basename(file)} 缺失`];
2117
- }
2118
- const result = [];
2119
- let section = '';
2120
- const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
2121
- for (let index = 0; index < lines.length; index += 1) {
2122
- const line = lines[index];
2123
- const heading = line.match(/^##\s+(.+)$/);
2124
- if (heading) {
2125
- section = heading[1].trim();
2126
- continue;
2127
- }
2128
- if (!/^-\s*\[\s\]/.test(line)) continue;
2129
- const stage = inferChecklistStage(options.kind || 'tasks', section, line);
2130
- if (!requiredStages.has(stage)) continue;
2131
- result.push(`${path.basename(file)}:${index + 1} [${stage}] ${line}`);
2132
- }
2133
- return result;
2134
- }
2135
-
2136
- function readUncheckedTasks(changeDir, stagePolicy = getSddVerifyStagePolicy('default')) {
2137
- return readUncheckedChecklistForStages(path.join(changeDir, 'tasks.md'), {
2138
- kind: 'tasks',
2139
- requiredStages: stagePolicy.taskStages,
2140
- missingMessage: 'tasks.md 缺失',
2141
- });
2142
- }
2143
-
2144
- function validateCoverageManifest(
2145
- coverage,
2146
- meta,
2147
- targets,
2148
- highRiskFiles,
2149
- legacyMode = false,
2150
- options = {}
2151
- ) {
2152
- const errors = [];
2153
- const warnings = [];
2154
- const addDocumentationIssue = issue => {
2155
- if (options.strictDocumentation) errors.push(issue);
2156
- else warnings.push(issue);
2157
- };
2158
- if (!coverage || coverage.__invalid) {
2159
- const message = coverage?.__invalid
2160
- ? `coverage.json 无效: ${coverage.error}`
2161
- : 'coverage.json 缺失';
2162
- if (legacyMode) warnings.push({ name: 'sdd-coverage-missing', message });
2163
- else errors.push({ name: 'sdd-coverage-missing', message });
2164
- return { errors, warnings };
2165
- }
2166
- if (coverage.schemaVersion && coverage.schemaVersion !== SDD_SCHEMA_VERSION) {
2167
- warnings.push({
2168
- name: 'sdd-coverage-schema-version',
2169
- message: `coverage schemaVersion=${coverage.schemaVersion},建议升级为 ${SDD_SCHEMA_VERSION}`,
2170
- });
2171
- }
2172
- if (!Array.isArray(coverage.requirements) || coverage.requirements.length === 0) {
2173
- addDocumentationIssue({
2174
- name: 'sdd-coverage-empty',
2175
- message: 'coverage.json 建议包含 requirements 映射;streamlined 模式仅以精确资源和文件范围作为发布硬门禁',
2176
- });
2177
- } else {
2178
- for (const requirement of coverage.requirements) {
2179
- if (!requirement.id || !requirement.title || !requirement.spec) {
2180
- addDocumentationIssue({
2181
- name: 'sdd-coverage-requirement-invalid',
2182
- message: 'coverage requirement 缺少 id/title/spec',
2183
- });
2184
- break;
2185
- }
2186
- if (!Array.isArray(requirement.scenarios) || requirement.scenarios.length === 0) {
2187
- addDocumentationIssue({
2188
- name: 'sdd-coverage-scenarios-missing',
2189
- message: `coverage requirement ${requirement.id} 缺少 scenarios`,
2190
- });
2191
- break;
2192
- }
2193
- }
2194
- }
2195
- const authoredChangeScope =
2196
- options.scopeModel?.changedResources || meta.affected;
2197
- const coverageTargets = options.scopeModel
2198
- ? authoredChangeScope
2199
- : targets;
2200
- const targetCoverage = coverageCoversTargets(
2201
- coverage,
2202
- authoredChangeScope,
2203
- coverageTargets
2204
- );
2205
- if (!targetCoverage.covered) {
2206
- errors.push({
2207
- name: 'sdd-change-targets-uncovered',
2208
- message: `coverage 未覆盖发布目标: ${targetCoverage.missing.join(', ')}`,
2209
- });
2210
- }
2211
- const fileCoverage = coverageCoversHighRiskFiles(
2212
- coverage,
2213
- authoredChangeScope,
2214
- highRiskFiles
2215
- );
2216
- if (!fileCoverage.covered) {
2217
- errors.push({
2218
- name: 'sdd-change-files-uncovered',
2219
- message: `coverage 未覆盖高风险文件: ${fileCoverage.missing.join(', ')}`,
2220
- });
2221
- }
2222
- return { errors, warnings };
2223
- }
2224
-
2225
- function splitDeltaSections(content) {
2226
- const sections = [];
2227
- const regex = /^##\s+(ADDED|MODIFIED|REMOVED)\s+Requirements\b.*$/gim;
2228
- const matches = [...content.matchAll(regex)];
2229
- for (let index = 0; index < matches.length; index += 1) {
2230
- const match = matches[index];
2231
- const start = match.index + match[0].length;
2232
- const end = matches[index + 1]?.index ?? content.length;
2233
- sections.push({
2234
- type: match[1].toUpperCase(),
2235
- body: content.slice(start, end),
2236
- });
2237
- }
2238
- return sections;
2239
- }
2240
-
2241
- function parseRequirementBlocks(sectionBody) {
2242
- const regex = /^###\s+Requirement:\s*(.+)$/gim;
2243
- const matches = [...sectionBody.matchAll(regex)];
2244
- const blocks = [];
2245
- for (let index = 0; index < matches.length; index += 1) {
2246
- const match = matches[index];
2247
- const start = match.index;
2248
- const end = matches[index + 1]?.index ?? sectionBody.length;
2249
- blocks.push({
2250
- title: match[1].trim(),
2251
- content: sectionBody.slice(start, end).trim(),
2252
- });
2253
- }
2254
- return blocks;
2255
- }
2256
-
2257
- function parseDeltaSpec(content) {
2258
- const sections = splitDeltaSections(content);
2259
- const result = { added: [], modified: [], removed: [] };
2260
- for (const section of sections) {
2261
- const key = section.type.toLowerCase();
2262
- result[key].push(...parseRequirementBlocks(section.body));
2263
- }
2264
- return result;
2265
- }
2266
-
2267
- function validateDeltaSpecFile(file, rootDir) {
2268
- const rel = path.relative(rootDir, file).replace(/\\/g, '/');
2269
- const content = fs.readFileSync(file, 'utf8');
2270
- const errors = [];
2271
- const warnings = [];
2272
- const sections = splitDeltaSections(content);
2273
- if (sections.length === 0) {
2274
- errors.push({ name: 'sdd-delta-section-missing', message: `${rel} 缺少 ADDED/MODIFIED/REMOVED Requirements 段落` });
2275
- return { errors, warnings };
2276
- }
2277
- const delta = parseDeltaSpec(content);
2278
- const all = [...delta.added, ...delta.modified, ...delta.removed];
2279
- if (all.length === 0) {
2280
- errors.push({ name: 'sdd-delta-requirement-missing', message: `${rel} 缺少 Requirement` });
2281
- }
2282
- for (const requirement of [...delta.added, ...delta.modified]) {
2283
- if (!/####\s+Scenario:/i.test(requirement.content)) {
2284
- errors.push({ name: 'sdd-scenario-missing', message: `${rel} requirement "${requirement.title}" 缺少 Scenario` });
2285
- continue;
2286
- }
2287
- if (!/\bGiven\b|\*\*Given\*\*/i.test(requirement.content) || !/\bWhen\b|\*\*When\*\*/i.test(requirement.content) || !/\bThen\b|\*\*Then\*\*/i.test(requirement.content)) {
2288
- errors.push({ name: 'sdd-scenario-unverifiable', message: `${rel} requirement "${requirement.title}" 场景必须包含 Given/When/Then` });
2289
- }
2290
- if (PLACEHOLDER_PATTERNS.some(pattern => pattern.test(requirement.content))) {
2291
- errors.push({ name: 'sdd-spec-placeholder', message: `${rel} requirement "${requirement.title}" 仍包含占位内容` });
2292
- }
2293
- }
2294
- for (const requirement of delta.removed) {
2295
- if (!/because|reason|原因|废弃|移除/i.test(requirement.content)) {
2296
- warnings.push({ name: 'sdd-removed-reason-missing', message: `${rel} removed requirement "${requirement.title}" 建议写明原因` });
2297
- }
2298
- }
2299
- return { errors, warnings };
2300
- }
2301
-
2302
- function validateDeltaSpecs(changeDir) {
2303
- const specRoot = path.join(changeDir, 'specs');
2304
- const files = listMarkdownFiles(specRoot);
2305
- const errors = [];
2306
- const warnings = [];
2307
- if (files.length === 0) {
2308
- errors.push({ name: 'sdd-delta-specs-missing', message: 'change 缺少 specs/**/*.md delta spec' });
2309
- return { errors, warnings, files: [] };
2310
- }
2311
- for (const file of files) {
2312
- const validation = validateDeltaSpecFile(file, changeDir);
2313
- errors.push(...validation.errors);
2314
- warnings.push(...validation.warnings);
2315
- }
2316
- return {
2317
- errors,
2318
- warnings,
2319
- files: files.map(file => path.relative(changeDir, file).replace(/\\/g, '/')),
2320
- };
2321
- }
2322
-
2323
- function buildUpdatedReleaseManifest(loaded, releasePlan, verificationStage = 'default') {
2324
- const verifiedAt = nowIso();
2325
- const reviewedCommands = Array.isArray(loaded.release?.commands)
2326
- ? loaded.release.commands.filter(command => String(command || '').trim())
2327
- : [];
2328
- const canonicalCommands = Array.isArray(releasePlan?.commands)
2329
- ? releasePlan.commands.filter(command => String(command || '').trim())
2330
- : [];
2331
- const actualExecution = releasePlan?.actualCommands === true;
2332
- return {
2333
- schemaVersion: SDD_SCHEMA_VERSION,
2334
- changeId: loaded.changeId,
2335
- ...(loaded.release && !loaded.release.__invalid ? loaded.release : {}),
2336
- targets:
2337
- actualExecution
2338
- ? loaded.release?.targets || {}
2339
- : releasePlan?.activationTargets ||
2340
- releasePlan?.targets ||
2341
- loaded.release?.targets ||
2342
- {},
2343
- changedFiles:
2344
- actualExecution
2345
- ? loaded.release?.changedFiles || []
2346
- : releasePlan?.changedFiles || loaded.release?.changedFiles || [],
2347
- commands:
2348
- !actualExecution && canonicalCommands.length > 0
2349
- ? canonicalCommands
2350
- : reviewedCommands,
2351
- verifiedAt,
2352
- verificationStage,
2353
- verificationStages: {
2354
- ...(loaded.release?.verificationStages || {}),
2355
- [verificationStage]: { verifiedAt },
2356
- },
2357
- };
2358
- }
2359
-
2360
- function updateReleaseManifest(loaded, releasePlan, verificationStage = 'default') {
2361
- const release = buildUpdatedReleaseManifest(loaded, releasePlan, verificationStage);
2362
- writeJsonFile(loaded.releaseFile, release);
2363
- return release;
2364
- }
2365
-
2366
- function writeReleaseAttestation(cwd, changeId, name, value) {
2367
- const root = path.resolve(cwd || process.cwd());
2368
- const dir = path.join(
2369
- root,
2370
- '.openxiangda',
2371
- 'releases',
2372
- normalizeChangeId(changeId)
2373
- );
2374
- ensureDir(dir);
2375
- const file = path.join(dir, name);
2376
- const tempFile = `${file}.${process.pid}.${Date.now()}.tmp`;
2377
- fs.writeFileSync(tempFile, `${JSON.stringify(value, null, 2)}\n`, {
2378
- mode: 0o600,
2379
- });
2380
- fs.renameSync(tempFile, file);
2381
- return path.relative(root, file).replace(/\\/g, '/');
2382
- }
2383
-
2384
- function readReleaseAttestation(cwd, changeId, name) {
2385
- const file = path.join(
2386
- path.resolve(cwd || process.cwd()),
2387
- '.openxiangda',
2388
- 'releases',
2389
- normalizeChangeId(changeId),
2390
- name
2391
- );
2392
- if (!fs.existsSync(file)) return null;
2393
- const value = readJsonFile(file, null);
2394
- return value && !value.__invalid ? value : null;
2395
- }
2396
-
2397
- function recordSddReleaseSourceRevision(options = {}) {
2398
- const loaded = loadSddChange(options);
2399
- const input = options.releaseSourceRevision;
2400
- if (!input || typeof input !== 'object' || Array.isArray(input)) {
2401
- throw new Error('releaseSourceRevision 必须是对象');
2402
- }
2403
- const releaseSourceRevision = {
2404
- repo: String(input.repo || input.repositoryId || '').trim(),
2405
- baseCommit: String(input.baseCommit || '').trim().toLowerCase(),
2406
- treeHash: String(input.treeHash || '').trim().toLowerCase(),
2407
- branch: String(input.branch || '').trim() || null,
2408
- remoteName: String(input.remoteName || '').trim() || null,
2409
- remoteUrlHash: String(input.remoteUrlHash || '').trim().toLowerCase() || null,
2410
- remoteUrlHashAliases: Array.isArray(input.remoteUrlHashAliases)
2411
- ? input.remoteUrlHashAliases
2412
- .map(value => String(value || '').trim().toLowerCase())
2413
- .filter(value => /^sha256:[0-9a-f]{64}$/.test(value))
2414
- : [],
2415
- repoAliases: Array.isArray(input.repoAliases)
2416
- ? input.repoAliases
2417
- .map(value => String(value || '').trim().toLowerCase())
2418
- .filter(value => /^sha256:[0-9a-f]{64}$/.test(value))
2419
- : [],
2420
- mainBranch: String(input.mainBranch || '').trim() || null,
2421
- mainTipCommit: String(input.mainTipCommit || '').trim().toLowerCase() || null,
2422
- capturedAt: input.capturedAt || nowIso(),
2423
- };
2424
- if (
2425
- !/^sha256:[0-9a-f]{64}$/i.test(releaseSourceRevision.repo) ||
2426
- !/^[0-9a-f]{40,64}$/.test(releaseSourceRevision.baseCommit) ||
2427
- !/^[0-9a-f]{40,64}$/.test(releaseSourceRevision.treeHash) ||
2428
- !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(releaseSourceRevision.remoteName || '') ||
2429
- !/^sha256:[0-9a-f]{64}$/.test(releaseSourceRevision.remoteUrlHash || '') ||
2430
- !['main', 'master'].includes(releaseSourceRevision.mainBranch)
2431
- ) {
2432
- throw new Error(
2433
- 'releaseSourceRevision 缺少有效的 repo/baseCommit/treeHash/remoteName/remoteUrlHash/mainBranch'
2434
- );
2435
- }
2436
- const existing =
2437
- readReleaseAttestation(
2438
- options.cwd || process.cwd(),
2439
- loaded.changeId,
2440
- 'source-revision.json'
2441
- )?.releaseSourceRevision ||
2442
- loaded.release?.releaseSourceRevision;
2443
- if (
2444
- existing?.baseCommit &&
2445
- (existing.baseCommit !== releaseSourceRevision.baseCommit ||
2446
- existing.treeHash !== releaseSourceRevision.treeHash ||
2447
- existing.repo !== releaseSourceRevision.repo ||
2448
- existing.remoteName !== releaseSourceRevision.remoteName ||
2449
- existing.remoteUrlHash !== releaseSourceRevision.remoteUrlHash ||
2450
- existing.mainBranch !== releaseSourceRevision.mainBranch)
2451
- ) {
2452
- const error = new Error(
2453
- `RELEASE_PUBLISH_REVISION_CHANGED: change ${loaded.changeId} 已冻结 ${existing.baseCommit},不能改为 ${releaseSourceRevision.baseCommit}`
2454
- );
2455
- error.code = 'RELEASE_PUBLISH_REVISION_CHANGED';
2456
- throw error;
2457
- }
2458
- writeReleaseAttestation(
2459
- options.cwd || process.cwd(),
2460
- loaded.changeId,
2461
- 'source-revision.json',
2462
- {
2463
- schemaVersion: SDD_SCHEMA_VERSION,
2464
- changeId: loaded.changeId,
2465
- mainlinePolicy: 'publish-from-main-v1',
2466
- releaseSourceRevision,
2467
- }
2468
- );
2469
- return {
2470
- ...(loaded.release && !loaded.release.__invalid ? loaded.release : {}),
2471
- releaseSourceRevision,
2472
- };
2473
- }
2474
-
2475
- function validateReleaseManifest(
2476
- release,
2477
- releasePlan,
2478
- legacyMode = false,
2479
- options = {}
2480
- ) {
2481
- const errors = [];
2482
- const warnings = [];
2483
- if (!release || release.__invalid) {
2484
- const issue = {
2485
- name: 'sdd-release-invalid',
2486
- message: release?.__invalid ? `release.json 无效: ${release.error}` : 'release.json 缺失',
2487
- };
2488
- if (legacyMode) warnings.push(issue);
2489
- else errors.push(issue);
2490
- return { errors, warnings };
2491
- }
2492
- const plannedTargets =
2493
- releasePlan?.activationTargets || releasePlan?.targets || null;
2494
- const targets = plannedTargets || release.targets || {};
2495
- if (plannedTargets) {
2496
- const releaseTargetCoverage = affectedCoversTargets(
2497
- release.targets || {},
2498
- plannedTargets
2499
- );
2500
- if (!releaseTargetCoverage.covered) {
2501
- errors.push({
2502
- name: 'sdd-release-targets-uncovered',
2503
- message: `release.json targets 未覆盖发布目标: ${releaseTargetCoverage.missing.join(', ')}`,
2504
- });
2505
- }
2506
- }
2507
- if (hasHighRiskTargets(targets) && !Array.isArray(release.commands)) {
2508
- errors.push({ name: 'sdd-release-commands-missing', message: 'release.json commands 必须是数组' });
2509
- }
2510
- if (
2511
- !options.actualExecution &&
2512
- releasePlan?.commands?.length &&
2513
- (!Array.isArray(release.commands) || release.commands.length === 0)
2514
- ) {
2515
- errors.push({ name: 'sdd-release-commands-empty', message: 'release.json 未记录发布命令' });
2516
- }
2517
- if (
2518
- !legacyMode &&
2519
- !options.actualExecution &&
2520
- Array.isArray(release.commands) &&
2521
- release.commands.length > 0
2522
- ) {
2523
- errors.push(
2524
- ...validateAtomicReleaseCommands(
2525
- release.commands,
2526
- targets,
2527
- release.changeId
2528
- )
2529
- );
2530
- }
2531
- return { errors, warnings };
2532
- }
2533
-
2534
- function commandFlagValue(command, name) {
2535
- const match = String(command || '').match(
2536
- new RegExp(`(?:^|\\s)--${escapeRegExp(name)}(?:=|\\s+)([^\\s]+)`)
2537
- );
2538
- return match ? match[1] : null;
2539
- }
2540
-
2541
- function normalizedCommandSelectors(command) {
2542
- return Array.from(
2543
- new Set(
2544
- String(commandFlagValue(command, 'only') || commandFlagValue(command, 'code') || '')
2545
- .split(',')
2546
- .map(value => value.trim())
2547
- .filter(Boolean)
2548
- )
2549
- ).sort();
2550
- }
2551
-
2552
- function normalizedResourcePublishTypes(command) {
2553
- const match = String(command || '').match(
2554
- /^openxiangda\s+resource\s+publish(?:\s+([^\s-][^\s]*))?/
2555
- );
2556
- if (!match?.[1]) return [];
2557
- const aliases = {
2558
- functions: 'function',
2559
- automations: 'automation',
2560
- workflows: 'workflow',
2561
- 'form-settings': 'form-setting',
2562
- };
2563
- return Array.from(
2564
- new Set(
2565
- match[1]
2566
- .split(',')
2567
- .map(value => value.trim().toLowerCase())
2568
- .filter(Boolean)
2569
- .map(value => aliases[value] || value)
2570
- )
2571
- ).sort();
2572
- }
2573
-
2574
- function sameStringSet(left, right) {
2575
- const normalizedLeft = Array.from(new Set(left || [])).sort();
2576
- const normalizedRight = Array.from(new Set(right || [])).sort();
2577
- return (
2578
- normalizedLeft.length === normalizedRight.length &&
2579
- normalizedLeft.every((value, index) => value === normalizedRight[index])
2580
- );
2581
- }
2582
-
2583
- function validateAtomicReleaseCommands(
2584
- commands,
2585
- targets,
2586
- changeId,
2587
- options = {}
2588
- ) {
2589
- const errors = [];
2590
- const requireCompletePlan = options.requireCompletePlan !== false;
2591
- const normalized = commands.map(command => String(command || '').trim());
2592
- const expectedFormPermissionGroups =
2593
- targets.resourceSelectors?.formPermissionGroups || [];
2594
- const expectedFormTypes = [
2595
- ...((targets.forms || []).length > 0 ? ['form-setting'] : []),
2596
- ...(expectedFormPermissionGroups.length > 0
2597
- ? ['form-permission-group']
2598
- : []),
2599
- ].sort();
2600
- const qualifyFormSelectors = expectedFormTypes.length > 1;
2601
- const expectedFormSelectors = [
2602
- ...(targets.forms || []).map(code =>
2603
- qualifyFormSelectors ? `form-setting:${code}` : String(code)
2604
- ),
2605
- ...expectedFormPermissionGroups.map(code =>
2606
- qualifyFormSelectors ? `form-permission-group:${code}` : String(code)
2607
- ),
2608
- ].sort();
2609
- const publishCommands = normalized.filter(command =>
2610
- /^openxiangda\s+(?:workspace\s+publish|resource\s+publish|runtime\s+deploy|release\s+app-finalize)\b/.test(
2611
- command
2612
- )
2613
- );
2614
- for (const command of publishCommands) {
2615
- const commandTypes = normalizedResourcePublishTypes(command);
2616
- if (!/\s--profile(?:=|\s+)\S+/.test(command) || /<profile>/.test(command)) {
2617
- errors.push({
2618
- name: 'sdd-release-profile-missing',
2619
- message: `发布命令必须显式指定真实 --profile: ${command}`,
2620
- });
2621
- }
2622
- if (
2623
- changeId &&
2624
- !new RegExp(`\\s--change(?:=|\\s+)${escapeRegExp(changeId)}(?:\\s|$)`).test(
2625
- command
2626
- )
2627
- ) {
2628
- errors.push({
2629
- name: 'sdd-release-change-missing',
2630
- message: `发布命令必须绑定 --change ${changeId}: ${command}`,
2631
- });
2632
- }
2633
- if (/^openxiangda\s+workspace\s+publish\b/.test(command) && /\s--form(?:=|\s+)/.test(command)) {
2634
- errors.push({
2635
- name: 'sdd-release-direct-form-schema-forbidden',
2636
- message: `React SPA Form 必须走 staged form-setting bundle,禁止 workspace publish --form: ${command}`,
2637
- });
2638
- }
2639
- if (
2640
- commandTypes.some(type =>
2641
- ['form-setting', 'form-permission-group'].includes(type)
2642
- ) &&
2643
- /\s--activate(?:\s|$)/.test(command)
2644
- ) {
2645
- errors.push({
2646
- name: 'sdd-release-direct-form-activation-forbidden',
2647
- message: `Form bundle 必须保持 staged,禁止在 Root App finalize 前 --activate: ${command}`,
2648
- });
2649
- }
2650
- if (
2651
- /^openxiangda\s+resource\s+publish\b/.test(command) &&
2652
- !/\s--(?:only|code)(?:=|\s+)/.test(command)
2653
- ) {
2654
- errors.push({
2655
- name: 'sdd-release-resource-scope-missing',
2656
- message: `resource publish 必须使用 --only/--code 精确范围: ${command}`,
2657
- });
2658
- }
2659
- if (
2660
- /^openxiangda\s+runtime\s+deploy\b/.test(command) &&
2661
- !/\s--no-activate(?:\s|$)/.test(command)
2662
- ) {
2663
- errors.push({
2664
- name: 'sdd-release-runtime-direct-activation-forbidden',
2665
- message: `Runtime 必须先 --no-activate 暂存,再由 App Release 原子激活: ${command}`,
2666
- });
2667
- }
2668
- const expectedBackendTypes = [
2669
- ...((targets.functions || []).length > 0 ? ['function'] : []),
2670
- ...((targets.automations || []).length > 0 ? ['automation'] : []),
2671
- ].sort();
2672
- if (
2673
- commandTypes.some(type => ['function', 'automation'].includes(type)) &&
2674
- expectedBackendTypes.length > 0
2675
- ) {
2676
- const qualified = expectedBackendTypes.length > 1;
2677
- const expectedSelectors = [
2678
- ...(targets.functions || []).map(code =>
2679
- qualified ? `function:${code}` : String(code)
2680
- ),
2681
- ...(targets.automations || []).map(code =>
2682
- qualified ? `automation:${code}` : String(code)
2683
- ),
2684
- ].sort();
2685
- if (!/\s--stage-only(?:\s|$)/.test(command)) {
2686
- errors.push({
2687
- name: 'sdd-release-backend-direct-activation-forbidden',
2688
- message: `Function/Automation 必须使用 --stage-only: ${command}`,
2689
- });
2690
- }
2691
- if (
2692
- !sameStringSet(commandTypes, expectedBackendTypes) ||
2693
- !sameStringSet(normalizedCommandSelectors(command), expectedSelectors)
2694
- ) {
2695
- errors.push({
2696
- name: 'sdd-release-backend-scope-inexact',
2697
- message: `Function/Automation 命令必须精确覆盖审核 selector,不能缺失或夹带资源: ${command}`,
2698
- });
2699
- }
2700
- }
2701
- if (
2702
- commandTypes.some(type =>
2703
- ['form-setting', 'form-permission-group'].includes(type)
2704
- ) &&
2705
- (!sameStringSet(commandTypes, expectedFormTypes) ||
2706
- !sameStringSet(
2707
- normalizedCommandSelectors(command),
2708
- expectedFormSelectors
2709
- ))
2710
- ) {
2711
- errors.push({
2712
- name: 'sdd-release-form-scope-inexact',
2713
- message: `Form bundle 命令必须以一条命令精确覆盖审核的 Form 与权限组 selector,不能缺失或夹带资源: ${command}`,
2714
- });
2715
- }
2716
- }
2717
-
2718
- if (!requireCompletePlan) return errors;
2719
-
2720
- const backendCodes = [
2721
- ...(targets.functions || []),
2722
- ...(targets.automations || []),
2723
- ];
2724
- if (backendCodes.length > 0) {
2725
- const backendStage = normalized.find(
2726
- command =>
2727
- /^openxiangda\s+resource\s+publish\b/.test(command) &&
2728
- /\s--stage-only(?:\s|$)/.test(command) &&
2729
- sameStringSet(normalizedResourcePublishTypes(command), [
2730
- ...((targets.functions || []).length > 0 ? ['function'] : []),
2731
- ...((targets.automations || []).length > 0 ? ['automation'] : []),
2732
- ]) &&
2733
- sameStringSet(normalizedCommandSelectors(command), [
2734
- ...(targets.functions || []).map(code =>
2735
- (targets.automations || []).length > 0
2736
- ? `function:${code}`
2737
- : String(code)
2738
- ),
2739
- ...(targets.automations || []).map(code =>
2740
- (targets.functions || []).length > 0
2741
- ? `automation:${code}`
2742
- : String(code)
2743
- ),
2744
- ])
2745
- );
2746
- if (!backendStage) {
2747
- errors.push({
2748
- name: 'sdd-release-backend-stage-missing',
2749
- message: 'Function/Automation 必须由一条精确 --only 的 --stage-only Backend Release 命令覆盖。',
2750
- });
2751
- }
2752
- }
2753
- if (expectedFormTypes.length > 0) {
2754
- const formStage = normalized.find(
2755
- command =>
2756
- /^openxiangda\s+resource\s+publish\b/.test(command) &&
2757
- !/\s--activate(?:\s|$)/.test(command) &&
2758
- sameStringSet(
2759
- normalizedResourcePublishTypes(command),
2760
- expectedFormTypes
2761
- ) &&
2762
- sameStringSet(normalizedCommandSelectors(command), expectedFormSelectors)
2763
- );
2764
- if (!formStage) {
2765
- errors.push({
2766
- name: 'sdd-release-form-stage-missing',
2767
- message: 'Form 与权限组必须由一条精确 bundle 命令暂存为 FormRelease。',
2768
- });
2769
- }
2770
- }
2771
- if (targets.runtime) {
2772
- const runtimeStage = normalized.find(
2773
- command =>
2774
- /^openxiangda\s+runtime\s+deploy\b/.test(command) &&
2775
- /\s--no-activate(?:\s|$)/.test(command)
2776
- );
2777
- if (!runtimeStage) {
2778
- errors.push({
2779
- name: 'sdd-release-runtime-stage-missing',
2780
- message: 'Runtime 目标缺少 runtime deploy --no-activate 暂存命令。',
2781
- });
2782
- }
2783
- }
2784
- if (
2785
- backendCodes.length > 0 ||
2786
- expectedFormTypes.length > 0 ||
2787
- targets.runtime
2788
- ) {
2789
- const finalize = normalized.find(
2790
- command =>
2791
- /^openxiangda\s+release\s+app-finalize\b/.test(command) &&
2792
- /\s--staged-resources-json(?:=|\s+)\S+/.test(command)
2793
- );
2794
- if (!finalize) {
2795
- errors.push({
2796
- name: 'sdd-release-app-finalize-missing',
2797
- message: '暂存的 Form/Backend/Runtime 必须由 app-finalize 一次原子激活。',
2798
- });
2799
- }
2800
- }
2801
- return errors;
2802
- }
2803
-
2804
- function escapeRegExp(value) {
2805
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2806
- }
2807
-
2808
- function verifySddChange(options = {}) {
2809
- const loaded = loadSddChange(options);
2810
- const scopeModel = loadedSddScopeModel(loaded);
2811
- const stagePolicy = getSddVerifyStagePolicy(options.stage || options.phase);
2812
- const verificationStage = stagePolicy.stage;
2813
- const targets = options.targets || options.releasePlan?.targets || {};
2814
- const highRiskFiles = classifyHighRiskFiles(options.files || options.releasePlan?.changedFiles || []);
2815
- const legacyMode = (loaded.meta.schemaVersion || LEGACY_SDD_SCHEMA_VERSION) !== SDD_SCHEMA_VERSION;
2816
- const errors = [];
2817
- const warnings = [];
2818
- if (loaded.meta.status !== 'approved' || !loaded.meta.approvedAt) {
2819
- errors.push({ name: 'sdd-change-not-approved', message: `change ${loaded.changeId} 未 approve` });
2820
- }
2821
- const targetCoverage = affectedCoversTargets(
2822
- scopeModel?.deployTargets || loaded.meta.affected,
2823
- targets
2824
- );
2825
- if (!targetCoverage.covered) {
2826
- errors.push({
2827
- name: 'sdd-change-targets-uncovered',
2828
- message: `affected 未覆盖发布目标: ${targetCoverage.missing.join(', ')}`,
2829
- });
2830
- }
2831
- const fileCoverage = affectedCoversHighRiskFiles(
2832
- scopeModel?.changedResources || loaded.meta.affected,
2833
- highRiskFiles
2834
- );
2835
- if (!fileCoverage.covered) {
2836
- errors.push({
2837
- name: 'sdd-change-files-uncovered',
2838
- message: `affected 未覆盖高风险文件: ${fileCoverage.missing.join(', ')}`,
2839
- });
2840
- }
2841
- const strictDocumentation = Boolean(loaded.governance.strictDocumentation);
2842
- const coverageValidation = validateCoverageManifest(
2843
- loaded.coverage,
2844
- loaded.meta,
2845
- targets,
2846
- highRiskFiles,
2847
- legacyMode,
2848
- { strictDocumentation, scopeModel }
2849
- );
2850
- errors.push(...coverageValidation.errors);
2851
- warnings.push(...coverageValidation.warnings);
2852
- const deltaValidation = validateDeltaSpecs(loaded.changeDir);
2853
- if (strictDocumentation) errors.push(...deltaValidation.errors);
2854
- else warnings.push(...deltaValidation.errors);
2855
- warnings.push(...deltaValidation.warnings);
2856
- const uncheckedTasks = readUncheckedTasks(loaded.changeDir, stagePolicy);
2857
- if (uncheckedTasks.length > 0) {
2858
- const issue = {
2859
- name: 'sdd-unfinished-tasks',
2860
- message: `${uncheckedTasks.length} 个 tasks.md 任务未完成;streamlined 模式不阻断主线发布`,
2861
- details: uncheckedTasks,
2862
- };
2863
- if (strictDocumentation) errors.push(issue);
2864
- else warnings.push(issue);
2865
- }
2866
- const evidenceFile = path.join(loaded.changeDir, 'evidence.md');
2867
- const uncheckedEvidence = readUncheckedChecklistForStages(evidenceFile, {
2868
- kind: 'evidence',
2869
- requiredStages: stagePolicy.evidenceStages,
2870
- missingMessage: 'evidence.md 缺失',
2871
- });
2872
- if (uncheckedEvidence.length > 0) {
2873
- const issue = {
2874
- name: 'sdd-evidence-incomplete',
2875
- message: `${uncheckedEvidence.length} 个 evidence.md 证据项未完成;streamlined 模式不阻断主线发布`,
2876
- details: uncheckedEvidence,
2877
- };
2878
- if (strictDocumentation) errors.push(issue);
2879
- else warnings.push(issue);
2880
- }
2881
- const actualExecution = options.releasePlan?.actualCommands === true;
2882
- const release =
2883
- options.releasePlan &&
2884
- loaded.release &&
2885
- !loaded.release.__invalid &&
2886
- !actualExecution
2887
- ? buildUpdatedReleaseManifest(
2888
- loaded,
2889
- options.releasePlan,
2890
- verificationStage
2891
- )
2892
- : loaded.release;
2893
- if (stagePolicy.requireRelease) {
2894
- const releaseValidation = validateReleaseManifest(
2895
- release,
2896
- options.releasePlan,
2897
- legacyMode,
2898
- { actualExecution }
2899
- );
2900
- errors.push(...releaseValidation.errors);
2901
- warnings.push(...releaseValidation.warnings);
2902
- const reviewedPlannedCommands = loaded.meta?.release?.plannedCommands;
2903
- if (
2904
- strictDocumentation &&
2905
- !legacyMode &&
2906
- Array.isArray(reviewedPlannedCommands) &&
2907
- reviewedPlannedCommands.length > 0
2908
- ) {
2909
- errors.push(
2910
- ...validateAtomicReleaseCommands(
2911
- reviewedPlannedCommands,
2912
- targets,
2913
- loaded.changeId
2914
- )
2915
- );
2916
- }
2917
- }
2918
- if (
2919
- !legacyMode &&
2920
- options.releasePlan?.actualCommands === true &&
2921
- Array.isArray(options.releasePlan.commands) &&
2922
- options.releasePlan.commands.length > 0
2923
- ) {
2924
- errors.push(
2925
- ...validateAtomicReleaseCommands(
2926
- options.releasePlan.commands,
2927
- targets,
2928
- loaded.changeId,
2929
- { requireCompletePlan: false }
2930
- )
2931
- );
2932
- }
2933
- const passed = errors.length === 0;
2934
- const verifiedAt = nowIso();
2935
- const verifiedMeta = {
2936
- ...loaded.meta,
2937
- release: {
2938
- ...(loaded.meta.release || {}),
2939
- plannedCommands:
2940
- release?.commands || options.releasePlan?.commands || [],
2941
- lastVerifiedAt: verifiedAt,
2942
- },
2943
- verification: {
2944
- ...(loaded.meta.verification || {}),
2945
- [verificationStage]: {
2946
- passed,
2947
- verifiedAt,
2948
- },
2949
- },
2950
- updatedAt: verifiedAt,
2951
- };
2952
- let metadataWritten = false;
2953
- let attestationFile = null;
2954
- if (options.writeMetadata && passed) {
2955
- attestationFile = writeReleaseAttestation(
2956
- options.cwd || process.cwd(),
2957
- loaded.changeId,
2958
- `verification-${verificationStage}.json`,
2959
- {
2960
- schemaVersion: SDD_SCHEMA_VERSION,
2961
- changeId: loaded.changeId,
2962
- stage: verificationStage,
2963
- passed,
2964
- verifiedAt,
2965
- planHash: releasePlanHash({
2966
- targets,
2967
- commands: release?.commands || [],
2968
- changedFiles:
2969
- options.releasePlan?.changedFiles ||
2970
- loaded.release?.changedFiles ||
2971
- [],
2972
- }),
2973
- }
2974
- );
2975
- metadataWritten = true;
2976
- }
2977
- return {
2978
- schemaVersion: loaded.meta.schemaVersion || loaded.governance.schemaVersion || LEGACY_SDD_SCHEMA_VERSION,
2979
- change: verifiedMeta,
2980
- coverage: loaded.coverage,
2981
- release,
2982
- deltaSpecs: deltaValidation.files,
2983
- errors,
2984
- warnings,
2985
- blockingErrors: errors,
2986
- passed,
2987
- metadataWritten,
2988
- trackedMetadataWritten: false,
2989
- attestationFile,
2990
- targets,
2991
- scopeModel,
2992
- highRiskFiles,
2993
- stage: verificationStage,
2994
- stageRequirements: {
2995
- tasks: Array.from(stagePolicy.taskStages),
2996
- evidence: Array.from(stagePolicy.evidenceStages),
2997
- release: stagePolicy.requireRelease,
2998
- },
2999
- };
3000
- }
3001
-
3002
- function appendToEvidence(changeDir, lines) {
3003
- const file = path.join(changeDir, 'evidence.md');
3004
- ensureDir(path.dirname(file));
3005
- fs.appendFileSync(file, `\n${lines.join('\n')}\n`);
3006
- }
3007
-
3008
- function appendBypassLog(options = {}) {
3009
- const governance = getSddGovernanceConfig(options);
3010
- if (!governance.initialized) {
3011
- throw new Error('SDD bypass 需要已初始化 openspec;先运行 openxiangda sdd init');
3012
- }
3013
- const reason = String(options.reason || '').trim();
3014
- if (!reason) throw new Error('--sdd-bypass 需要同时提供 --reason "..."');
3015
- const file = path.join(governance.rootDir, BYPASS_LOG_FILE);
3016
- ensureDir(path.dirname(file));
3017
- const entry = [
3018
- `## ${nowIso()}`,
3019
- '',
3020
- `- command: ${options.command || '-'}`,
3021
- `- reason: ${reason}`,
3022
- `- targets: ${JSON.stringify(options.targets || {})}`,
3023
- `- followUp: 补齐对应 change 的 evidence.md,并在发布后 archive`,
3024
- `- user: ${os.userInfo().username || 'unknown'}`,
3025
- '',
3026
- ];
3027
- fs.appendFileSync(file, entry.join('\n'));
3028
-
3029
- const requestedChangeId = options.changeId ? normalizeChangeId(options.changeId) : null;
3030
- const approved = listChanges(governance.rootDir).find(
3031
- item =>
3032
- item.valid &&
3033
- item.metadata.status === 'approved' &&
3034
- item.metadata.approvedAt &&
3035
- (!requestedChangeId || item.id === requestedChangeId)
3036
- );
3037
- if (approved) {
3038
- appendToEvidence(approved.dir, [
3039
- '## SDD Bypass',
3040
- '',
3041
- `- ${nowIso()} ${options.command || '-'} bypassed SDD gate: ${reason}`,
3042
- ]);
3043
- }
3044
-
3045
- return {
3046
- file: path.relative(options.cwd || process.cwd(), file).replace(/\\/g, '/'),
3047
- reason,
3048
- evidenceChange: approved?.id || null,
3049
- };
3050
- }
3051
-
3052
- function buildSddStatus(options = {}) {
3053
- const governance = getSddGovernanceConfig(options);
3054
- const specsDir = path.join(governance.rootDir, 'specs');
3055
- const changes = governance.initialized ? listChanges(governance.rootDir) : [];
3056
- const archived = governance.initialized ? listChanges(governance.rootDir, { includeArchive: true }).filter(item => item.dir.includes(`${path.sep}archive${path.sep}`)) : [];
3057
- const releasePlan = options.releasePlan || {};
3058
- const highRiskFiles = classifyHighRiskFiles(releasePlan.changedFiles || []);
3059
- const activeChange = chooseActiveChange(changes, releasePlan.targets || {}, highRiskFiles, {
3060
- changeId: options.changeId,
3061
- });
3062
- const report = buildSddGovernanceReport({
3063
- cwd: options.cwd,
3064
- configText: options.configText,
3065
- targets: releasePlan.targets || {},
3066
- files: releasePlan.changedFiles || [],
3067
- commands: releasePlan.commands || [],
3068
- changeId: options.changeId,
3069
- stage: options.stage || options.verifyStage,
3070
- });
3071
- return {
3072
- cwd: options.cwd || process.cwd(),
3073
- schemaVersion: governance.schemaVersion,
3074
- governance,
3075
- specs: listMarkdownFiles(specsDir).map(file => path.relative(governance.rootDir, file).replace(/\\/g, '/')),
3076
- specSummaries: governance.initialized
3077
- ? listMarkdownFiles(specsDir).map(file => summarizeSpecFile(governance.rootDir, file))
3078
- : [],
3079
- activeChange: activeChange ? summarizeChange(options.cwd || process.cwd(), activeChange) : null,
3080
- releaseTargets: releasePlan.targets || {},
3081
- coverage: summarizeCoverage(activeChange?.coverage),
3082
- activeChanges: changes.map(item => summarizeChange(options.cwd || process.cwd(), item)),
3083
- archivedChanges: archived.slice(-10).map(item => ({
3084
- id: item.id,
3085
- dir: path.relative(options.cwd || process.cwd(), item.dir).replace(/\\/g, '/'),
3086
- })),
3087
- bypassLog: governance.initialized && fs.existsSync(path.join(governance.rootDir, BYPASS_LOG_FILE))
3088
- ? path.join(governance.path, BYPASS_LOG_FILE).replace(/\\/g, '/')
3089
- : null,
3090
- nextActions: report.nextActions,
3091
- blockingErrors: report.blockingErrors,
3092
- };
3093
- }
3094
-
3095
- function findRequirementBlock(content, title) {
3096
- const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3097
- const regex = new RegExp(`^###\\s+Requirement:\\s*${escaped}\\s*$`, 'mi');
3098
- const match = content.match(regex);
3099
- if (!match || match.index === undefined) return null;
3100
- const start = match.index;
3101
- const next = content.slice(start + match[0].length).search(/^###\s+Requirement:/mi);
3102
- const end = next >= 0 ? start + match[0].length + next : content.length;
3103
- return { start, end };
3104
- }
3105
-
3106
- function mergeDeltaIntoSpec(current, delta, targetRel) {
3107
- let content = current.trimEnd();
3108
- const errors = [];
3109
- const warnings = [];
3110
- if (!content) {
3111
- const domain = targetRel.replace(/\/spec\.md$/, '') || 'app';
3112
- content = `# ${domain} Specification\n\n## Purpose\n\nCurrent behavior for ${domain}.\n\n## Requirements\n`;
3113
- }
3114
- if (!/##\s+Requirements/i.test(content)) {
3115
- content += '\n\n## Requirements\n';
3116
- }
3117
- for (const requirement of delta.added) {
3118
- const existing = findRequirementBlock(content, requirement.title);
3119
- if (existing) {
3120
- warnings.push({ name: 'sdd-added-existing-requirement', message: `${targetRel} 已存在 requirement "${requirement.title}",跳过 ADDED` });
3121
- continue;
3122
- }
3123
- content = `${content.trimEnd()}\n\n${requirement.content}\n`;
3124
- }
3125
- for (const requirement of delta.modified) {
3126
- const existing = findRequirementBlock(content, requirement.title);
3127
- if (!existing) {
3128
- errors.push({ name: 'sdd-modified-requirement-missing', message: `${targetRel} 不存在要修改的 requirement "${requirement.title}"` });
3129
- continue;
3130
- }
3131
- content = `${content.slice(0, existing.start).trimEnd()}\n\n${requirement.content}\n\n${content.slice(existing.end).trimStart()}`;
3132
- }
3133
- for (const requirement of delta.removed) {
3134
- const existing = findRequirementBlock(content, requirement.title);
3135
- if (!existing) {
3136
- warnings.push({ name: 'sdd-removed-requirement-missing', message: `${targetRel} 不存在要移除的 requirement "${requirement.title}"` });
3137
- continue;
3138
- }
3139
- content = `${content.slice(0, existing.start).trimEnd()}\n\n${content.slice(existing.end).trimStart()}`;
3140
- }
3141
- return { content: `${content.trimEnd()}\n`, errors, warnings };
3142
- }
3143
-
3144
- function syncSddChange(options = {}) {
3145
- const loaded = loadSddChange(options);
3146
- const specRoot = path.join(loaded.changeDir, 'specs');
3147
- const specFiles = listMarkdownFiles(specRoot);
3148
- const synced = [];
3149
- const errors = [];
3150
- const warnings = [];
3151
- const plannedWrites = [];
3152
- for (const deltaFile of specFiles) {
3153
- const rel = path.relative(specRoot, deltaFile).replace(/\\/g, '/');
3154
- const target = path.join(loaded.governance.rootDir, 'specs', rel);
3155
- const deltaContent = fs.readFileSync(deltaFile, 'utf8');
3156
- const delta = parseDeltaSpec(deltaContent);
3157
- const current = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : '';
3158
- const merged = mergeDeltaIntoSpec(current, delta, rel);
3159
- errors.push(...merged.errors);
3160
- warnings.push(...merged.warnings);
3161
- if (merged.errors.length === 0) {
3162
- plannedWrites.push({ target, content: merged.content });
3163
- }
3164
- }
3165
- if (errors.length > 0 && !options.force) {
3166
- const first = errors[0];
3167
- throw new Error(`SDD sync 失败: ${first.message}`);
3168
- }
3169
- for (const item of plannedWrites) {
3170
- ensureDir(path.dirname(item.target));
3171
- fs.writeFileSync(item.target, item.content);
3172
- synced.push(path.relative(loaded.governance.rootDir, item.target).replace(/\\/g, '/'));
3173
- }
3174
- return {
3175
- schemaVersion: loaded.meta.schemaVersion || loaded.governance.schemaVersion,
3176
- change: loaded.meta,
3177
- synced,
3178
- warnings,
3179
- errors,
3180
- };
3181
- }
3182
-
3183
- function archiveSddChange(options = {}) {
3184
- const releaseLoaded = loadSddChange(options);
3185
- const sourceRevisionAttestation = readReleaseAttestation(
3186
- options.cwd || process.cwd(),
3187
- releaseLoaded.changeId,
3188
- 'source-revision.json'
3189
- );
3190
- const releaseSourceRevision =
3191
- sourceRevisionAttestation?.releaseSourceRevision ||
3192
- releaseLoaded.release?.releaseSourceRevision ||
3193
- null;
3194
- const coverageTargets = normalizeCoverageResources(releaseLoaded.coverage, {});
3195
- const releaseTargets = mergeAffectedScopes(
3196
- options.targets,
3197
- options.releasePlan?.targets,
3198
- releaseLoaded.release?.targets,
3199
- coverageTargets,
3200
- releaseLoaded.meta,
3201
- { files: options.files },
3202
- { files: options.releasePlan?.changedFiles },
3203
- { files: releaseLoaded.release?.changedFiles }
3204
- );
3205
- const riskAssessment =
3206
- releaseLoaded.meta?.riskMetadata?.assessment ||
3207
- releaseLoaded.meta?.riskAssessment ||
3208
- {};
3209
- const metadataDeclaresHighRisk =
3210
- ['l2', 'l3'].includes(String(releaseLoaded.meta?.riskTier || '').toLowerCase()) ||
3211
- ['high', 'critical'].includes(
3212
- String(releaseLoaded.meta?.riskLevel || '').toLowerCase()
3213
- ) ||
3214
- [
3215
- 'schema',
3216
- 'permission',
3217
- 'auth',
3218
- 'publicAccess',
3219
- 'dataMigration',
3220
- 'destructive',
3221
- 'crossResource',
3222
- ].some(key => riskAssessment[key] === true);
3223
- const requiresMainlineClosure =
3224
- hasHighRiskTargets(releaseTargets) ||
3225
- Boolean(releaseTargets.backend) ||
3226
- Boolean(releaseTargets.contracts) ||
3227
- classifyHighRiskFiles(releaseTargets.files).length > 0 ||
3228
- metadataDeclaresHighRisk ||
3229
- Boolean(releaseLoaded.release?.mainlinePolicy) ||
3230
- Boolean(releaseSourceRevision);
3231
- if (requiresMainlineClosure) {
3232
- if (
3233
- !releaseLoaded.release ||
3234
- releaseLoaded.release.__invalid ||
3235
- !releaseSourceRevision
3236
- ) {
3237
- const error = new Error(
3238
- 'SDD_RELEASE_SOURCE_LINEAGE_REQUIRED: 高风险 change 归档必须保留冻结发布提交的本地 attestation;--force 不能绕过主分支回合证据'
3239
- );
3240
- error.code = 'SDD_RELEASE_SOURCE_LINEAGE_REQUIRED';
3241
- throw error;
3242
- }
3243
- assertReleaseSourceIntegrated(releaseSourceRevision, {
3244
- cwd: options.cwd || process.cwd(),
3245
- });
3246
- }
3247
- const verify = verifySddChange({ ...options, stage: 'archive' });
3248
- if (!verify.passed && !options.force) {
3249
- const first = verify.errors[0];
3250
- throw new Error(`change 不能归档: ${first.message}`);
3251
- }
3252
- const loaded = loadSddChange(options);
3253
- const sync = syncSddChange(options);
3254
- const archiveName = `${new Date().toISOString().slice(0, 10)}-${loaded.changeId}`;
3255
- const archiveDir = path.join(loaded.governance.rootDir, 'changes', 'archive', archiveName);
3256
- ensureDir(path.dirname(archiveDir));
3257
- if (fs.existsSync(archiveDir)) throw new Error(`archive 已存在: ${archiveName}`);
3258
- const meta = {
3259
- ...loaded.meta,
3260
- status: 'archived',
3261
- archivedAt: nowIso(),
3262
- syncedSpecs: sync.synced,
3263
- updatedAt: nowIso(),
3264
- };
3265
- writeJsonFile(loaded.metaFile, meta);
3266
- fs.renameSync(loaded.changeDir, archiveDir);
3267
- return {
3268
- schemaVersion: meta.schemaVersion || loaded.governance.schemaVersion,
3269
- change: meta,
3270
- archiveDir: path.relative(options.cwd || process.cwd(), archiveDir).replace(/\\/g, '/'),
3271
- synced: sync.synced,
3272
- warnings: [...verify.warnings, ...sync.warnings],
3273
- };
3274
- }
3275
-
3276
- module.exports = {
3277
- DEFAULT_SDD_DIR,
3278
- QUICK_FUNCTION_SAFETY_FIELDS,
3279
- QUICK_SDD_ALLOWED_KINDS,
3280
- QUICK_SDD_RISK_TIER,
3281
- SDD_SCHEMA_VERSION,
3282
- SDD_SCOPE_MODEL_VERSION,
3283
- SDD_VERIFY_STAGES,
3284
- appendBypassLog,
3285
- approveQuickSddChange,
3286
- archiveSddChange,
3287
- buildSddGovernanceReport,
3288
- buildSddStatus,
3289
- createQuickSddChange,
3290
- createMainlineSddBundle,
3291
- getSddGovernanceConfig,
3292
- getSddChangeScope,
3293
- getSddVerifyStagePolicy,
3294
- initSddWorkspace,
3295
- normalizeAffected,
3296
- normalizeSddScopeModel,
3297
- normalizeSddVerifyStage,
3298
- proposeQuickSddChange,
3299
- proposeSddChange,
3300
- recordSddReleaseSourceRevision,
3301
- renderSddDocumentation,
3302
- approveSddChange,
3303
- syncSddChange,
3304
- validateQuickSddScope,
3305
- verifySddChange,
3306
- };