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.
- package/README.md +11 -666
- package/bin/run.js +11 -0
- package/dist/browser/AuthoritativeSelector.d.ts +37 -0
- package/dist/browser/AuthoritativeSelector.d.ts.map +1 -0
- package/dist/browser/AuthoritativeSelector.js +357 -0
- package/dist/browser/AuthoritativeSelector.js.map +1 -0
- package/dist/browser/FilePreviewPage.d.ts +2 -0
- package/dist/browser/FilePreviewPage.d.ts.map +1 -0
- package/dist/browser/FilePreviewPage.js +191 -0
- package/dist/browser/FilePreviewPage.js.map +1 -0
- package/dist/browser/Shell.d.ts +6 -0
- package/dist/browser/Shell.d.ts.map +1 -0
- package/dist/browser/Shell.js +198 -0
- package/dist/browser/Shell.js.map +1 -0
- package/dist/browser/appearance.d.ts +12 -0
- package/dist/browser/appearance.d.ts.map +1 -0
- package/dist/browser/appearance.js +100 -0
- package/dist/browser/appearance.js.map +1 -0
- package/dist/browser/application.d.ts +8 -0
- package/dist/browser/application.d.ts.map +1 -0
- package/dist/browser/application.js +42 -0
- package/dist/browser/application.js.map +1 -0
- package/dist/browser/components/platform-fields/AddressField.d.ts +12 -0
- package/dist/browser/components/platform-fields/AddressField.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/AddressField.js +179 -0
- package/dist/browser/components/platform-fields/AddressField.js.map +1 -0
- package/dist/browser/components/platform-fields/AttachmentFileList.d.ts +9 -0
- package/dist/browser/components/platform-fields/AttachmentFileList.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/AttachmentFileList.js +164 -0
- package/dist/browser/components/platform-fields/AttachmentFileList.js.map +1 -0
- package/dist/browser/components/platform-fields/CascadeField.d.ts +13 -0
- package/dist/browser/components/platform-fields/CascadeField.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/CascadeField.js +14 -0
- package/dist/browser/components/platform-fields/CascadeField.js.map +1 -0
- package/dist/browser/components/platform-fields/DateTimeField.d.ts +16 -0
- package/dist/browser/components/platform-fields/DateTimeField.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/DateTimeField.js +51 -0
- package/dist/browser/components/platform-fields/DateTimeField.js.map +1 -0
- package/dist/browser/components/platform-fields/JsonField.d.ts +11 -0
- package/dist/browser/components/platform-fields/JsonField.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/JsonField.js +40 -0
- package/dist/browser/components/platform-fields/JsonField.js.map +1 -0
- package/dist/browser/components/platform-fields/LocationField.d.ts +11 -0
- package/dist/browser/components/platform-fields/LocationField.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/LocationField.js +73 -0
- package/dist/browser/components/platform-fields/LocationField.js.map +1 -0
- package/dist/browser/components/platform-fields/PlatformDirectoryPicker.d.ts +12 -0
- package/dist/browser/components/platform-fields/PlatformDirectoryPicker.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/PlatformDirectoryPicker.js +269 -0
- package/dist/browser/components/platform-fields/PlatformDirectoryPicker.js.map +1 -0
- package/dist/browser/components/platform-fields/ResourceReferenceField.d.ts +12 -0
- package/dist/browser/components/platform-fields/ResourceReferenceField.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/ResourceReferenceField.js +22 -0
- package/dist/browser/components/platform-fields/ResourceReferenceField.js.map +1 -0
- package/dist/browser/components/platform-fields/RichTextField.d.ts +14 -0
- package/dist/browser/components/platform-fields/RichTextField.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/RichTextField.js +214 -0
- package/dist/browser/components/platform-fields/RichTextField.js.map +1 -0
- package/dist/browser/components/platform-fields/SignatureField.d.ts +15 -0
- package/dist/browser/components/platform-fields/SignatureField.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/SignatureField.js +179 -0
- package/dist/browser/components/platform-fields/SignatureField.js.map +1 -0
- package/dist/browser/components/platform-fields/SubtableField.d.ts +18 -0
- package/dist/browser/components/platform-fields/SubtableField.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/SubtableField.js +236 -0
- package/dist/browser/components/platform-fields/SubtableField.js.map +1 -0
- package/dist/browser/components/platform-fields/address-value.d.ts +13 -0
- package/dist/browser/components/platform-fields/address-value.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/address-value.js +70 -0
- package/dist/browser/components/platform-fields/address-value.js.map +1 -0
- package/dist/browser/components/platform-fields/cascade-value.d.ts +6 -0
- package/dist/browser/components/platform-fields/cascade-value.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/cascade-value.js +47 -0
- package/dist/browser/components/platform-fields/cascade-value.js.map +1 -0
- package/dist/browser/components/platform-fields/directory-value.d.ts +7 -0
- package/dist/browser/components/platform-fields/directory-value.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/directory-value.js +35 -0
- package/dist/browser/components/platform-fields/directory-value.js.map +1 -0
- package/dist/browser/components/platform-fields/field-form-codec.d.ts +10 -0
- package/dist/browser/components/platform-fields/field-form-codec.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/field-form-codec.js +82 -0
- package/dist/browser/components/platform-fields/field-form-codec.js.map +1 -0
- package/dist/browser/components/platform-fields/location-value.d.ts +20 -0
- package/dist/browser/components/platform-fields/location-value.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/location-value.js +57 -0
- package/dist/browser/components/platform-fields/location-value.js.map +1 -0
- package/dist/browser/components/platform-fields/resource-query.d.ts +14 -0
- package/dist/browser/components/platform-fields/resource-query.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/resource-query.js +102 -0
- package/dist/browser/components/platform-fields/resource-query.js.map +1 -0
- package/dist/browser/components/platform-fields/rich-text-value.d.ts +12 -0
- package/dist/browser/components/platform-fields/rich-text-value.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/rich-text-value.js +83 -0
- package/dist/browser/components/platform-fields/rich-text-value.js.map +1 -0
- package/dist/browser/components/platform-fields/subtable-value.d.ts +31 -0
- package/dist/browser/components/platform-fields/subtable-value.d.ts.map +1 -0
- package/dist/browser/components/platform-fields/subtable-value.js +120 -0
- package/dist/browser/components/platform-fields/subtable-value.js.map +1 -0
- package/dist/browser/components/resource/GeneratedResourceCrud.d.ts +8 -0
- package/dist/browser/components/resource/GeneratedResourceCrud.d.ts.map +1 -0
- package/dist/browser/components/resource/GeneratedResourceCrud.js +580 -0
- package/dist/browser/components/resource/GeneratedResourceCrud.js.map +1 -0
- package/dist/browser/components/resource/ResourceBatchActions.d.ts +25 -0
- package/dist/browser/components/resource/ResourceBatchActions.d.ts.map +1 -0
- package/dist/browser/components/resource/ResourceBatchActions.js +168 -0
- package/dist/browser/components/resource/ResourceBatchActions.js.map +1 -0
- package/dist/browser/components/resource/StandardResourcePages.d.ts +57 -0
- package/dist/browser/components/resource/StandardResourcePages.d.ts.map +1 -0
- package/dist/browser/components/resource/StandardResourcePages.js +89 -0
- package/dist/browser/components/resource/StandardResourcePages.js.map +1 -0
- package/dist/browser/components/resource/SurfaceFields.d.ts +88 -0
- package/dist/browser/components/resource/SurfaceFields.d.ts.map +1 -0
- package/dist/browser/components/resource/SurfaceFields.js +497 -0
- package/dist/browser/components/resource/SurfaceFields.js.map +1 -0
- package/dist/browser/components/resource/generated-resource-definition.d.ts +19 -0
- package/dist/browser/components/resource/generated-resource-definition.d.ts.map +1 -0
- package/dist/browser/components/resource/generated-resource-definition.js +2 -0
- package/dist/browser/components/resource/generated-resource-definition.js.map +1 -0
- package/dist/browser/components/resource/resource-import.d.ts +15 -0
- package/dist/browser/components/resource/resource-import.d.ts.map +1 -0
- package/dist/browser/components/resource/resource-import.js +197 -0
- package/dist/browser/components/resource/resource-import.js.map +1 -0
- package/dist/browser/data-provider.d.ts +4 -0
- package/dist/browser/data-provider.d.ts.map +1 -0
- package/dist/browser/data-provider.js +45 -0
- package/dist/browser/data-provider.js.map +1 -0
- package/dist/browser/platform-client.d.ts +81 -0
- package/dist/browser/platform-client.d.ts.map +1 -0
- package/dist/browser/platform-client.js +422 -0
- package/dist/browser/platform-client.js.map +1 -0
- package/dist/browser/resource-definitions.d.ts +8 -0
- package/dist/browser/resource-definitions.d.ts.map +1 -0
- package/dist/browser/resource-definitions.js +14 -0
- package/dist/browser/resource-definitions.js.map +1 -0
- package/dist/browser/runtime-meta.d.ts +22 -0
- package/dist/browser/runtime-meta.d.ts.map +1 -0
- package/dist/browser/runtime-meta.js +79 -0
- package/dist/browser/runtime-meta.js.map +1 -0
- package/dist/browser/runtime.d.ts +13 -0
- package/dist/browser/runtime.d.ts.map +1 -0
- package/dist/browser/runtime.js +47 -0
- package/dist/browser/runtime.js.map +1 -0
- package/dist/browser/styles.css +1941 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -0
- package/dist/core.d.ts +6 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +4 -0
- package/dist/core.js.map +1 -0
- package/dist/field-kit.d.ts +22 -0
- package/dist/field-kit.d.ts.map +1 -0
- package/dist/field-kit.js +22 -0
- package/dist/field-kit.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/nest.d.ts +10 -0
- package/dist/nest.d.ts.map +1 -0
- package/dist/nest.js +9 -0
- package/dist/nest.js.map +1 -0
- package/dist/react.d.ts +12 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +11 -0
- package/dist/react.js.map +1 -0
- package/dist/testing.d.ts +4 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +4 -0
- package/dist/testing.js.map +1 -0
- package/package.json +95 -200
- package/skills/manifest.json +10 -0
- package/skills/openxiangda-v2/SKILL.md +55 -0
- package/skills/openxiangda-v2/references/architecture.md +7 -0
- package/skills/openxiangda-v2/references/backend.md +116 -0
- package/skills/openxiangda-v2/references/data-authz.md +93 -0
- package/skills/openxiangda-v2/references/delivery.md +11 -0
- package/skills/openxiangda-v2/references/discovery.md +15 -0
- package/skills/openxiangda-v2/references/frontend.md +10 -0
- package/skills/openxiangda-v2/references/testing.md +16 -0
- package/skills/openxiangda-v2/references/workflow-notification.md +69 -0
- package/skills/openxiangda-v2/references/workspace.md +21 -0
- package/bin/openxiangda.js +0 -26
- package/lib/app-function-invoke.js +0 -28
- package/lib/application-environments.js +0 -1134
- package/lib/change-baseline.js +0 -1359
- package/lib/cli.js +0 -31084
- package/lib/config.js +0 -450
- package/lib/content-cache.js +0 -97
- package/lib/delivery-v2-cli.js +0 -329
- package/lib/delivery-v2-executor.js +0 -1703
- package/lib/delivery-v2-package.js +0 -1176
- package/lib/dependency-capsule.js +0 -246
- package/lib/design-gates.js +0 -668
- package/lib/design-review.js +0 -220
- package/lib/developer-center.js +0 -160
- package/lib/form-field-contract.js +0 -79
- package/lib/form-schema-loader.js +0 -181
- package/lib/git-remote-identity.js +0 -89
- package/lib/http.js +0 -196
- package/lib/integration-bundle.js +0 -225
- package/lib/js-code-build.js +0 -175
- package/lib/open-api.js +0 -256
- package/lib/policy.js +0 -137
- package/lib/publish-context.js +0 -98
- package/lib/publish-lease-heartbeat.js +0 -182
- package/lib/publish-lease.js +0 -221
- package/lib/release-error-classification.js +0 -41
- package/lib/release-explain.js +0 -75
- package/lib/release-mainline.js +0 -654
- package/lib/release-plan.js +0 -550
- package/lib/release-telemetry.js +0 -182
- package/lib/resource-binding-contract.js +0 -210
- package/lib/role-resolution.js +0 -57
- package/lib/runtime-lineage.js +0 -326
- package/lib/sdd.js +0 -3306
- package/lib/skills.js +0 -453
- package/lib/source-dependencies.js +0 -921
- package/lib/task-status.js +0 -330
- package/lib/utils.js +0 -178
- package/lib/workspace-bootstrap.js +0 -268
- package/lib/workspace-init.js +0 -202
- package/lib/worktree-cleanup.js +0 -631
- package/lib/worktree-owner.js +0 -354
- package/openxiangda-skills/SKILL.md +0 -226
- package/openxiangda-skills/references/architecture-design.md +0 -409
- package/openxiangda-skills/references/architecture-patterns.md +0 -264
- package/openxiangda-skills/references/automation-v3.md +0 -349
- package/openxiangda-skills/references/best-practices.md +0 -272
- package/openxiangda-skills/references/component-guide.md +0 -287
- package/openxiangda-skills/references/connector-resources.md +0 -80
- package/openxiangda-skills/references/data-views.md +0 -360
- package/openxiangda-skills/references/forms/component-registry.md +0 -90
- package/openxiangda-skills/references/forms/form-schema.md +0 -146
- package/openxiangda-skills/references/forms/layout-and-rules.md +0 -24
- package/openxiangda-skills/references/notifications.md +0 -227
- package/openxiangda-skills/references/openxiangda-api.md +0 -971
- package/openxiangda-skills/references/pages/app-shell.md +0 -66
- package/openxiangda-skills/references/pages/page-sdk.md +0 -401
- package/openxiangda-skills/references/pages/publish-flow.md +0 -84
- package/openxiangda-skills/references/pages/workspace-structure.md +0 -41
- package/openxiangda-skills/references/permission-design-patterns.md +0 -313
- package/openxiangda-skills/references/permissions-settings.md +0 -468
- package/openxiangda-skills/references/platform-data-model.md +0 -340
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +0 -936
- package/openxiangda-skills/references/school-contact-relations.md +0 -224
- package/openxiangda-skills/references/style-system.md +0 -218
- package/openxiangda-skills/references/troubleshooting.md +0 -271
- package/openxiangda-skills/references/workflow-v3.md +0 -441
- package/openxiangda-skills/references/workspace-state.md +0 -115
- package/openxiangda-skills/skills/openxiangda-app/SKILL.md +0 -118
- package/openxiangda-skills/skills/openxiangda-architecture-design/SKILL.md +0 -137
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +0 -222
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +0 -184
- package/openxiangda-skills/skills/openxiangda-inspect/SKILL.md +0 -63
- package/openxiangda-skills/skills/openxiangda-open-api/SKILL.md +0 -57
- package/openxiangda-skills/skills/openxiangda-open-api/agents/openai.yaml +0 -4
- package/openxiangda-skills/skills/openxiangda-open-api/references/backend-authentication.md +0 -63
- package/openxiangda-skills/skills/openxiangda-open-api/references/dingtalk-api.openapi.json +0 -15653
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +0 -154
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +0 -209
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +0 -201
- package/packages/sdk/bin/lowcode-workspace.mjs +0 -4
- package/packages/sdk/dist/ProcessPreview-DSUIJi5V.d.mts +0 -1292
- package/packages/sdk/dist/ProcessPreview-DSUIJi5V.d.ts +0 -1292
- package/packages/sdk/dist/build/index.cjs +0 -1
- package/packages/sdk/dist/build/index.d.mts +0 -60
- package/packages/sdk/dist/build/index.d.ts +0 -60
- package/packages/sdk/dist/build/index.mjs +0 -1
- package/packages/sdk/dist/components/index.cjs +0 -148
- package/packages/sdk/dist/components/index.d.mts +0 -970
- package/packages/sdk/dist/components/index.d.ts +0 -970
- package/packages/sdk/dist/components/index.mjs +0 -148
- package/packages/sdk/dist/dataManagementApi-Ro9itwm8.d.ts +0 -142
- package/packages/sdk/dist/dataManagementApi-p_HOhQnA.d.mts +0 -142
- package/packages/sdk/dist/runtime/index.cjs +0 -148
- package/packages/sdk/dist/runtime/index.d.mts +0 -171
- package/packages/sdk/dist/runtime/index.d.ts +0 -171
- package/packages/sdk/dist/runtime/index.mjs +0 -148
- package/packages/sdk/dist/runtime/react.cjs +0 -148
- package/packages/sdk/dist/runtime/react.d.mts +0 -3202
- package/packages/sdk/dist/runtime/react.d.ts +0 -3202
- package/packages/sdk/dist/runtime/react.mjs +0 -148
- package/packages/sdk/dist/styles/antd-theme.cjs +0 -1
- package/packages/sdk/dist/styles/antd-theme.d.mts +0 -6
- package/packages/sdk/dist/styles/antd-theme.d.ts +0 -6
- package/packages/sdk/dist/styles/antd-theme.mjs +0 -1
- package/packages/sdk/dist/styles/tailwind-preset.cjs +0 -1
- package/packages/sdk/dist/styles/tailwind-preset.d.mts +0 -74
- package/packages/sdk/dist/styles/tailwind-preset.d.ts +0 -74
- package/packages/sdk/dist/styles/tailwind-preset.mjs +0 -1
- package/packages/sdk/dist/styles/tokens.css +0 -74
- package/packages/sdk/dist/workflow/index.cjs +0 -3
- package/packages/sdk/dist/workflow/index.d.mts +0 -291
- package/packages/sdk/dist/workflow/index.d.ts +0 -291
- package/packages/sdk/dist/workflow/index.mjs +0 -3
- package/packages/sdk/source/admin-list/AdminList.tsx +0 -1734
- package/packages/sdk/source/admin-list/README.md +0 -12
- package/packages/sdk/source/admin-list/dataSources.ts +0 -345
- package/packages/sdk/source/admin-list/index.ts +0 -42
- package/packages/sdk/source/admin-list/preferences.ts +0 -174
- package/packages/sdk/source/admin-list/types.ts +0 -273
- package/packages/sdk/src/build-source/README.md +0 -9
- package/packages/sdk/src/build-source/bin/lowcode-workspace.mjs +0 -7
- package/packages/sdk/src/build-source/package.json +0 -34
- package/packages/sdk/src/build-source/scripts/build-forms.mjs +0 -1129
- package/packages/sdk/src/build-source/scripts/build-pages.mjs +0 -897
- package/packages/sdk/src/build-source/scripts/build-workspace.mjs +0 -89
- package/packages/sdk/src/build-source/scripts/publish-all.mjs +0 -190
- package/packages/sdk/src/build-source/scripts/publish-oss.mjs +0 -144
- package/packages/sdk/src/build-source/scripts/register-bundle.mjs +0 -1
- package/packages/sdk/src/build-source/scripts/register.mjs +0 -362
- package/packages/sdk/src/build-source/scripts/sync-schema.mjs +0 -381
- package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +0 -1058
- package/packages/sdk/src/build-source/scripts/utils/form-runtime-assets.mjs +0 -57
- package/packages/sdk/src/build-source/scripts/utils/incremental.mjs +0 -348
- package/packages/sdk/src/build-source/scripts/utils/load-config.mjs +0 -331
- package/packages/sdk/src/build-source/scripts/utils/mime-types.mjs +0 -70
- package/packages/sdk/src/build-source/scripts/utils/namespace-css.mjs +0 -77
- package/packages/sdk/src/build-source/scripts/utils/oss-client.mjs +0 -128
- package/packages/sdk/src/build-source/scripts/utils/pages.mjs +0 -150
- package/packages/sdk/src/build-source/scripts/utils/progress.mjs +0 -57
- package/packages/sdk/src/build-source/scripts/utils/publish-oss-patterns.mjs +0 -20
- package/packages/sdk/src/build-source/scripts/utils/register-payload.mjs +0 -90
- package/packages/sdk/src/build-source/scripts/utils/runtime-css-check.mjs +0 -44
- package/packages/sdk/src/build-source/scripts/utils/schema-transform.mjs +0 -142
- package/packages/sdk/src/build-source/scripts/utils/static-assets.mjs +0 -165
- package/packages/sdk/src/build-source/scripts/utils/tailwind-config.mjs +0 -283
- package/packages/sdk/src/build-source/scripts/utils/tailwind-token-warnings.mjs +0 -146
- package/packages/sdk/src/build-source/scripts/utils/workspace-style-entry.mjs +0 -29
- package/packages/sdk/src/build-source/src/cli.mjs +0 -735
- package/policy/openxiangda-policy.json +0 -72
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-form.mdc +0 -24
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-page.mdc +0 -25
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +0 -32
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-workflow-automation.mdc +0 -26
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +0 -68
- package/templates/openxiangda-react-spa/.env.example +0 -4
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-form.md +0 -37
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-page.md +0 -29
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +0 -32
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-workflow-automation.md +0 -34
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +0 -66
- package/templates/openxiangda-react-spa/AGENTS.md +0 -192
- package/templates/openxiangda-react-spa/DELIVERY.md +0 -43
- package/templates/openxiangda-react-spa/app-workspace.config.ts +0 -50
- package/templates/openxiangda-react-spa/index.html +0 -12
- package/templates/openxiangda-react-spa/package.json +0 -53
- package/templates/openxiangda-react-spa/postcss.config.cjs +0 -6
- package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +0 -600
- package/templates/openxiangda-react-spa/scripts/deploy.mjs +0 -18
- package/templates/openxiangda-react-spa/scripts/guard-publish.mjs +0 -21
- package/templates/openxiangda-react-spa/src/app/navigation.ts +0 -73
- package/templates/openxiangda-react-spa/src/app/router.tsx +0 -146
- package/templates/openxiangda-react-spa/src/app/starter-content.ts +0 -144
- package/templates/openxiangda-react-spa/src/forms/.gitkeep +0 -1
- package/templates/openxiangda-react-spa/src/layouts/AdminShell.tsx +0 -570
- package/templates/openxiangda-react-spa/src/main.tsx +0 -21
- package/templates/openxiangda-react-spa/src/pages/admin/AdminDashboardPage.tsx +0 -170
- package/templates/openxiangda-react-spa/src/pages/admin/LoginLogPage.tsx +0 -425
- package/templates/openxiangda-react-spa/src/pages/defaults/DataRoutePage.tsx +0 -36
- package/templates/openxiangda-react-spa/src/pages/defaults/FilePreviewRoutePage.tsx +0 -65
- package/templates/openxiangda-react-spa/src/pages/defaults/FormRoutePage.tsx +0 -235
- package/templates/openxiangda-react-spa/src/pages/public/PublicRegisterPage.tsx +0 -15
- package/templates/openxiangda-react-spa/src/pages/states/NotFoundPage.tsx +0 -31
- package/templates/openxiangda-react-spa/src/resources/menus/menus.json +0 -22
- package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/app-admin.json +0 -8
- package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/public-visitor.json +0 -8
- package/templates/openxiangda-react-spa/src/resources/public-access/public-register.json +0 -14
- package/templates/openxiangda-react-spa/src/resources/roles/roles.json +0 -15
- package/templates/openxiangda-react-spa/src/resources/routes/public-register.json +0 -8
- package/templates/openxiangda-react-spa/src/runtime/default-page-overrides.tsx +0 -35
- package/templates/openxiangda-react-spa/src/runtime/default-routes.ts +0 -16
- package/templates/openxiangda-react-spa/src/shared/form-schema.ts +0 -135
- package/templates/openxiangda-react-spa/src/shared/mac-admin.tsx +0 -651
- package/templates/openxiangda-react-spa/src/shared/ui.tsx +0 -22
- package/templates/openxiangda-react-spa/src/styles/index.css +0 -105
- package/templates/openxiangda-react-spa/tailwind.config.cjs +0 -37
- package/templates/openxiangda-react-spa/tsconfig.app.json +0 -36
- package/templates/openxiangda-react-spa/tsconfig.js-code-nodes.json +0 -19
- package/templates/openxiangda-react-spa/tsconfig.json +0 -7
- package/templates/openxiangda-react-spa/tsconfig.node.json +0 -10
- package/templates/openxiangda-react-spa/vite.config.ts +0 -209
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-form.mdc +0 -24
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-page.mdc +0 -21
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +0 -40
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-workflow-automation.mdc +0 -21
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +0 -81
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-form.md +0 -38
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-page.md +0 -39
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +0 -59
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-workflow-automation.md +0 -46
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +0 -82
- package/templates/sy-lowcode-app-workspace/AGENTS.md +0 -158
- package/templates/sy-lowcode-app-workspace/DELIVERY.md +0 -43
- package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +0 -51
- package/templates/sy-lowcode-app-workspace/examples/best-practices/README.md +0 -43
- package/templates/sy-lowcode-app-workspace/examples/best-practices/access-governance.md +0 -239
- package/templates/sy-lowcode-app-workspace/examples/best-practices/catalog.json +0 -99
- package/templates/sy-lowcode-app-workspace/examples/best-practices/decision-guide.md +0 -94
- package/templates/sy-lowcode-app-workspace/examples/best-practices/design-style.md +0 -91
- package/templates/sy-lowcode-app-workspace/examples/best-practices/module-structure.md +0 -48
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/index.ts +0 -2
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.test.ts +0 -52
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.ts +0 -37
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/types.ts +0 -19
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/index.ts +0 -4
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.test.ts +0 -42
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.ts +0 -25
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.test.ts +0 -63
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.ts +0 -73
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.test.ts +0 -34
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.ts +0 -73
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/types.ts +0 -66
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/page.tsx +0 -1
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/schema.ts +0 -109
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/page.tsx +0 -1
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/schema.ts +0 -92
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/page.tsx +0 -1
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/schema.ts +0 -153
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/page.tsx +0 -1
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/schema.ts +0 -65
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/daily_ticket_digest/index.ts +0 -44
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/sync_roles_to_platform/index.ts +0 -33
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/glass-home-dashboard/App.tsx +0 -8
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/glass-home-dashboard/GlassHomeDashboard.tsx +0 -232
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/glass-home-dashboard/index.tsx +0 -10
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/glass-home-dashboard/page.config.ts +0 -14
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/glass-home-dashboard/styles.css +0 -196
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/App.tsx +0 -7
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/WorkbenchPage.tsx +0 -36
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/ConfigPanel.tsx +0 -34
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/PreviewPanel.tsx +0 -17
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/index.tsx +0 -10
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/page.config.ts +0 -9
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/reducer.ts +0 -29
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/styles.css +0 -24
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mint-analytics-dashboard/App.tsx +0 -8
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mint-analytics-dashboard/MintAnalyticsDashboard.tsx +0 -279
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mint-analytics-dashboard/index.tsx +0 -10
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mint-analytics-dashboard/page.config.ts +0 -14
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mint-analytics-dashboard/styles.css +0 -163
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/App.tsx +0 -7
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/MobilePortalShell.tsx +0 -31
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/index.tsx +0 -10
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/modules/MobileHome.tsx +0 -13
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/page.config.ts +0 -14
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/routes.ts +0 -13
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/styles.css +0 -11
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/ops-monitor-dashboard/App.tsx +0 -8
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/ops-monitor-dashboard/OpsMonitorDashboard.tsx +0 -306
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/ops-monitor-dashboard/index.tsx +0 -10
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/ops-monitor-dashboard/page.config.ts +0 -14
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/ops-monitor-dashboard/styles.css +0 -248
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/App.tsx +0 -7
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/PcPortalShell.tsx +0 -35
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/components/PortalMetric.tsx +0 -11
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/index.tsx +0 -10
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/HomeModule.tsx +0 -25
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/TicketsModule.tsx +0 -14
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/page.config.ts +0 -14
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/routes.ts +0 -19
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/styles.css +0 -35
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/App.tsx +0 -7
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/TicketOpsPage.tsx +0 -105
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketActionTimeline.tsx +0 -22
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketDetailDrawer.tsx +0 -44
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketTableActions.tsx +0 -57
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/index.tsx +0 -10
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/page.config.ts +0 -9
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/styles.css +0 -35
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/work-order-list-drawer/App.tsx +0 -8
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/work-order-list-drawer/WorkOrderListDrawerPage.tsx +0 -371
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/work-order-list-drawer/index.tsx +0 -10
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/work-order-list-drawer/page.config.ts +0 -14
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/work-order-list-drawer/styles.css +0 -182
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/automation.json +0 -25
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/trigger.json +0 -9
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/notifications/daily-ticket-digest.json +0 -24
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/form-groups/service-ticket-college.json +0 -21
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/roles.json +0 -17
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/workflows/expense-approval-workflow.json +0 -48
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/ConfirmAction.tsx +0 -22
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/QueryState.tsx +0 -37
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/StatusTag.tsx +0 -20
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/admin-ui-templates/DashboardPrimitives.tsx +0 -832
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/admin-ui-templates/chartOptions.ts +0 -140
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/admin-ui-templates/sampleData.ts +0 -466
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/admin-ui-templates/styles.css +0 -874
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/admin-ui-templates/types.ts +0 -150
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/hooks/useTicketOps.ts +0 -96
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/role-governance.ts +0 -48
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/service-ticket.ts +0 -113
- package/templates/sy-lowcode-app-workspace/examples/forms/customer/page.tsx +0 -1
- package/templates/sy-lowcode-app-workspace/examples/forms/customer/schema.ts +0 -36
- package/templates/sy-lowcode-app-workspace/index.html +0 -12
- package/templates/sy-lowcode-app-workspace/package.json +0 -58
- package/templates/sy-lowcode-app-workspace/postcss.config.cjs +0 -9
- package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +0 -600
- package/templates/sy-lowcode-app-workspace/scripts/guard-publish.mjs +0 -29
- package/templates/sy-lowcode-app-workspace/src/dev/App.tsx +0 -820
- package/templates/sy-lowcode-app-workspace/src/forms/.gitkeep +0 -1
- package/templates/sy-lowcode-app-workspace/src/forms/README.md +0 -52
- package/templates/sy-lowcode-app-workspace/src/index.css +0 -26
- package/templates/sy-lowcode-app-workspace/src/js-code-nodes/.gitkeep +0 -1
- package/templates/sy-lowcode-app-workspace/src/js-code-nodes/types.d.ts +0 -3
- package/templates/sy-lowcode-app-workspace/src/main.tsx +0 -29
- package/templates/sy-lowcode-app-workspace/src/pages/.gitkeep +0 -1
- package/templates/sy-lowcode-app-workspace/src/runtime/builtin-overrides.tsx +0 -8
- package/templates/sy-lowcode-app-workspace/src/shared/form-schema.ts +0 -135
- package/templates/sy-lowcode-app-workspace/src/types/app-workspace.types.ts +0 -88
- package/templates/sy-lowcode-app-workspace/tailwind.config.cjs +0 -31
- package/templates/sy-lowcode-app-workspace/tsconfig.app.json +0 -40
- package/templates/sy-lowcode-app-workspace/tsconfig.examples.json +0 -24
- package/templates/sy-lowcode-app-workspace/tsconfig.js-code-nodes.json +0 -19
- package/templates/sy-lowcode-app-workspace/tsconfig.json +0 -7
- package/templates/sy-lowcode-app-workspace/tsconfig.node.json +0 -10
- package/templates/sy-lowcode-app-workspace/vite.config.ts +0 -85
- package/v2/README.md +0 -35
- package/v2/skills/manifest.json +0 -45
- package/v2/skills/openxiangda-v1-maintenance/SKILL.md +0 -21
- package/v2/skills/openxiangda-v1-maintenance/agents/openai.yaml +0 -4
- package/v2/skills/openxiangda-v2/SKILL.md +0 -38
- package/v2/skills/openxiangda-v2-architecture/SKILL.md +0 -29
- package/v2/skills/openxiangda-v2-architecture/agents/openai.yaml +0 -4
- package/v2/skills/openxiangda-v2-backend/SKILL.md +0 -28
- package/v2/skills/openxiangda-v2-backend/agents/openai.yaml +0 -4
- package/v2/skills/openxiangda-v2-data-authz/SKILL.md +0 -28
- package/v2/skills/openxiangda-v2-data-authz/agents/openai.yaml +0 -4
- package/v2/skills/openxiangda-v2-delivery/SKILL.md +0 -30
- package/v2/skills/openxiangda-v2-delivery/agents/openai.yaml +0 -4
- package/v2/skills/openxiangda-v2-frontend/SKILL.md +0 -21
- package/v2/skills/openxiangda-v2-frontend/agents/openai.yaml +0 -4
- package/v2/skills/openxiangda-v2-workflow-events/SKILL.md +0 -28
- package/v2/skills/openxiangda-v2-workflow-events/agents/openai.yaml +0 -4
- /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
|
-
};
|