openxiangda 1.0.144 → 1.0.146

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 (3) hide show
  1. package/lib/cli.js +124 -24
  2. package/lib/sdd.js +53 -5
  3. package/package.json +1 -1
package/lib/cli.js CHANGED
@@ -542,7 +542,7 @@ async function sdd(args) {
542
542
  '说明:',
543
543
  ' - SDD 记录保存在 openspec/,不依赖官方 OpenSpec CLI。',
544
544
  ' - 新工作区使用 openxiangda-sdd-v2:proposal/specs/design/tasks/evidence 给人读,coverage.json/release.json 给 CLI 校验。',
545
- ' - 高风险发布需要 approved change 的 coverage 覆盖本次 forms/pages/resources/runtime 目标和高风险文件。',
545
+ ' - 高风险发布需要 approved change 的 coverage 覆盖本次 forms/pages/functions/automations/workflows/JS_CODE/resources/runtime 目标和高风险文件。',
546
546
  ].join('\n'));
547
547
  return;
548
548
  }
@@ -624,17 +624,22 @@ async function sdd(args) {
624
624
 
625
625
  if (subcommand === 'verify') {
626
626
  const changeId = positional[0] || flags.change;
627
- const releasePlan = buildWorkspaceReleasePlan({
628
- changed: flags.changed !== false,
629
- since: flags.since,
630
- profile: flags.profile || '<profile>',
631
- skipSdd: true,
632
- });
627
+ const hasExplicitReleaseScope = Boolean(
628
+ flags.changed || flags['changed-only'] || flags.since || flags.all || flags['no-changed']
629
+ );
630
+ const releasePlan = hasExplicitReleaseScope
631
+ ? buildWorkspaceReleasePlan({
632
+ changed: Boolean(flags.changed || flags['changed-only'] || flags.since) && flags.changed !== false,
633
+ since: flags.since,
634
+ profile: flags.profile || '<profile>',
635
+ skipSdd: true,
636
+ })
637
+ : null;
633
638
  const result = verifySddChange({
634
639
  cwd: process.cwd(),
635
640
  changeId,
636
641
  releasePlan,
637
- writeMetadata: true,
642
+ writeMetadata: Boolean(releasePlan),
638
643
  });
639
644
  if (flags.json) {
640
645
  writeJson(result);
@@ -901,7 +906,7 @@ function printDoctorReport(result) {
901
906
  }
902
907
  if (result.release) {
903
908
  lines.push(
904
- `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'}`
909
+ `release plan: forms=${result.release.targets.forms.length}, pages=${result.release.targets.pages.length}, functions=${(result.release.targets.functions || []).length}, resources=${result.release.targets.resources ? 'yes' : 'no'}, runtime=${result.release.targets.runtime ? 'yes' : 'no'}`
905
910
  );
906
911
  }
907
912
  if (result.sdd) {
@@ -4677,7 +4682,8 @@ async function runtime(args) {
4677
4682
  ' openxiangda runtime releases --profile <name>',
4678
4683
  '说明:',
4679
4684
  ' - deploy 上传当前工作区 dist/ 并激活 React SPA runtime release。',
4680
- ' - 默认 --upload-mode auto;multipart 上传遇到 403 时自动切换到阿里 OSS 直传。',
4685
+ ' - 默认 --upload-mode auto;multipart 上传遇到 403 时自动切换到平台内置对象存储直传。',
4686
+ ' - --upload-mode oss-direct 是兼容参数名,实际 provider 由平台内置存储配置决定。',
4681
4687
  ' - 如果平台服务较旧不支持分片上传,可临时使用 --upload-mode legacy-json。',
4682
4688
  ' - runtime 只发布前端产物;表单、流程、通知等资源仍走 resource publish。',
4683
4689
  ].join('\n'));
@@ -4747,8 +4753,12 @@ async function runtime(args) {
4747
4753
  assetBaseUrl = plan.assetBaseUrl;
4748
4754
  if (uploadMode === 'auto') {
4749
4755
  effectiveUploadMode = 'oss-direct';
4756
+ const storageProvider = resolveRuntimeStorageProvider(
4757
+ plan.storageProvider,
4758
+ plan.provider
4759
+ );
4750
4760
  printRuntimeProgress(
4751
- `runtime builtin OSS available; using OSS direct upload traceId=${traceId}`
4761
+ `runtime builtin storage available; using ${formatRuntimeStorageProvider(storageProvider)} direct upload traceId=${traceId}`
4752
4762
  );
4753
4763
  }
4754
4764
  }
@@ -4759,7 +4769,7 @@ async function runtime(args) {
4759
4769
  effectiveUploadMode = 'auto';
4760
4770
  assetBaseUrl = gatewayAssetBaseUrl;
4761
4771
  printRuntimeProgress(
4762
- `runtime builtin OSS preflight unavailable; using staged upload first traceId=${traceId}`
4772
+ `runtime builtin storage preflight unavailable; using staged upload first traceId=${traceId}`
4763
4773
  );
4764
4774
  }
4765
4775
  }
@@ -4816,7 +4826,7 @@ async function runtime(args) {
4816
4826
  }
4817
4827
  effectiveUploadMode = 'oss-direct';
4818
4828
  printRuntimeProgress(
4819
- `runtime staged upload blocked by HTTP 403; fallback to OSS direct upload traceId=${traceId}`
4829
+ `runtime staged upload blocked by HTTP 403; fallback to builtin storage direct upload traceId=${traceId}`
4820
4830
  );
4821
4831
  releaseFiles = await uploadRuntimeDistFilesOssDirect({
4822
4832
  config,
@@ -4858,6 +4868,10 @@ async function runtime(args) {
4858
4868
  assetBaseUrl: releaseAssetBaseUrl,
4859
4869
  activated: !flags['no-activate'],
4860
4870
  uploadMode: effectiveUploadMode,
4871
+ storageProvider:
4872
+ data?.storageProvider ||
4873
+ releaseFiles.find(file => file.storageProvider)?.storageProvider ||
4874
+ null,
4861
4875
  traceId,
4862
4876
  };
4863
4877
  if (flags.json) return writeJson(result);
@@ -5014,6 +5028,10 @@ async function uploadRuntimeDistFilesOssDirect(options) {
5014
5028
  );
5015
5029
  const uploadFiles = Array.isArray(plan?.files) ? plan.files : [];
5016
5030
  const uploadByPath = new Map(uploadFiles.map(file => [file.path, file]));
5031
+ const planStorageProvider = resolveRuntimeStorageProvider(
5032
+ plan?.storageProvider,
5033
+ plan?.provider
5034
+ );
5017
5035
  let completed = 0;
5018
5036
  const results = await runWithConcurrency(
5019
5037
  files,
@@ -5021,14 +5039,19 @@ async function uploadRuntimeDistFilesOssDirect(options) {
5021
5039
  async file => {
5022
5040
  const uploadInfo = uploadByPath.get(file.path);
5023
5041
  if (!uploadInfo?.uploadUrl) {
5024
- fail(`runtime OSS 上传计划缺少文件: ${file.path}`);
5042
+ fail(`runtime 内置对象存储上传计划缺少文件: ${file.path}`);
5025
5043
  }
5044
+ const storageProvider = resolveRuntimeStorageProvider(
5045
+ uploadInfo.storageProvider,
5046
+ uploadInfo.provider,
5047
+ planStorageProvider
5048
+ );
5026
5049
  const uploaded = await uploadRuntimeDistFileToSignedUrl(options, file, uploadInfo);
5027
5050
  completed += 1;
5028
5051
  printRuntimeProgress(
5029
- `runtime OSS file uploaded [${completed}/${files.length}] ${file.path} ${formatBytes(file.size)} traceId=${options.traceId}`
5052
+ `runtime ${formatRuntimeStorageProvider(storageProvider)} file uploaded [${completed}/${files.length}] ${file.path} ${formatBytes(file.size)} traceId=${options.traceId}`
5030
5053
  );
5031
- return uploaded;
5054
+ return { ...uploaded, storageProvider };
5032
5055
  }
5033
5056
  );
5034
5057
  return results.map(file => ({
@@ -5036,7 +5059,7 @@ async function uploadRuntimeDistFilesOssDirect(options) {
5036
5059
  size: file.size,
5037
5060
  sha256: file.sha256,
5038
5061
  contentType: file.contentType,
5039
- storageProvider: 'oss',
5062
+ storageProvider: resolveRuntimeStorageProvider(file.storageProvider),
5040
5063
  objectName: file.objectName,
5041
5064
  bucketName: file.bucketName,
5042
5065
  }));
@@ -5056,16 +5079,16 @@ async function uploadRuntimeDistFileToSignedUrl(options, file, uploadInfo) {
5056
5079
  });
5057
5080
  } catch (error) {
5058
5081
  if (isAbortError(error)) {
5059
- throw new Error(`runtime OSS 文件上传超时: ${file.path}, timeout=${timeoutMs}ms`);
5082
+ throw new Error(`runtime 内置对象存储文件上传超时: ${file.path}, timeout=${timeoutMs}ms`);
5060
5083
  }
5061
- throw new Error(formatFetchError(error, `runtime OSS upload ${file.path}`));
5084
+ throw new Error(formatFetchError(error, `runtime builtin storage upload ${file.path}`));
5062
5085
  } finally {
5063
5086
  clearTimeout(timer);
5064
5087
  }
5065
5088
  if (!response.ok) {
5066
5089
  const text = await response.text().catch(() => '');
5067
5090
  throw new Error(
5068
- `runtime OSS 文件上传失败: ${file.path}, HTTP ${response.status}: ${
5091
+ `runtime 内置对象存储文件上传失败: ${file.path}, HTTP ${response.status}: ${
5069
5092
  text || response.statusText || 'request failed'
5070
5093
  }`
5071
5094
  );
@@ -5147,6 +5170,21 @@ function normalizeRuntimeUploadMode(value) {
5147
5170
  fail(`--upload-mode 只支持 auto、staged、oss-direct 或 legacy-json,当前: ${value}`);
5148
5171
  }
5149
5172
 
5173
+ function resolveRuntimeStorageProvider(...values) {
5174
+ for (const value of values) {
5175
+ const provider = String(value || '').trim().toLowerCase();
5176
+ if (provider) return provider;
5177
+ }
5178
+ return 'oss';
5179
+ }
5180
+
5181
+ function formatRuntimeStorageProvider(value) {
5182
+ const provider = resolveRuntimeStorageProvider(value);
5183
+ if (provider === 'oss') return 'OSS';
5184
+ if (provider === 'cos') return 'COS';
5185
+ return provider.toUpperCase();
5186
+ }
5187
+
5150
5188
  function isRuntimeStagedUploadBlocked(error) {
5151
5189
  return /^HTTP 403\b/.test(String(error?.message || ''));
5152
5190
  }
@@ -12493,12 +12531,35 @@ function getAllWorkspaceSourceFiles() {
12493
12531
  return files.sort();
12494
12532
  }
12495
12533
 
12534
+ function addCodeResourceTargetFromPath(targets, file, prefix, key) {
12535
+ if (!file.startsWith(prefix)) return false;
12536
+ const rest = file.slice(prefix.length);
12537
+ const first = rest.split('/').filter(Boolean)[0] || '';
12538
+ const code = first.replace(/\.json$/i, '');
12539
+ if (code) targets[key].add(code);
12540
+ return true;
12541
+ }
12542
+
12543
+ function isRuntimeSourceFile(file) {
12544
+ return /\.(ts|tsx|js|jsx|mjs|cjs|css|scss|sass|less|html|json|svg|png|jpe?g|gif|webp|avif|ico)$/i.test(file);
12545
+ }
12546
+
12496
12547
  function classifyWorkspaceFiles(files, runtimeMode) {
12497
12548
  const forms = new Set();
12498
12549
  const pages = new Set();
12550
+ const functions = new Set();
12551
+ const automations = new Set();
12552
+ const workflows = new Set();
12553
+ const jsCodeNodes = new Set();
12499
12554
  let resources = false;
12500
12555
  let runtime = false;
12501
12556
  const other = [];
12557
+ const codeResourceTargets = {
12558
+ functions,
12559
+ automations,
12560
+ workflows,
12561
+ jsCodeNodes,
12562
+ };
12502
12563
 
12503
12564
  for (const file of files) {
12504
12565
  const formMatch = file.match(/^src\/forms\/([^/]+)/);
@@ -12515,6 +12576,30 @@ function classifyWorkspaceFiles(files, runtimeMode) {
12515
12576
  }
12516
12577
  continue;
12517
12578
  }
12579
+ if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/functions/', 'functions')) {
12580
+ continue;
12581
+ }
12582
+ if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/automations/', 'automations')) {
12583
+ continue;
12584
+ }
12585
+ if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/workflows/', 'workflows')) {
12586
+ continue;
12587
+ }
12588
+ if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/js-code-nodes/', 'jsCodeNodes')) {
12589
+ continue;
12590
+ }
12591
+ if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/resources/functions/', 'functions')) {
12592
+ continue;
12593
+ }
12594
+ if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/resources/automations/', 'automations')) {
12595
+ continue;
12596
+ }
12597
+ if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/resources/workflows/', 'workflows')) {
12598
+ continue;
12599
+ }
12600
+ if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/resources/js-code-nodes/', 'jsCodeNodes')) {
12601
+ continue;
12602
+ }
12518
12603
  if (file.startsWith('src/resources/')) {
12519
12604
  resources = true;
12520
12605
  continue;
@@ -12523,7 +12608,8 @@ function classifyWorkspaceFiles(files, runtimeMode) {
12523
12608
  file === 'package.json' ||
12524
12609
  file === 'vite.config.ts' ||
12525
12610
  file === 'index.html' ||
12526
- file.startsWith('src/')
12611
+ file.startsWith('src/runtime/') ||
12612
+ (file.startsWith('src/') && isRuntimeSourceFile(file))
12527
12613
  ) {
12528
12614
  runtime = runtimeMode === 'react-spa' || runtime;
12529
12615
  continue;
@@ -12534,6 +12620,10 @@ function classifyWorkspaceFiles(files, runtimeMode) {
12534
12620
  return {
12535
12621
  forms: Array.from(forms).sort(),
12536
12622
  pages: Array.from(pages).sort(),
12623
+ functions: Array.from(functions).sort(),
12624
+ automations: Array.from(automations).sort(),
12625
+ workflows: Array.from(workflows).sort(),
12626
+ jsCodeNodes: Array.from(jsCodeNodes).sort(),
12537
12627
  resources,
12538
12628
  runtime,
12539
12629
  other,
@@ -12621,11 +12711,17 @@ function scanWorkspaceReliabilityIssues(files) {
12621
12711
  function buildWorkspaceReleaseCommands(targets, runtimeMode, profile) {
12622
12712
  const profileArg = profile || '<profile>';
12623
12713
  const commands = [];
12714
+ const hasResourceTargets =
12715
+ Boolean(targets.resources) ||
12716
+ (targets.functions || []).length > 0 ||
12717
+ (targets.automations || []).length > 0 ||
12718
+ (targets.workflows || []).length > 0 ||
12719
+ (targets.jsCodeNodes || []).length > 0;
12624
12720
  if (runtimeMode === 'react-spa') {
12625
12721
  for (const formCode of targets.forms) {
12626
12722
  commands.push(`openxiangda workspace publish --profile ${profileArg} --form ${formCode}`);
12627
12723
  }
12628
- if (targets.resources) {
12724
+ if (hasResourceTargets) {
12629
12725
  commands.push(`openxiangda resource publish --profile ${profileArg}`);
12630
12726
  }
12631
12727
  if (targets.runtime) {
@@ -12641,10 +12737,10 @@ function buildWorkspaceReleaseCommands(targets, runtimeMode, profile) {
12641
12737
  if (onlyTargets.length > 0) {
12642
12738
  commands.push(
12643
12739
  `openxiangda workspace publish --profile ${profileArg} --only ${onlyTargets.join(',')}${
12644
- targets.resources ? ' --resources' : ''
12740
+ hasResourceTargets ? ' --resources' : ''
12645
12741
  }`
12646
12742
  );
12647
- } else if (targets.resources) {
12743
+ } else if (hasResourceTargets) {
12648
12744
  commands.push(`openxiangda resource publish --profile ${profileArg}`);
12649
12745
  }
12650
12746
  return commands;
@@ -12734,6 +12830,10 @@ function printWorkspaceReleasePlan(plan) {
12734
12830
  `compatibility: apiContracts=${plan.workspace.compatibility.apiContracts}, legacyFallbacks=${plan.workspace.compatibility.legacyFallbacks}`,
12735
12831
  `forms: ${plan.targets.forms.length ? plan.targets.forms.join(', ') : '(none)'}`,
12736
12832
  `pages: ${plan.targets.pages.length ? plan.targets.pages.join(', ') : '(none)'}`,
12833
+ `functions: ${(plan.targets.functions || []).length ? plan.targets.functions.join(', ') : '(none)'}`,
12834
+ `automations: ${(plan.targets.automations || []).length ? plan.targets.automations.join(', ') : '(none)'}`,
12835
+ `workflows: ${(plan.targets.workflows || []).length ? plan.targets.workflows.join(', ') : '(none)'}`,
12836
+ `jsCodeNodes: ${(plan.targets.jsCodeNodes || []).length ? plan.targets.jsCodeNodes.join(', ') : '(none)'}`,
12737
12837
  `resources: ${plan.targets.resources ? 'yes' : 'no'}`,
12738
12838
  `runtime deploy: ${plan.targets.runtime ? 'yes' : 'no'}`,
12739
12839
  ];
package/lib/sdd.js CHANGED
@@ -10,6 +10,7 @@ const RELEASE_FILE = 'release.json';
10
10
  const BYPASS_LOG_FILE = path.join('changes', 'bypass-log.md');
11
11
  const SDD_SCHEMA_VERSION = 'openxiangda-sdd-v2';
12
12
  const LEGACY_SDD_SCHEMA_VERSION = 'openxiangda-sdd-v1';
13
+ const CODE_RESOURCE_TARGETS = ['functions', 'automations', 'workflows', 'jsCodeNodes'];
13
14
 
14
15
  const DEFAULT_SPEC_CONTENT = `# Application Specification
15
16
 
@@ -310,6 +311,17 @@ function normalizeAffected(input = {}) {
310
311
  };
311
312
  }
312
313
 
314
+ function extractCodeFromPath(file, prefix) {
315
+ if (!file.startsWith(prefix)) return null;
316
+ const rest = file.slice(prefix.length);
317
+ const first = rest.split('/').filter(Boolean)[0] || '';
318
+ return first.replace(/\.json$/i, '') || null;
319
+ }
320
+
321
+ function coversCodeTarget(normalized, key, code) {
322
+ return Boolean(normalized.resources || normalized.backend || includesAll(normalized[key], [code]));
323
+ }
324
+
313
325
  function createChangeMetadata(changeId, options = {}) {
314
326
  return {
315
327
  schemaVersion: SDD_SCHEMA_VERSION,
@@ -501,6 +513,10 @@ function createReleaseManifest(meta) {
501
513
  targets: {
502
514
  forms: meta.affected.forms,
503
515
  pages: meta.affected.pages,
516
+ functions: meta.affected.functions,
517
+ automations: meta.affected.automations,
518
+ workflows: meta.affected.workflows,
519
+ jsCodeNodes: meta.affected.jsCodeNodes,
504
520
  resources: meta.affected.resources,
505
521
  runtime: meta.affected.runtime,
506
522
  other: [],
@@ -613,6 +629,7 @@ function hasHighRiskTargets(targets = {}) {
613
629
  return (
614
630
  (targets.forms || []).length > 0 ||
615
631
  (targets.pages || []).length > 0 ||
632
+ CODE_RESOURCE_TARGETS.some(key => (targets[key] || []).length > 0) ||
616
633
  Boolean(targets.resources) ||
617
634
  Boolean(targets.runtime)
618
635
  );
@@ -647,6 +664,12 @@ function affectedCoversTargets(affected, targets = {}) {
647
664
  if (!includesAll(normalized.pages, targets.pages || [])) {
648
665
  missing.push(`pages:${(targets.pages || []).filter(item => !normalized.pages.includes(item)).join(',')}`);
649
666
  }
667
+ for (const key of CODE_RESOURCE_TARGETS) {
668
+ const required = targets[key] || [];
669
+ if (required.length === 0) continue;
670
+ if (normalized.resources || normalized.backend || includesAll(normalized[key], required)) continue;
671
+ missing.push(`${key}:${required.filter(item => !normalized[key].includes(item)).join(',')}`);
672
+ }
650
673
  if (targets.resources && !normalized.resources && !normalized.backend) missing.push('resources');
651
674
  if (targets.runtime && !normalized.runtime) missing.push('runtime');
652
675
  return {
@@ -668,6 +691,18 @@ function affectedCoversHighRiskFiles(affected, files = []) {
668
691
  } else if (file.startsWith('src/pages/')) {
669
692
  const pageCode = file.split('/')[2];
670
693
  if (!normalized.runtime && !includesAll(normalized.pages, [pageCode])) missing.push(file);
694
+ } else if (file.startsWith('src/resources/functions/')) {
695
+ const code = extractCodeFromPath(file, 'src/resources/functions/');
696
+ if (code && !coversCodeTarget(normalized, 'functions', code)) missing.push(file);
697
+ } else if (file.startsWith('src/resources/automations/')) {
698
+ const code = extractCodeFromPath(file, 'src/resources/automations/');
699
+ if (code && !coversCodeTarget(normalized, 'automations', code)) missing.push(file);
700
+ } else if (file.startsWith('src/resources/workflows/')) {
701
+ const code = extractCodeFromPath(file, 'src/resources/workflows/');
702
+ if (code && !coversCodeTarget(normalized, 'workflows', code)) missing.push(file);
703
+ } else if (file.startsWith('src/resources/js-code-nodes/')) {
704
+ const code = extractCodeFromPath(file, 'src/resources/js-code-nodes/');
705
+ if (code && !coversCodeTarget(normalized, 'jsCodeNodes', code)) missing.push(file);
671
706
  } else if (file.startsWith('src/resources/') && !normalized.resources) {
672
707
  missing.push(file);
673
708
  } else if (file.startsWith('src/functions/')) {
@@ -1116,8 +1151,8 @@ function validateDeltaSpecs(changeDir) {
1116
1151
  };
1117
1152
  }
1118
1153
 
1119
- function updateReleaseManifest(loaded, releasePlan) {
1120
- const release = {
1154
+ function buildUpdatedReleaseManifest(loaded, releasePlan) {
1155
+ return {
1121
1156
  schemaVersion: SDD_SCHEMA_VERSION,
1122
1157
  changeId: loaded.changeId,
1123
1158
  ...(loaded.release && !loaded.release.__invalid ? loaded.release : {}),
@@ -1126,6 +1161,10 @@ function updateReleaseManifest(loaded, releasePlan) {
1126
1161
  commands: releasePlan?.commands || loaded.release?.commands || [],
1127
1162
  verifiedAt: nowIso(),
1128
1163
  };
1164
+ }
1165
+
1166
+ function updateReleaseManifest(loaded, releasePlan) {
1167
+ const release = buildUpdatedReleaseManifest(loaded, releasePlan);
1129
1168
  writeJsonFile(loaded.releaseFile, release);
1130
1169
  return release;
1131
1170
  }
@@ -1210,11 +1249,12 @@ function verifySddChange(options = {}) {
1210
1249
  }
1211
1250
  const release =
1212
1251
  options.writeMetadata && options.releasePlan && loaded.release && !loaded.release.__invalid
1213
- ? updateReleaseManifest(loaded, options.releasePlan)
1252
+ ? buildUpdatedReleaseManifest(loaded, options.releasePlan)
1214
1253
  : loaded.release;
1215
1254
  const releaseValidation = validateReleaseManifest(release, options.releasePlan, legacyMode);
1216
1255
  errors.push(...releaseValidation.errors);
1217
1256
  warnings.push(...releaseValidation.warnings);
1257
+ const passed = errors.length === 0;
1218
1258
  const verifiedMeta = {
1219
1259
  ...loaded.meta,
1220
1260
  release: {
@@ -1224,7 +1264,14 @@ function verifySddChange(options = {}) {
1224
1264
  },
1225
1265
  updatedAt: nowIso(),
1226
1266
  };
1227
- if (options.writeMetadata) writeJsonFile(loaded.metaFile, verifiedMeta);
1267
+ let metadataWritten = false;
1268
+ if (options.writeMetadata && passed) {
1269
+ if (options.releasePlan && loaded.release && !loaded.release.__invalid) {
1270
+ updateReleaseManifest(loaded, options.releasePlan);
1271
+ }
1272
+ writeJsonFile(loaded.metaFile, verifiedMeta);
1273
+ metadataWritten = true;
1274
+ }
1228
1275
  return {
1229
1276
  schemaVersion: loaded.meta.schemaVersion || loaded.governance.schemaVersion || LEGACY_SDD_SCHEMA_VERSION,
1230
1277
  change: verifiedMeta,
@@ -1234,7 +1281,8 @@ function verifySddChange(options = {}) {
1234
1281
  errors,
1235
1282
  warnings,
1236
1283
  blockingErrors: errors,
1237
- passed: errors.length === 0,
1284
+ passed,
1285
+ metadataWritten,
1238
1286
  targets,
1239
1287
  highRiskFiles,
1240
1288
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.144",
3
+ "version": "1.0.146",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {