openxiangda 1.0.134 → 1.0.136

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +6 -0
  2. package/lib/cli.js +947 -16
  3. package/lib/design-gates.js +40 -10
  4. package/lib/sdd.js +777 -0
  5. package/lib/workspace-init.js +3 -0
  6. package/openxiangda-skills/SKILL.md +11 -1
  7. package/openxiangda-skills/references/best-practices.md +32 -5
  8. package/openxiangda-skills/references/openxiangda-api.md +32 -6
  9. package/openxiangda-skills/references/pages/page-sdk.md +3 -0
  10. package/openxiangda-skills/references/pages/publish-flow.md +16 -0
  11. package/openxiangda-skills/references/permission-design-patterns.md +216 -0
  12. package/openxiangda-skills/references/permissions-settings.md +58 -0
  13. package/openxiangda-skills/references/platform-data-model.md +19 -4
  14. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +32 -1
  15. package/openxiangda-skills/skills/openxiangda-architecture-design/SKILL.md +8 -0
  16. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +2 -0
  17. package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +38 -1
  18. package/package.json +2 -1
  19. package/packages/sdk/dist/{ProcessPreview-Dfc4-wIq.d.mts → ProcessPreview-Ci8_UsbN.d.mts} +4 -0
  20. package/packages/sdk/dist/{ProcessPreview-Dfc4-wIq.d.ts → ProcessPreview-Ci8_UsbN.d.ts} +4 -0
  21. package/packages/sdk/dist/build/index.cjs.map +1 -1
  22. package/packages/sdk/dist/build/index.d.mts +7 -0
  23. package/packages/sdk/dist/build/index.d.ts +7 -0
  24. package/packages/sdk/dist/build/index.mjs.map +1 -1
  25. package/packages/sdk/dist/components/index.cjs +32 -4
  26. package/packages/sdk/dist/components/index.cjs.map +1 -1
  27. package/packages/sdk/dist/components/index.d.mts +8 -2
  28. package/packages/sdk/dist/components/index.d.ts +8 -2
  29. package/packages/sdk/dist/components/index.mjs +32 -4
  30. package/packages/sdk/dist/components/index.mjs.map +1 -1
  31. package/packages/sdk/dist/runtime/index.cjs +181 -28
  32. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  33. package/packages/sdk/dist/runtime/index.d.mts +2 -2
  34. package/packages/sdk/dist/runtime/index.d.ts +2 -2
  35. package/packages/sdk/dist/runtime/index.mjs +181 -28
  36. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  37. package/packages/sdk/dist/runtime/react.cjs +152 -25
  38. package/packages/sdk/dist/runtime/react.cjs.map +1 -1
  39. package/packages/sdk/dist/runtime/react.d.mts +57 -4
  40. package/packages/sdk/dist/runtime/react.d.ts +57 -4
  41. package/packages/sdk/dist/runtime/react.mjs +152 -25
  42. package/packages/sdk/dist/runtime/react.mjs.map +1 -1
  43. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +8 -1
  44. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +8 -1
  45. package/templates/openxiangda-react-spa/AGENTS.md +9 -1
  46. package/templates/openxiangda-react-spa/app-workspace.config.ts +14 -0
  47. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +5 -1
  48. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +7 -1
  49. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +6 -0
  50. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +7 -1
  51. package/templates/sy-lowcode-app-workspace/AGENTS.md +10 -1
  52. package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +14 -0
  53. package/templates/sy-lowcode-app-workspace/examples/best-practices/README.md +3 -0
  54. package/templates/sy-lowcode-app-workspace/examples/best-practices/access-governance.md +239 -0
  55. package/templates/sy-lowcode-app-workspace/examples/best-practices/catalog.json +6 -0
  56. package/templates/sy-lowcode-app-workspace/examples/best-practices/decision-guide.md +25 -1
  57. package/templates/sy-lowcode-app-workspace/src/types/app-workspace.types.ts +12 -0
package/lib/cli.js CHANGED
@@ -37,6 +37,17 @@ const {
37
37
  renderResourceExplain,
38
38
  } = require('./design-gates');
39
39
  const { buildDesignReview, renderDesignReview } = require('./design-review');
40
+ const {
41
+ appendBypassLog,
42
+ archiveSddChange,
43
+ approveSddChange,
44
+ buildSddGovernanceReport,
45
+ buildSddStatus,
46
+ initSddWorkspace,
47
+ proposeSddChange,
48
+ syncSddChange,
49
+ verifySddChange,
50
+ } = require('./sdd');
40
51
  const {
41
52
  fail,
42
53
  formatFetchError,
@@ -71,7 +82,9 @@ async function main(argv) {
71
82
  if (command === 'auth') return auth(rest);
72
83
  if (command === 'platform') return platform(rest);
73
84
  if (command === 'doctor') return doctor(rest);
85
+ if (command === 'contract') return contract(rest);
74
86
  if (command === 'design') return design(rest);
87
+ if (command === 'sdd') return sdd(rest);
75
88
  if (command === 'workspace') return workspace(rest);
76
89
  if (command === 'app') return app(rest);
77
90
  if (command === 'form') return form(rest);
@@ -110,13 +123,16 @@ Usage:
110
123
  openxiangda platform list
111
124
  openxiangda platform use <name>
112
125
  openxiangda auth status|refresh|logout [--profile name]
113
- openxiangda doctor [--profile name] [--app-type APP_XXX] [--json]
126
+ openxiangda doctor [--profile name] [--app-type APP_XXX] [--release] [--json]
127
+ openxiangda contract list|explain <contract-name> [--json]
114
128
  openxiangda design gates|template|review [--topic code] [--json]
129
+ openxiangda sdd init|propose|approve|status|context|verify|sync|archive [change] [--json]
115
130
  openxiangda env [--profile name]
116
131
  openxiangda workspace init [dir] [--name package-name] [--runtime legacy|react-spa] [--install] [--profile name --app-type APP_XXX]
117
132
  openxiangda workspace init [dir] --profile <name> --app-name <app-name> [--runtime legacy|react-spa] [--install]
118
133
  openxiangda workspace bind --profile <name> --app-type <APP_XXX>
119
- openxiangda workspace publish --profile <name> [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources] [--prune]
134
+ openxiangda workspace plan|check [--changed] [--json]
135
+ openxiangda workspace publish --profile <name> [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources] [--prune] [--sdd-bypass --reason text]
120
136
  openxiangda app list [--profile name] [--json]
121
137
  openxiangda app create <name> [--profile name] [--runtime legacy|react-spa] [--description text]
122
138
  openxiangda app snapshot <APP_XXX> [--profile name] [--json]
@@ -125,6 +141,7 @@ Usage:
125
141
  openxiangda form bind <formCode> --form-uuid <FORM_XXX>
126
142
  openxiangda form pull <formCode|formUuid> [--json]
127
143
  openxiangda form publish <formCode|formUuid> --bundle-url <url> # repair only
144
+ openxiangda form schema-plan <formCode|formUuid> --schema-json <file>|--components-json <file>
128
145
  openxiangda page list [--profile name] [--json]
129
146
  openxiangda page publish <pageCode> --entry-url <url> --version <v> --build-id <id> # repair only
130
147
  openxiangda page bind <pageCode> --page-id <id>
@@ -156,8 +173,8 @@ Usage:
156
173
  openxiangda permission page-group-list|page-group-create|page-group-update|page-group-delete|page-group-bind
157
174
  openxiangda permission form-group-list|form-group-create|form-group-update|form-group-delete|form-group-bind
158
175
  openxiangda settings get|save|indexes|indexes-save|data-management|data-management-save|public-access
159
- openxiangda resource validate|plan|publish|pull|typegen|explain [--profile name] [--json] [--dry-run]
160
- openxiangda runtime deploy [--profile name] [--dist dist] [--build-id id] [--upload-mode auto|staged|oss-direct|legacy-json] [--upload-timeout-ms ms] [--no-build] [--no-activate] [--json]
176
+ openxiangda resource validate|plan|publish|pull|typegen|explain [--profile name] [--json] [--dry-run] [--sdd-bypass --reason text]
177
+ openxiangda runtime deploy [--profile name] [--dist dist] [--build-id id] [--upload-mode auto|staged|oss-direct|legacy-json] [--upload-timeout-ms ms] [--no-build] [--no-activate] [--json] [--sdd-bypass --reason text]
161
178
  openxiangda runtime releases [--profile name] [--json]
162
179
  openxiangda runtime activate <releaseId> [--profile name] [--json]
163
180
  openxiangda inspect app|form|workflow|automation|permissions
@@ -188,9 +205,11 @@ const SUBCOMMAND_BOOLEAN_FLAGS = new Set([
188
205
  '--publish',
189
206
  '--published',
190
207
  '--redact',
208
+ '--release',
191
209
  '--resources',
192
210
  '--skip-resources',
193
211
  '--strict',
212
+ '--sdd-bypass',
194
213
  '--summary',
195
214
  '--unpublish',
196
215
  '--write-manifest',
@@ -505,10 +524,213 @@ async function design(args) {
505
524
  fail('用法: openxiangda design gates|template|review [--topic code] [--json]');
506
525
  }
507
526
 
527
+ async function sdd(args) {
528
+ const { subcommand, rest } = parseSubcommandArgs(args);
529
+ const { flags, positional } = parseArgs(rest);
530
+ if (wantsSubcommandHelp(subcommand, flags)) {
531
+ print([
532
+ '用法: openxiangda sdd init|propose|approve|status|context|verify|sync|archive [change] [--json]',
533
+ '常用流程:',
534
+ ' openxiangda sdd init',
535
+ ' openxiangda sdd propose add-customer-list --title "客户列表" --forms customer --pages customer_list',
536
+ ' openxiangda sdd approve add-customer-list --summary "用户确认设计"',
537
+ ' openxiangda sdd context --json',
538
+ ' openxiangda sdd verify add-customer-list --changed --json',
539
+ ' openxiangda sdd archive add-customer-list',
540
+ '说明:',
541
+ ' - SDD 记录保存在 openspec/,不依赖官方 OpenSpec CLI。',
542
+ ' - 高风险发布需要 approved change 覆盖本次 forms/pages/resources/runtime 目标。',
543
+ ].join('\n'));
544
+ return;
545
+ }
546
+
547
+ if (subcommand === 'init') {
548
+ const result = initSddWorkspace({
549
+ cwd: process.cwd(),
550
+ path: flags.path,
551
+ force: Boolean(flags.force),
552
+ });
553
+ if (flags.json) return writeJson(result);
554
+ printSddInitReport(result);
555
+ return;
556
+ }
557
+
558
+ if (subcommand === 'propose' || subcommand === 'new') {
559
+ const changeId = positional[0] || flags.change;
560
+ const result = proposeSddChange({
561
+ cwd: process.cwd(),
562
+ changeId,
563
+ title: flags.title || flags.name,
564
+ riskLevel: flags.risk || flags['risk-level'],
565
+ domain: flags.domain,
566
+ force: Boolean(flags.force),
567
+ affected: parseSddAffectedFlags(flags),
568
+ });
569
+ if (flags.json) return writeJson(result);
570
+ print(`已创建 SDD change: ${result.change.id}\n目录: ${result.dir}`);
571
+ return;
572
+ }
573
+
574
+ if (subcommand === 'approve') {
575
+ const changeId = positional[0] || flags.change;
576
+ const result = approveSddChange({
577
+ cwd: process.cwd(),
578
+ changeId,
579
+ summary: flags.summary || flags.note,
580
+ });
581
+ if (flags.json) return writeJson(result);
582
+ print(`SDD change 已确认: ${result.change.id} approvedAt=${result.change.approvedAt}`);
583
+ return;
584
+ }
585
+
586
+ if (subcommand === 'status' || subcommand === 'context') {
587
+ const status = buildSddStatus({ cwd: process.cwd(), configText: readWorkspaceConfigText() });
588
+ const releasePlan = buildWorkspaceReleasePlan({
589
+ changed: Boolean(flags.changed || flags['changed-only']),
590
+ since: flags.since,
591
+ profile: flags.profile || '<profile>',
592
+ skipSdd: true,
593
+ });
594
+ const result = {
595
+ ...status,
596
+ releasePlan,
597
+ contracts:
598
+ subcommand === 'context'
599
+ ? Object.fromEntries(
600
+ Object.entries(API_CONTRACTS).map(([name, contract]) => [
601
+ name,
602
+ {
603
+ mode: contract.mode,
604
+ endpoint: contract.endpoint,
605
+ response: contract.response,
606
+ guarantees: contract.guarantees,
607
+ },
608
+ ])
609
+ )
610
+ : undefined,
611
+ };
612
+ if (flags.json || subcommand === 'context') return writeJson(result);
613
+ printSddStatusReport(result);
614
+ return;
615
+ }
616
+
617
+ if (subcommand === 'verify') {
618
+ const changeId = positional[0] || flags.change;
619
+ const releasePlan = buildWorkspaceReleasePlan({
620
+ changed: flags.changed !== false,
621
+ since: flags.since,
622
+ profile: flags.profile || '<profile>',
623
+ skipSdd: true,
624
+ });
625
+ const result = verifySddChange({
626
+ cwd: process.cwd(),
627
+ changeId,
628
+ releasePlan,
629
+ writeMetadata: true,
630
+ });
631
+ if (flags.json) {
632
+ writeJson(result);
633
+ if (!result.passed) process.exitCode = 1;
634
+ return;
635
+ }
636
+ printSddVerifyReport(result);
637
+ if (!result.passed) fail(`SDD verify 失败: ${result.errors[0].message}`);
638
+ return;
639
+ }
640
+
641
+ if (subcommand === 'sync') {
642
+ const changeId = positional[0] || flags.change;
643
+ const result = syncSddChange({ cwd: process.cwd(), changeId });
644
+ if (flags.json) return writeJson(result);
645
+ print(`SDD specs 已同步: ${result.change.id}\n${result.synced.map(item => `- ${item}`).join('\n')}`);
646
+ return;
647
+ }
648
+
649
+ if (subcommand === 'archive') {
650
+ const changeId = positional[0] || flags.change;
651
+ const releasePlan = buildWorkspaceReleasePlan({
652
+ changed: flags.changed !== false,
653
+ since: flags.since,
654
+ profile: flags.profile || '<profile>',
655
+ skipSdd: true,
656
+ });
657
+ const result = archiveSddChange({
658
+ cwd: process.cwd(),
659
+ changeId,
660
+ releasePlan,
661
+ force: Boolean(flags.force),
662
+ });
663
+ if (flags.json) return writeJson(result);
664
+ print(`SDD change 已归档: ${result.change.id}\n目录: ${result.archiveDir}`);
665
+ return;
666
+ }
667
+
668
+ fail('用法: openxiangda sdd init|propose|approve|status|context|verify|sync|archive [change] [--json]');
669
+ }
670
+
671
+ function parseSddAffectedFlags(flags) {
672
+ const split = value =>
673
+ value === undefined || value === null || value === false
674
+ ? []
675
+ : String(value)
676
+ .split(',')
677
+ .map(item => item.trim())
678
+ .filter(Boolean);
679
+ return {
680
+ forms: split(flags.forms || flags.form),
681
+ pages: split(flags.pages || flags.page),
682
+ resources: Boolean(flags.resources),
683
+ runtime: Boolean(flags.runtime),
684
+ backend: Boolean(flags.backend),
685
+ contracts: Boolean(flags.contracts),
686
+ };
687
+ }
688
+
689
+ function printSddInitReport(result) {
690
+ const lines = [`OpenXiangda SDD initialized: ${result.path}`];
691
+ for (const op of result.operations) {
692
+ lines.push(`- ${op.action}: ${op.path}`);
693
+ }
694
+ print(lines.join('\n'));
695
+ }
696
+
697
+ function printSddStatusReport(result) {
698
+ const lines = [
699
+ `OpenXiangda SDD status: ${result.governance.initialized ? 'initialized' : 'not initialized'}`,
700
+ `path: ${result.governance.path}`,
701
+ `strictHighRisk: ${result.governance.strictHighRisk ? 'true' : 'false'}`,
702
+ `active changes: ${result.activeChanges.length}`,
703
+ ];
704
+ for (const change of result.activeChanges) {
705
+ lines.push(`- ${change.id}: ${change.status}${change.approvedAt ? ` approvedAt=${change.approvedAt}` : ''}`);
706
+ }
707
+ if (result.bypassLog) lines.push(`bypass log: ${result.bypassLog}`);
708
+ print(lines.join('\n'));
709
+ }
710
+
711
+ function printSddVerifyReport(result) {
712
+ const lines = [
713
+ `OpenXiangda SDD verify: ${result.passed ? 'passed' : 'failed'}`,
714
+ `change: ${result.change.id}`,
715
+ ];
716
+ if (result.errors.length > 0) {
717
+ lines.push('Errors:');
718
+ for (const error of result.errors) {
719
+ lines.push(`- ${error.name}: ${error.message}`);
720
+ for (const detail of error.details || []) lines.push(` ${detail}`);
721
+ }
722
+ }
723
+ if (result.warnings.length > 0) {
724
+ lines.push('Warnings:');
725
+ for (const warning of result.warnings) lines.push(`- ${warning.name}: ${warning.message}`);
726
+ }
727
+ print(lines.join('\n'));
728
+ }
729
+
508
730
  async function doctor(args) {
509
731
  const { flags } = parseArgs(args);
510
732
  if (flags.help || flags.h) {
511
- print('用法: openxiangda doctor [--profile name] [--app-type APP_XXX] [--agent codex] [--json]');
733
+ print('用法: openxiangda doctor [--profile name] [--app-type APP_XXX] [--agent codex] [--release] [--json]');
512
734
  return;
513
735
  }
514
736
  const config = loadConfig();
@@ -537,6 +759,8 @@ async function doctor(args) {
537
759
  hasDirectory: fs.existsSync(path.join(process.cwd(), 'src', 'resources')),
538
760
  validation: null,
539
761
  },
762
+ sdd: null,
763
+ release: null,
540
764
  skills: null,
541
765
  checks: [],
542
766
  };
@@ -603,6 +827,23 @@ async function doctor(args) {
603
827
  message: outdatedSkills.length > 0 ? `${outdatedSkills.length} 个 skill 未安装或过期` : undefined,
604
828
  });
605
829
 
830
+ if (flags.release) {
831
+ result.release = buildWorkspaceReleasePlan({
832
+ changed: true,
833
+ profile: profileName || config.currentProfile || '<profile>',
834
+ check: true,
835
+ });
836
+ result.sdd = result.release.sdd || buildSddStatus({ cwd: process.cwd(), configText: readWorkspaceConfigText() });
837
+ result.checks.push({
838
+ name: 'workspace-release',
839
+ status: result.release.errors.length > 0 ? 'error' : 'ok',
840
+ message:
841
+ result.release.errors.length > 0
842
+ ? `${result.release.errors.length} 个发布前错误`
843
+ : undefined,
844
+ });
845
+ }
846
+
606
847
  if (flags.json) return writeJson(result);
607
848
  printDoctorReport(result);
608
849
  }
@@ -638,6 +879,17 @@ function printDoctorReport(result) {
638
879
  `resource validation: ${result.resources.validation.errors.length} errors, ${result.resources.validation.warnings.length} warnings`
639
880
  );
640
881
  }
882
+ if (result.release) {
883
+ lines.push(
884
+ `release plan: forms=${result.release.targets.forms.length}, pages=${result.release.targets.pages.length}, resources=${result.release.targets.resources ? 'yes' : 'no'}, runtime=${result.release.targets.runtime ? 'yes' : 'no'}`
885
+ );
886
+ }
887
+ if (result.sdd) {
888
+ const governance = result.sdd.governance || result.sdd.governanceConfig || {};
889
+ lines.push(
890
+ `sdd: ${governance.initialized ? 'initialized' : 'not initialized'} path=${governance.path || 'openspec'} strict=${governance.strictHighRisk ? 'true' : 'false'}`
891
+ );
892
+ }
641
893
  for (const check of result.checks) {
642
894
  const suffix = check.message ? ` - ${check.message}` : '';
643
895
  lines.push(`- ${check.name}: ${check.status}${suffix}`);
@@ -646,6 +898,131 @@ function printDoctorReport(result) {
646
898
  print(lines.join('\n'));
647
899
  }
648
900
 
901
+ const API_CONTRACTS = {
902
+ 'sdk.form.create': {
903
+ name: 'sdk.form.create',
904
+ mode: 'strict-by-default',
905
+ endpoint: '/:appType/v1/form/saveFormData.json',
906
+ method: 'POST',
907
+ request: {
908
+ formUuid: 'string',
909
+ formDataJson: 'JSON.stringify(data)',
910
+ saveAsDraft: 'boolean | undefined',
911
+ startProcess: 'boolean | undefined',
912
+ processStartMode: 'auto | manual | draft | delayed | none | string | undefined',
913
+ },
914
+ response: {
915
+ code: '200 | 0 | success code',
916
+ result: 'normalized FormCreateResult object containing formInstId/formInstanceId',
917
+ data: 'legacy payload containing formInstId/formInstanceId',
918
+ formInstId: 'string',
919
+ formInstanceId: 'string',
920
+ processInstanceId: 'string | undefined',
921
+ serialNumber: 'string | undefined, first generated SerialNumberField value when present',
922
+ serialNumbers: 'Record<fieldId, string> | undefined',
923
+ },
924
+ guarantees: [
925
+ '保存接口只承诺返回实例标识、流程标识,以及保存时已生成的流水号字段。',
926
+ '保存接口不返回完整业务数据、公式回填或其他服务端格式化后的行数据。',
927
+ '缺少 formInstId/formInstanceId 时 strict 模式报错。',
928
+ ],
929
+ followUp: '需要完整行数据、公式回填或非流水号服务端字段时,保存成功后调用 sdk.form.getDetail({ formUuid, formInstId })。',
930
+ forbidden: [
931
+ '不要从 response.data/response.result 猜完整行数据。',
932
+ '不要生成 inst_${Date.now()} 这类假实例 ID。',
933
+ '不要默认 lookupAfterCreate;仅旧项目显式启用。',
934
+ ],
935
+ },
936
+ 'sdk.form.update': {
937
+ name: 'sdk.form.update',
938
+ endpoint: '/:appType/v1/form/updateFormData.json',
939
+ method: 'POST',
940
+ response: {
941
+ code: '200 | 0 | success code',
942
+ result: 'formInstId string',
943
+ formInstId: 'string',
944
+ formInstanceId: 'string',
945
+ },
946
+ guarantees: [
947
+ '更新接口只承诺返回被更新的实例标识。',
948
+ '更新接口不返回完整业务数据。',
949
+ ],
950
+ },
951
+ 'sdk.form.getDetail': {
952
+ name: 'sdk.form.getDetail',
953
+ endpoint: '/:appType/v1/form/getFormDataById.json',
954
+ method: 'GET',
955
+ response: {
956
+ result: '完整表单实例详情,包含平台可返回的业务字段和系统字段',
957
+ },
958
+ guarantees: [
959
+ '需要读取保存后的流水号、服务端生成字段或完整行数据时使用该接口。',
960
+ ],
961
+ },
962
+ 'sdk.form.advancedSearch': {
963
+ name: 'sdk.form.advancedSearch',
964
+ endpoint: '/:appType/v1/form/advancedSearch.json',
965
+ method: 'GET',
966
+ response: {
967
+ currentPage: 'number',
968
+ data: 'array',
969
+ totalCount: 'number',
970
+ },
971
+ guarantees: [
972
+ '列表页必须使用分页查询和服务端过滤。',
973
+ '不要拉取大页数据后在本地过滤。',
974
+ ],
975
+ },
976
+ };
977
+
978
+ async function contract(args) {
979
+ const [subcommand, ...rest] = args;
980
+ const { flags, positional } = parseArgs(rest);
981
+ if (wantsSubcommandHelp(subcommand, flags)) {
982
+ print('用法: openxiangda contract list|explain <contract-name> [--json]');
983
+ return;
984
+ }
985
+
986
+ if (subcommand === 'list') {
987
+ const result = Object.keys(API_CONTRACTS).sort();
988
+ if (flags.json) return writeJson({ contracts: result });
989
+ print(result.join('\n'));
990
+ return;
991
+ }
992
+
993
+ if (subcommand === 'explain') {
994
+ const name = positional[0];
995
+ if (!name) {
996
+ fail('用法: openxiangda contract explain <contract-name>');
997
+ }
998
+ const contractInfo = API_CONTRACTS[name];
999
+ if (!contractInfo) {
1000
+ fail(`未知契约: ${name}。可用契约: ${Object.keys(API_CONTRACTS).sort().join(', ')}`);
1001
+ }
1002
+ if (flags.json) return writeJson(contractInfo);
1003
+ const lines = [
1004
+ `${contractInfo.name}`,
1005
+ `${contractInfo.method || 'GET'} ${contractInfo.endpoint}`,
1006
+ '',
1007
+ 'Response:',
1008
+ JSON.stringify(contractInfo.response || {}, null, 2),
1009
+ ];
1010
+ if (contractInfo.guarantees?.length) {
1011
+ lines.push('', 'Guarantees:', ...contractInfo.guarantees.map(item => `- ${item}`));
1012
+ }
1013
+ if (contractInfo.followUp) {
1014
+ lines.push('', `Follow-up: ${contractInfo.followUp}`);
1015
+ }
1016
+ if (contractInfo.forbidden?.length) {
1017
+ lines.push('', 'Forbidden:', ...contractInfo.forbidden.map(item => `- ${item}`));
1018
+ }
1019
+ print(lines.join('\n'));
1020
+ return;
1021
+ }
1022
+
1023
+ fail('用法: openxiangda contract list|explain <contract-name> [--json]');
1024
+ }
1025
+
649
1026
  async function platform(args) {
650
1027
  const [subcommand, ...rest] = args;
651
1028
  const { flags, positional } = parseArgs(rest);
@@ -1437,6 +1814,28 @@ async function workspace(args) {
1437
1814
  return;
1438
1815
  }
1439
1816
 
1817
+ if (subcommand === 'plan' || subcommand === 'check') {
1818
+ const changed = Boolean(flags.changed || flags['changed-only'] || flags.since);
1819
+ const plan = buildWorkspaceReleasePlan({
1820
+ changed,
1821
+ since: flags.since,
1822
+ profile: flags.profile || config.currentProfile || '<profile>',
1823
+ check: subcommand === 'check',
1824
+ });
1825
+ if (flags.json) {
1826
+ writeJson(plan);
1827
+ if (subcommand === 'check' && plan.errors.length > 0) {
1828
+ process.exitCode = 1;
1829
+ }
1830
+ return;
1831
+ }
1832
+ printWorkspaceReleasePlan(plan);
1833
+ if (subcommand === 'check' && plan.errors.length > 0) {
1834
+ fail(`workspace check 失败: ${plan.errors[0].message}`);
1835
+ }
1836
+ return;
1837
+ }
1838
+
1440
1839
  const { profileName, profile } = getProfile(config, flags.profile);
1441
1840
 
1442
1841
  if (subcommand === 'bind') {
@@ -1482,12 +1881,22 @@ async function workspace(args) {
1482
1881
  `profile ${profileName} 未绑定应用。先执行 openxiangda workspace bind --profile ${profileName} --app-type APP_XXX`
1483
1882
  );
1484
1883
  }
1884
+ const reactSpaWorkspace = isReactSpaWorkspace();
1885
+ const sddPlan = buildWorkspacePublishSddPlan(
1886
+ publishOptions,
1887
+ reactSpaWorkspace ? 'react-spa' : getWorkspaceRuntimeModeFromConfig(readWorkspaceConfigText()),
1888
+ profileName
1889
+ );
1890
+ if (publishOptions.dryRun) {
1891
+ printWorkspaceReleasePlan(sddPlan);
1892
+ } else {
1893
+ assertSddReleaseGate(sddPlan, flags, 'openxiangda workspace publish');
1894
+ }
1485
1895
  await requestWithAuth(
1486
1896
  config,
1487
1897
  profileName,
1488
1898
  `/openxiangda-api/v1/apps/${encodeURIComponent(bound.appType)}/snapshot`
1489
1899
  );
1490
- const reactSpaWorkspace = isReactSpaWorkspace();
1491
1900
  if (reactSpaWorkspace && !publishOptions.legacyFormBundle) {
1492
1901
  if (publishOptions.targetForm && !publishOptions.targetPage && !publishOptions.only && !publishOptions.changed) {
1493
1902
  runReactSpaFormSchemaPublish(profileName, profile, bound.appType, publishOptions);
@@ -1528,7 +1937,7 @@ async function workspace(args) {
1528
1937
  return;
1529
1938
  }
1530
1939
 
1531
- fail('用法: openxiangda workspace init|bind|publish [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources]');
1940
+ fail('用法: openxiangda workspace init|bind|plan|check|publish [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources]');
1532
1941
  }
1533
1942
 
1534
1943
  function printWorkspaceHelp() {
@@ -1539,7 +1948,9 @@ Usage:
1539
1948
  openxiangda workspace init [dir] --profile <name> --app-name <app-name> [--runtime legacy|react-spa] [--description text] [--install] [--force]
1540
1949
  openxiangda workspace init [dir] --profile <name> --app-type <APP_XXX> [--runtime legacy|react-spa] [--install] [--force]
1541
1950
  openxiangda workspace bind --profile <name> --app-type <APP_XXX>
1542
- openxiangda workspace publish --profile <name> [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources] [--prune]
1951
+ openxiangda workspace plan [--changed] [--json]
1952
+ openxiangda workspace check [--changed] [--json]
1953
+ openxiangda workspace publish --profile <name> [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources] [--prune] [--sdd-bypass --reason text]
1543
1954
 
1544
1955
  React SPA:
1545
1956
  openxiangda workspace publish --profile <name> --form <formCode> # sync form schema only
@@ -1556,7 +1967,8 @@ Options:
1556
1967
  --changed [--since ref] Publish changed classic resources.
1557
1968
  --dry-run Show planned work without writing.
1558
1969
  --resources / --skip-resources Include or skip src/resources publish.
1559
- --legacy-form-bundle Force legacy workspace publish in React SPA workspaces.`);
1970
+ --legacy-form-bundle Force legacy workspace publish in React SPA workspaces.
1971
+ plan/check Show release order and fail on risky AI fallback patterns.`);
1560
1972
  }
1561
1973
 
1562
1974
  async function app(args) {
@@ -1637,7 +2049,7 @@ async function form(args) {
1637
2049
  const [subcommand, ...rest] = args;
1638
2050
  const { flags, positional } = parseArgs(rest);
1639
2051
  if (wantsSubcommandHelp(subcommand, flags)) {
1640
- print('用法: openxiangda form list|create|bind|pull|publish [--profile name] [--json]');
2052
+ print('用法: openxiangda form list|create|bind|pull|publish|schema-plan [--profile name] [--json]');
1641
2053
  return;
1642
2054
  }
1643
2055
  const config = loadConfig();
@@ -1742,7 +2154,30 @@ async function form(args) {
1742
2154
  return;
1743
2155
  }
1744
2156
 
1745
- fail('用法: openxiangda form list|create|bind|pull|publish');
2157
+ if (subcommand === 'schema-plan') {
2158
+ const [formKey] = positional;
2159
+ if (!formKey) {
2160
+ fail('用法: openxiangda form schema-plan <formCode|formUuid> --schema-json <file>|--components-json <file>');
2161
+ }
2162
+ const target = getWorkspaceTarget(config, profileName, flags);
2163
+ const formUuid = resolveFormUuid(target.bound, formKey, flags);
2164
+ const body = readFormSchemaPlanBody(flags);
2165
+ const data = await requestWithAuth(
2166
+ config,
2167
+ target.profileName,
2168
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/schema-storage-plan`,
2169
+ {
2170
+ method: 'POST',
2171
+ body,
2172
+ strictEnvelope: true,
2173
+ }
2174
+ );
2175
+ if (flags.json) return writeJson(data);
2176
+ print(JSON.stringify(data, null, 2));
2177
+ return;
2178
+ }
2179
+
2180
+ fail('用法: openxiangda form list|create|bind|pull|publish|schema-plan');
1746
2181
  }
1747
2182
 
1748
2183
  async function page(args) {
@@ -4046,9 +4481,12 @@ async function resource(args) {
4046
4481
  return;
4047
4482
  }
4048
4483
 
4049
- const target = getWorkspaceTarget(config, profileName, flags);
4050
4484
  const validation = validateWorkspaceResources(manifest);
4051
- validateWorkspaceResourceBindings(manifest, target.bound, validation);
4485
+ let target = null;
4486
+ if (!isManifestEmpty(manifest)) {
4487
+ target = getWorkspaceTarget(config, profileName, flags);
4488
+ validateWorkspaceResourceBindings(manifest, target.bound, validation);
4489
+ }
4052
4490
  await validateCompiledWorkflowResources(manifest, validation);
4053
4491
  if (subcommand === 'validate') {
4054
4492
  if (flags.json) return writeJson(validation);
@@ -4063,6 +4501,7 @@ async function resource(args) {
4063
4501
  }
4064
4502
 
4065
4503
  const dryRun = subcommand === 'publish' && Boolean(flags['dry-run']);
4504
+ target = target || getWorkspaceTarget(config, profileName, flags);
4066
4505
  if (subcommand === 'plan' || dryRun) {
4067
4506
  const plan = await buildResourcePlan(config, target, manifest);
4068
4507
  if (dryRun) {
@@ -4075,6 +4514,16 @@ async function resource(args) {
4075
4514
  }
4076
4515
 
4077
4516
  const publishPlan = await buildResourcePlan(config, target, manifest);
4517
+ if ((publishPlan.summary.create || 0) + (publishPlan.summary.update || 0) + (publishPlan.summary.delete || 0) > 0) {
4518
+ assertSddReleaseGate(
4519
+ buildExplicitSddReleasePlan(
4520
+ { forms: [], pages: [], resources: true, runtime: false, other: [] },
4521
+ { files: getWorkspaceChangedFiles() }
4522
+ ),
4523
+ flags,
4524
+ 'openxiangda resource publish'
4525
+ );
4526
+ }
4078
4527
  const result = await publishResourceManifest(config, target, manifest, {
4079
4528
  planActions: publishPlan.actions,
4080
4529
  skipNoop: !Boolean(flags.force),
@@ -4133,6 +4582,14 @@ async function runtime(args) {
4133
4582
  }
4134
4583
 
4135
4584
  if (subcommand === 'deploy') {
4585
+ assertSddReleaseGate(
4586
+ buildExplicitSddReleasePlan(
4587
+ { forms: [], pages: [], resources: false, runtime: true, other: [] },
4588
+ { files: getWorkspaceChangedFiles() }
4589
+ ),
4590
+ flags,
4591
+ 'openxiangda runtime deploy'
4592
+ );
4136
4593
  const buildId = normalizeRuntimeBuildId(flags['build-id'] || createRuntimeBuildId());
4137
4594
  const distDir = path.resolve(process.cwd(), flags.dist || 'dist');
4138
4595
  const uploadMode = normalizeRuntimeUploadMode(flags['upload-mode']);
@@ -4742,12 +5199,14 @@ async function commands(args) {
4742
5199
  'update check|install',
4743
5200
  'platform add|list|use|remove',
4744
5201
  'auth status|refresh|logout',
4745
- 'doctor [--profile name] [--app-type APP_XXX]',
5202
+ 'doctor [--profile name] [--app-type APP_XXX] [--release]',
5203
+ 'contract list|explain <contract-name>',
4746
5204
  'design gates|template|review [--topic code]',
5205
+ 'sdd init|propose|approve|status|context|verify|sync|archive',
4747
5206
  'env',
4748
- 'workspace init|bind|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
5207
+ 'workspace init|bind|plan|check|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
4749
5208
  'app list|create|snapshot',
4750
- 'form list|create|bind|pull|publish',
5209
+ 'form list|create|bind|pull|publish|schema-plan',
4751
5210
  'page list|publish|bind|releases|activate',
4752
5211
  'menu list|create|update|sort|bind|delete',
4753
5212
  'workflow compile|list|create|bind|pull|publish|delete|validate',
@@ -4777,6 +5236,7 @@ async function commands(args) {
4777
5236
  'public-access is the new React SPA public policy resource for /view/:appType/public/* routes; page code should use PublicAccessGate, and Page SDK hooks must stay inside OpenXiangdaProvider + OpenXiangdaPageProvider.',
4778
5237
  'settings public-access is legacy form public-access compatibility/repair and should not be used for new React SPA apps.',
4779
5238
  'Direct live mutation commands should use --dry-run first and --write-manifest when the repository should remain source of truth.',
5239
+ 'OpenXiangda native SDD uses openspec/ records; high-risk publishes require an approved change unless --sdd-bypass --reason is used.',
4780
5240
  ],
4781
5241
  };
4782
5242
  if (flags.json) return writeJson(manifest);
@@ -4794,6 +5254,9 @@ function printWorkspaceInitReport(result) {
4794
5254
  if (result.bound) {
4795
5255
  lines.push(`已绑定 ${result.bound.profile}: ${result.bound.appType}`);
4796
5256
  }
5257
+ if (result.sdd?.initialized) {
5258
+ lines.push(`SDD: ${result.sdd.path}`);
5259
+ }
4797
5260
  if (result.installedDependencies) {
4798
5261
  lines.push('依赖已安装');
4799
5262
  }
@@ -5357,6 +5820,19 @@ function resolveFormUuid(bound, formKey, flags = {}) {
5357
5820
  return mapped || formKey;
5358
5821
  }
5359
5822
 
5823
+ function readFormSchemaPlanBody(flags = {}) {
5824
+ if (flags['schema-json']) {
5825
+ return { schema: readJsonArg(flags['schema-json'], 'schema-json') };
5826
+ }
5827
+ if (flags['components-json']) {
5828
+ return { components: readJsonArg(flags['components-json'], 'components-json') };
5829
+ }
5830
+ if (flags['json-file'] || flags['body-json']) {
5831
+ return readDirectJsonBody(flags, 'form schema-plan');
5832
+ }
5833
+ fail('form schema-plan 需要 --schema-json <file> 或 --components-json <file>');
5834
+ }
5835
+
5360
5836
  function resolveOptionalFormUuid(bound, formCode) {
5361
5837
  if (!formCode) return undefined;
5362
5838
  return bound.resources?.forms?.[formCode]?.formUuid || formCode;
@@ -10926,6 +11402,459 @@ function unwrapStrictApi(payload) {
10926
11402
  return 'data' in payload ? payload.data : payload;
10927
11403
  }
10928
11404
 
11405
+ function listDirectoriesIfExists(dir) {
11406
+ if (!fs.existsSync(dir)) return [];
11407
+ return fs
11408
+ .readdirSync(dir, { withFileTypes: true })
11409
+ .filter(entry => entry.isDirectory())
11410
+ .map(entry => entry.name)
11411
+ .sort();
11412
+ }
11413
+
11414
+ function normalizeWorkspaceRelativePath(value) {
11415
+ return String(value || '')
11416
+ .trim()
11417
+ .replace(/^"|"$/g, '')
11418
+ .split(path.sep)
11419
+ .join('/');
11420
+ }
11421
+
11422
+ function parseGitStatusPath(line) {
11423
+ const raw = line.slice(3).trim();
11424
+ const renamed = raw.includes(' -> ') ? raw.split(' -> ').pop() : raw;
11425
+ return normalizeWorkspaceRelativePath(renamed);
11426
+ }
11427
+
11428
+ function readGitNameList(args, options = {}) {
11429
+ const result = spawnSync('git', args, {
11430
+ cwd: process.cwd(),
11431
+ encoding: 'utf8',
11432
+ });
11433
+ if (result.status !== 0) {
11434
+ if (options.required) {
11435
+ fail(`git ${args.join(' ')} 失败: ${(result.stderr || result.stdout || '').trim()}`);
11436
+ }
11437
+ return [];
11438
+ }
11439
+ return result.stdout
11440
+ .split(/\r?\n/)
11441
+ .map(line => line.trimEnd())
11442
+ .filter(Boolean)
11443
+ .map(normalizeWorkspaceRelativePath)
11444
+ .filter(Boolean);
11445
+ }
11446
+
11447
+ function getWorkspaceChangedFiles(options = {}) {
11448
+ const since = String(options.since || '').trim();
11449
+ if (since) {
11450
+ return unique([
11451
+ ...readGitNameList(
11452
+ ['diff', '--name-only', '--diff-filter=ACMRTUXB', `${since}...HEAD`],
11453
+ { required: true }
11454
+ ),
11455
+ ...readGitNameList(['diff', '--name-only', '--diff-filter=ACMRTUXB']),
11456
+ ...readGitNameList(['diff', '--name-only', '--cached', '--diff-filter=ACMRTUXB']),
11457
+ ...readGitNameList(['ls-files', '--others', '--exclude-standard']),
11458
+ ]).sort();
11459
+ }
11460
+
11461
+ const result = spawnSync('git', ['status', '--short', '--untracked-files=all'], {
11462
+ cwd: process.cwd(),
11463
+ encoding: 'utf8',
11464
+ });
11465
+ if (result.status !== 0) {
11466
+ return [];
11467
+ }
11468
+ return result.stdout
11469
+ .split(/\r?\n/)
11470
+ .map(line => line.trimEnd())
11471
+ .filter(Boolean)
11472
+ .map(parseGitStatusPath)
11473
+ .filter(Boolean);
11474
+ }
11475
+
11476
+ function getAllWorkspaceSourceFiles() {
11477
+ const files = [];
11478
+ const visit = dir => {
11479
+ if (!fs.existsSync(dir)) return;
11480
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
11481
+ if (['node_modules', 'dist', '.vite', '.cache', 'coverage'].includes(entry.name)) continue;
11482
+ const entryPath = path.join(dir, entry.name);
11483
+ if (entry.isDirectory()) {
11484
+ visit(entryPath);
11485
+ continue;
11486
+ }
11487
+ if (!entry.isFile()) continue;
11488
+ files.push(normalizeWorkspaceRelativePath(path.relative(process.cwd(), entryPath)));
11489
+ }
11490
+ };
11491
+ ['src', 'app-workspace.config.ts', 'package.json', 'vite.config.ts'].forEach(item => {
11492
+ const target = path.join(process.cwd(), item);
11493
+ if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {
11494
+ visit(target);
11495
+ } else if (fs.existsSync(target)) {
11496
+ files.push(item);
11497
+ }
11498
+ });
11499
+ return files.sort();
11500
+ }
11501
+
11502
+ function classifyWorkspaceFiles(files, runtimeMode) {
11503
+ const forms = new Set();
11504
+ const pages = new Set();
11505
+ let resources = false;
11506
+ let runtime = false;
11507
+ const other = [];
11508
+
11509
+ for (const file of files) {
11510
+ const formMatch = file.match(/^src\/forms\/([^/]+)/);
11511
+ if (formMatch) {
11512
+ forms.add(formMatch[1]);
11513
+ continue;
11514
+ }
11515
+ const pageMatch = file.match(/^src\/pages\/([^/]+)/);
11516
+ if (pageMatch) {
11517
+ if (runtimeMode === 'react-spa') {
11518
+ runtime = true;
11519
+ } else {
11520
+ pages.add(pageMatch[1]);
11521
+ }
11522
+ continue;
11523
+ }
11524
+ if (file.startsWith('src/resources/')) {
11525
+ resources = true;
11526
+ continue;
11527
+ }
11528
+ if (
11529
+ file === 'package.json' ||
11530
+ file === 'vite.config.ts' ||
11531
+ file === 'index.html' ||
11532
+ file.startsWith('src/')
11533
+ ) {
11534
+ runtime = runtimeMode === 'react-spa' || runtime;
11535
+ continue;
11536
+ }
11537
+ other.push(file);
11538
+ }
11539
+
11540
+ return {
11541
+ forms: Array.from(forms).sort(),
11542
+ pages: Array.from(pages).sort(),
11543
+ resources,
11544
+ runtime,
11545
+ other,
11546
+ };
11547
+ }
11548
+
11549
+ function readWorkspaceConfigText() {
11550
+ const file = path.join(process.cwd(), 'app-workspace.config.ts');
11551
+ return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
11552
+ }
11553
+
11554
+ function getWorkspaceRuntimeModeFromConfig(configText) {
11555
+ return /runtimeMode\s*:\s*['"]react-spa['"]/.test(configText) ? 'react-spa' : 'legacy';
11556
+ }
11557
+
11558
+ function getWorkspaceCompatibilityFromConfig(configText) {
11559
+ return {
11560
+ apiContracts: /apiContracts\s*:\s*['"]legacy['"]/.test(configText) ? 'legacy' : 'strict',
11561
+ legacyFallbacks: /legacyFallbacks\s*:\s*true/.test(configText),
11562
+ requestTrace: /requestTrace\s*:\s*true/.test(configText),
11563
+ };
11564
+ }
11565
+
11566
+ const AI_RELIABILITY_RULES = [
11567
+ {
11568
+ id: 'fake-form-instance-id',
11569
+ severity: 'error',
11570
+ pattern: /inst_\$\{Date\.now\(\)\}|inst_['"]?\s*\+\s*Date\.now\(/,
11571
+ message: '禁止生成假 formInstanceId;保存接口缺少实例 ID 必须报错。',
11572
+ },
11573
+ {
11574
+ id: 'implicit-response-guessing',
11575
+ severity: 'warn',
11576
+ pattern: /response\??\.(data|result)\s*(\?\?|\|\|)\s*response\??\.(result|data)/,
11577
+ message: '避免在业务代码中猜 data/result;应使用 SDK 契约或 endpoint normalizer。',
11578
+ },
11579
+ {
11580
+ id: 'legacy-lookup-after-create',
11581
+ severity: 'warn',
11582
+ pattern: /lookupAfterCreate\s*:\s*true/,
11583
+ message: 'lookupAfterCreate 只能用于旧项目兼容;新代码保存后应显式 getDetail。',
11584
+ },
11585
+ {
11586
+ id: 'direct-openxiangda-api',
11587
+ severity: 'error',
11588
+ pattern: /fetch\s*\([^)]*openxiangda-api|axios\.[a-z]+\([^)]*openxiangda-api/,
11589
+ message: '页面代码不要直接请求 openxiangda-api;使用 Page SDK、connector 或 App Function。',
11590
+ },
11591
+ {
11592
+ id: 'mock-or-fallback-data',
11593
+ severity: 'warn',
11594
+ pattern: /\b(mockData|fallbackData|fallbackRows|demoRows)\b/,
11595
+ message: '不要用 mock/fallback 数据掩盖真实接口失败;开发态应暴露错误。',
11596
+ },
11597
+ ];
11598
+
11599
+ function scanWorkspaceReliabilityIssues(files) {
11600
+ const issues = [];
11601
+ for (const file of files) {
11602
+ if (!file.startsWith('src/')) continue;
11603
+ if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(file)) continue;
11604
+ if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file)) continue;
11605
+ const abs = path.join(process.cwd(), file);
11606
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) continue;
11607
+ const content = fs.readFileSync(abs, 'utf8');
11608
+ const lines = content.split(/\r?\n/);
11609
+ for (let index = 0; index < lines.length; index += 1) {
11610
+ const line = lines[index];
11611
+ for (const rule of AI_RELIABILITY_RULES) {
11612
+ if (rule.pattern.test(line)) {
11613
+ issues.push({
11614
+ rule: rule.id,
11615
+ severity: rule.severity,
11616
+ file,
11617
+ line: index + 1,
11618
+ message: rule.message,
11619
+ });
11620
+ }
11621
+ }
11622
+ }
11623
+ }
11624
+ return issues;
11625
+ }
11626
+
11627
+ function buildWorkspaceReleaseCommands(targets, runtimeMode, profile) {
11628
+ const profileArg = profile || '<profile>';
11629
+ const commands = [];
11630
+ if (runtimeMode === 'react-spa') {
11631
+ for (const formCode of targets.forms) {
11632
+ commands.push(`openxiangda workspace publish --profile ${profileArg} --form ${formCode}`);
11633
+ }
11634
+ if (targets.resources) {
11635
+ commands.push(`openxiangda resource publish --profile ${profileArg}`);
11636
+ }
11637
+ if (targets.runtime) {
11638
+ commands.push(`openxiangda runtime deploy --profile ${profileArg}`);
11639
+ }
11640
+ return commands;
11641
+ }
11642
+
11643
+ const onlyTargets = [
11644
+ ...targets.forms.map(code => `forms/${code}`),
11645
+ ...targets.pages.map(code => `pages/${code}`),
11646
+ ];
11647
+ if (onlyTargets.length > 0) {
11648
+ commands.push(
11649
+ `openxiangda workspace publish --profile ${profileArg} --only ${onlyTargets.join(',')}${
11650
+ targets.resources ? ' --resources' : ''
11651
+ }`
11652
+ );
11653
+ } else if (targets.resources) {
11654
+ commands.push(`openxiangda resource publish --profile ${profileArg}`);
11655
+ }
11656
+ return commands;
11657
+ }
11658
+
11659
+ function buildWorkspaceReleasePlan(options = {}) {
11660
+ const configText = readWorkspaceConfigText();
11661
+ const runtimeMode = getWorkspaceRuntimeModeFromConfig(configText);
11662
+ const compatibility = getWorkspaceCompatibilityFromConfig(configText);
11663
+ const files = options.changed
11664
+ ? getWorkspaceChangedFiles({ since: options.since })
11665
+ : getAllWorkspaceSourceFiles();
11666
+ const targets = classifyWorkspaceFiles(files, runtimeMode);
11667
+ const issues = scanWorkspaceReliabilityIssues(files);
11668
+ const errors = issues
11669
+ .filter(issue => issue.severity === 'error')
11670
+ .map(issue => ({
11671
+ name: issue.rule,
11672
+ message: `${issue.file}:${issue.line} ${issue.message}`,
11673
+ }));
11674
+ const commands = buildWorkspaceReleaseCommands(targets, runtimeMode, options.profile);
11675
+ const sdd = options.skipSdd
11676
+ ? null
11677
+ : buildSddGovernanceReport({
11678
+ cwd: process.cwd(),
11679
+ configText,
11680
+ targets,
11681
+ files,
11682
+ });
11683
+ const sddErrors = sdd ? sdd.errors.map(error => ({ name: error.name, message: error.message })) : [];
11684
+ const checks = [
11685
+ {
11686
+ name: 'workspace-config',
11687
+ status: configText ? 'ok' : 'warn',
11688
+ message: configText ? undefined : '未发现 app-workspace.config.ts',
11689
+ },
11690
+ {
11691
+ name: 'api-contracts',
11692
+ status: compatibility.apiContracts === 'strict' ? 'ok' : 'warn',
11693
+ message:
11694
+ compatibility.apiContracts === 'strict'
11695
+ ? undefined
11696
+ : '当前工作区处于 legacy API 契约模式',
11697
+ },
11698
+ {
11699
+ name: 'legacy-fallbacks',
11700
+ status: compatibility.legacyFallbacks ? 'warn' : 'ok',
11701
+ message: compatibility.legacyFallbacks ? 'legacyFallbacks 已开启,可能掩盖接口/schema 问题' : undefined,
11702
+ },
11703
+ {
11704
+ name: 'ai-reliability',
11705
+ status: errors.length > 0 ? 'error' : issues.length > 0 ? 'warn' : 'ok',
11706
+ message: issues.length > 0 ? `${issues.length} 个风险项` : undefined,
11707
+ },
11708
+ ...(sdd ? sdd.checks : []),
11709
+ ];
11710
+
11711
+ return {
11712
+ workspace: {
11713
+ cwd: process.cwd(),
11714
+ runtimeMode,
11715
+ configFile: path.join(process.cwd(), 'app-workspace.config.ts'),
11716
+ compatibility,
11717
+ },
11718
+ mode: options.changed ? 'changed' : 'all',
11719
+ since: options.changed ? options.since || undefined : undefined,
11720
+ changedFiles: options.changed ? files : undefined,
11721
+ targets,
11722
+ publishOrder:
11723
+ runtimeMode === 'react-spa'
11724
+ ? ['forms', 'resources', 'runtime']
11725
+ : ['forms/pages', 'resources'],
11726
+ commands,
11727
+ checks,
11728
+ sdd,
11729
+ issues,
11730
+ errors: [...errors, ...sddErrors],
11731
+ };
11732
+ }
11733
+
11734
+ function printWorkspaceReleasePlan(plan) {
11735
+ const lines = [
11736
+ `OpenXiangda workspace ${plan.mode} plan`,
11737
+ `runtime: ${plan.workspace.runtimeMode}`,
11738
+ `compatibility: apiContracts=${plan.workspace.compatibility.apiContracts}, legacyFallbacks=${plan.workspace.compatibility.legacyFallbacks}`,
11739
+ `forms: ${plan.targets.forms.length ? plan.targets.forms.join(', ') : '(none)'}`,
11740
+ `pages: ${plan.targets.pages.length ? plan.targets.pages.join(', ') : '(none)'}`,
11741
+ `resources: ${plan.targets.resources ? 'yes' : 'no'}`,
11742
+ `runtime deploy: ${plan.targets.runtime ? 'yes' : 'no'}`,
11743
+ ];
11744
+ if (plan.commands.length > 0) {
11745
+ lines.push('', 'Commands:', ...plan.commands.map(command => ` ${command}`));
11746
+ }
11747
+ if (plan.issues.length > 0) {
11748
+ lines.push('', 'Reliability issues:');
11749
+ for (const issue of plan.issues) {
11750
+ lines.push(`- [${issue.severity}] ${issue.file}:${issue.line} ${issue.message}`);
11751
+ }
11752
+ }
11753
+ if (plan.sdd) {
11754
+ lines.push(
11755
+ '',
11756
+ `SDD: enabled=${plan.sdd.governance.enabled ? 'true' : 'false'}, initialized=${plan.sdd.governance.initialized ? 'true' : 'false'}, strictHighRisk=${plan.sdd.governance.strictHighRisk ? 'true' : 'false'}, activeChanges=${plan.sdd.activeChanges.length}`
11757
+ );
11758
+ if (plan.sdd.highRiskFiles.length > 0) {
11759
+ lines.push(`high-risk files: ${plan.sdd.highRiskFiles.join(', ')}`);
11760
+ }
11761
+ }
11762
+ for (const check of plan.checks) {
11763
+ const suffix = check.message ? ` - ${check.message}` : '';
11764
+ lines.push(`- ${check.name}: ${check.status}${suffix}`);
11765
+ }
11766
+ print(lines.join('\n'));
11767
+ }
11768
+
11769
+ function buildExplicitSddReleasePlan(targets, options = {}) {
11770
+ const configText = readWorkspaceConfigText();
11771
+ const sdd = buildSddGovernanceReport({
11772
+ cwd: process.cwd(),
11773
+ configText,
11774
+ targets,
11775
+ files: options.files || [],
11776
+ });
11777
+ return {
11778
+ targets,
11779
+ sdd,
11780
+ errors: sdd.errors.map(error => ({ name: error.name, message: error.message })),
11781
+ };
11782
+ }
11783
+
11784
+ function assertSddReleaseGate(plan, flags, command) {
11785
+ if (!plan?.sdd || plan.sdd.errors.length === 0) return;
11786
+ if (flags['sdd-bypass']) {
11787
+ const bypass = appendBypassLog({
11788
+ cwd: process.cwd(),
11789
+ configText: readWorkspaceConfigText(),
11790
+ command,
11791
+ reason: flags.reason,
11792
+ targets: plan.targets,
11793
+ });
11794
+ warn(`SDD gate 已通过 bypass 放行: ${bypass.file}`);
11795
+ return;
11796
+ }
11797
+ const first = plan.sdd.errors[0];
11798
+ fail(`${first.message}\n如为紧急修复,可使用 --sdd-bypass --reason "..." 并在发布后补齐 SDD 归档。`);
11799
+ }
11800
+
11801
+ function buildWorkspacePublishSddPlan(publishOptions, runtimeMode, profile) {
11802
+ if (publishOptions.changed || (!publishOptions.targetForm && !publishOptions.targetPage && !publishOptions.only)) {
11803
+ const plan = buildWorkspaceReleasePlan({
11804
+ changed: publishOptions.changed,
11805
+ since: publishOptions.since,
11806
+ profile,
11807
+ });
11808
+ if (publishOptions.includeResources) {
11809
+ plan.targets.resources = true;
11810
+ }
11811
+ return plan;
11812
+ }
11813
+ const targets = {
11814
+ forms: [],
11815
+ pages: [],
11816
+ resources: Boolean(publishOptions.includeResources),
11817
+ runtime: false,
11818
+ other: [],
11819
+ };
11820
+ if (publishOptions.targetForm) targets.forms.push(publishOptions.targetForm);
11821
+ if (publishOptions.targetPage) {
11822
+ if (runtimeMode === 'react-spa') targets.runtime = true;
11823
+ else targets.pages.push(publishOptions.targetPage);
11824
+ }
11825
+ if (publishOptions.only) {
11826
+ for (const item of publishOptions.only.split(',').map(value => value.trim()).filter(Boolean)) {
11827
+ const formMatch = item.match(/^forms\/([^/]+)$/);
11828
+ const pageMatch = item.match(/^pages\/([^/]+)$/);
11829
+ if (formMatch) targets.forms.push(formMatch[1]);
11830
+ else if (pageMatch) targets.pages.push(pageMatch[1]);
11831
+ }
11832
+ }
11833
+ targets.forms = unique(targets.forms).sort();
11834
+ targets.pages = unique(targets.pages).sort();
11835
+ const commands = buildWorkspaceReleaseCommands(targets, runtimeMode, profile);
11836
+ const sddPlan = buildExplicitSddReleasePlan(targets, { files: [] });
11837
+ return {
11838
+ workspace: {
11839
+ cwd: process.cwd(),
11840
+ runtimeMode,
11841
+ configFile: path.join(process.cwd(), 'app-workspace.config.ts'),
11842
+ compatibility: getWorkspaceCompatibilityFromConfig(readWorkspaceConfigText()),
11843
+ },
11844
+ mode: 'targeted',
11845
+ targets,
11846
+ publishOrder:
11847
+ runtimeMode === 'react-spa'
11848
+ ? ['forms', 'resources', 'runtime']
11849
+ : ['forms/pages', 'resources'],
11850
+ commands,
11851
+ checks: sddPlan.sdd.checks,
11852
+ sdd: sddPlan.sdd,
11853
+ issues: [],
11854
+ errors: sddPlan.errors,
11855
+ };
11856
+ }
11857
+
10929
11858
  function isUnauthorized(error) {
10930
11859
  return (
10931
11860
  Number(error?.apiCode) === 401 ||
@@ -11155,8 +12084,10 @@ function normalizeWorkspacePublishOptions(flags) {
11155
12084
  targetPage,
11156
12085
  only,
11157
12086
  changed,
12087
+ since,
11158
12088
  legacyFormBundle: Boolean(flags['legacy-form-bundle']),
11159
12089
  includeResources,
12090
+ prune,
11160
12091
  quietResourceSkip: !targeted || skipResources,
11161
12092
  };
11162
12093
  }