openxiangda-devkit-core 2.0.0-alpha.114 → 2.0.0-alpha.117

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 (73) hide show
  1. package/README.md +3 -9
  2. package/dist/app-spec.d.ts +23 -11
  3. package/dist/app-spec.d.ts.map +1 -1
  4. package/dist/app-spec.js +171 -52
  5. package/dist/app-spec.js.map +1 -1
  6. package/dist/application-services.d.ts +344 -31
  7. package/dist/application-services.d.ts.map +1 -1
  8. package/dist/application-services.js +239 -80
  9. package/dist/application-services.js.map +1 -1
  10. package/dist/backend-image-build.js +6 -11
  11. package/dist/backend-image-build.js.map +1 -1
  12. package/dist/command-process.d.ts +14 -0
  13. package/dist/command-process.d.ts.map +1 -0
  14. package/dist/command-process.js +51 -0
  15. package/dist/command-process.js.map +1 -0
  16. package/dist/command-registry.d.ts +22 -2
  17. package/dist/command-registry.d.ts.map +1 -1
  18. package/dist/command-registry.js +7 -5
  19. package/dist/command-registry.js.map +1 -1
  20. package/dist/compiler/permission-review.d.ts.map +1 -1
  21. package/dist/configuration-preflight.d.ts +7 -0
  22. package/dist/configuration-preflight.d.ts.map +1 -0
  23. package/dist/configuration-preflight.js +13 -0
  24. package/dist/configuration-preflight.js.map +1 -0
  25. package/dist/control-plane-client.d.ts +8 -1
  26. package/dist/control-plane-client.d.ts.map +1 -1
  27. package/dist/control-plane-client.js +13 -0
  28. package/dist/control-plane-client.js.map +1 -1
  29. package/dist/delivery-cache.d.ts +34 -0
  30. package/dist/delivery-cache.d.ts.map +1 -0
  31. package/dist/delivery-cache.js +186 -0
  32. package/dist/delivery-cache.js.map +1 -0
  33. package/dist/delivery-source.d.ts +17 -0
  34. package/dist/delivery-source.d.ts.map +1 -0
  35. package/dist/delivery-source.js +85 -0
  36. package/dist/delivery-source.js.map +1 -0
  37. package/dist/deployment-observer.d.ts +14 -0
  38. package/dist/deployment-observer.d.ts.map +1 -0
  39. package/dist/deployment-observer.js +68 -0
  40. package/dist/deployment-observer.js.map +1 -0
  41. package/dist/deployment.d.ts +13 -0
  42. package/dist/deployment.d.ts.map +1 -1
  43. package/dist/deployment.js +34 -7
  44. package/dist/deployment.js.map +1 -1
  45. package/dist/developer-errors.d.ts +11 -0
  46. package/dist/developer-errors.d.ts.map +1 -0
  47. package/dist/developer-errors.js +16 -0
  48. package/dist/developer-errors.js.map +1 -0
  49. package/dist/developer-operations.d.ts +168 -0
  50. package/dist/developer-operations.d.ts.map +1 -0
  51. package/dist/developer-operations.js +58 -0
  52. package/dist/developer-operations.js.map +1 -0
  53. package/dist/development-lifecycle.d.ts +48 -0
  54. package/dist/development-lifecycle.d.ts.map +1 -0
  55. package/dist/development-lifecycle.js +180 -0
  56. package/dist/development-lifecycle.js.map +1 -0
  57. package/dist/documentation.d.ts +103 -0
  58. package/dist/documentation.d.ts.map +1 -0
  59. package/dist/documentation.js +111 -0
  60. package/dist/documentation.js.map +1 -0
  61. package/dist/index.d.ts +6 -0
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +6 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/operation-progress.d.ts +26 -0
  66. package/dist/operation-progress.d.ts.map +1 -0
  67. package/dist/operation-progress.js +92 -0
  68. package/dist/operation-progress.js.map +1 -0
  69. package/dist/workspace-operation.d.ts +3 -0
  70. package/dist/workspace-operation.d.ts.map +1 -0
  71. package/dist/workspace-operation.js +45 -0
  72. package/dist/workspace-operation.js.map +1 -0
  73. package/package.json +3 -3
@@ -1,5 +1,11 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { randomUUID } from "node:crypto";
2
+ import { publishedDeliverySource, assertDeliverySourceUnchanged, DeliverySourceError } from './delivery-source.js';
3
+ import { operationStage, skippedOperationStage, updateOperationStage } from './operation-progress.js';
4
+ import { deliveryInputDigest, reusableValidation, recordValidation, recordSealedCandidate, reusableSealedCandidate } from './delivery-cache.js';
5
+ import { developmentLifecycle, summarizeLifecycle, lifecycleAtCommit, verifyBusinessAcceptance } from './development-lifecycle.js';
6
+ import { runCommandProcess } from './command-process.js';
7
+ import { compileLocalConfiguration } from './configuration-preflight.js';
8
+ import { NativeConfigurationCompilerError, NATIVE_CONFIGURATION_VALIDATOR_DIGEST } from 'openxiangda-contracts/native-compiler';
3
9
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs";
4
10
  import { dirname, join, relative, resolve } from "node:path";
5
11
  import { CONFIGURATION_COMPATIBILITY_CAPABILITY, CURRENT_APPLICATION_CONTRACT, OPENXIANGDA_CONTRACT_VERSION, SCHEMA_VERSIONS, canonicalJson, sha256Digest, } from "openxiangda-contracts";
@@ -24,7 +30,15 @@ export class OpenXiangdaApplicationServices {
24
30
  }
25
31
  async workspaceContext(root) {
26
32
  const workspace = await this.workspace(root);
27
- return this.ok("workspace.context", workspace.context.workspace, workspace.context);
33
+ const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true));
34
+ return this.ok("workspace.context", workspace.context.workspace, { ...workspace.context, development: {
35
+ ...summarizeLifecycle(lifecycle),
36
+ application: lifecycle.context.application ? { path: lifecycle.context.application.path, content: lifecycle.context.application.content } : null,
37
+ records: [
38
+ ...lifecycle.context.index.capabilities, ...lifecycle.context.index.activeChanges,
39
+ ...lifecycle.context.index.decisions, ...lifecycle.context.index.history,
40
+ ].map(({ id, title, status, path, kind }) => ({ id, title, status, path, kind })),
41
+ } });
28
42
  }
29
43
  async appSpecInit(root) {
30
44
  const workspace = await this.workspace(root);
@@ -77,15 +91,16 @@ export class OpenXiangdaApplicationServices {
77
91
  },
78
92
  ]);
79
93
  }
80
- async appSpecContext(root, selector) {
94
+ async appSpecContext(root, selector, historyOffset = 0) {
81
95
  const workspace = await this.workspace(root);
82
- const context = inspectAppSpec(workspace.root, this.appSpecContractIndex(workspace, true), selector);
83
- return this.result("spec.context", workspace.context.workspace, context, advisoryAppSpecDiagnostics(context.diagnostics));
96
+ const context = inspectAppSpec(workspace.root, this.appSpecContractIndex(workspace, true), selector, historyOffset);
97
+ return this.result("spec.context", workspace.context.workspace, { ...context, lifecycle: summarizeLifecycle(developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true))) }, advisoryAppSpecDiagnostics(context.diagnostics));
84
98
  }
85
99
  async appSpecCheck(root) {
86
100
  const workspace = await this.workspace(root);
87
101
  const context = inspectAppSpec(workspace.root, this.appSpecContractIndex(workspace, true));
88
- return this.result("spec.check", workspace.context.workspace, context, context.diagnostics, [
102
+ const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true));
103
+ return this.result("spec.check", workspace.context.workspace, { ...context, lifecycle: summarizeLifecycle(lifecycle) }, lifecycle.diagnostics, [
89
104
  {
90
105
  code: "check",
91
106
  label: "继续运行应用检查",
@@ -93,6 +108,13 @@ export class OpenXiangdaApplicationServices {
93
108
  },
94
109
  ]);
95
110
  }
111
+ async appSpecVerify(root, deploymentId, evidencePath) {
112
+ const workspace = await this.workspace(root);
113
+ const run = await (await this.client(workspace.root)).deployment(workspace.config.app.code, deploymentId);
114
+ const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true));
115
+ const verification = verifyBusinessAcceptance(workspace.root, run, lifecycle, evidencePath);
116
+ return this.result('spec.verify', workspace.context.workspace, verification, verification.diagnostics);
117
+ }
96
118
  async appSpecClose(input) {
97
119
  const workspace = await this.workspace(input.root);
98
120
  const outcome = closeAppSpecChange({
@@ -180,6 +202,51 @@ export class OpenXiangdaApplicationServices {
180
202
  },
181
203
  });
182
204
  }
205
+ async contractContext(root, input = {}) {
206
+ const result = await this.contractDescribe(root);
207
+ const data = result.data;
208
+ const groups = {
209
+ resource: data.contract.resources,
210
+ operation: data.contract.operations,
211
+ workflow: data.contract.workflows,
212
+ route: data.contract.routes,
213
+ capability: data.aiCatalog.capabilities,
214
+ };
215
+ const entries = Object.entries(groups).flatMap(([kind, values]) => values.map(value => ({
216
+ selector: `${kind}:${String(value.code || value.resourceCode || value.id)}`,
217
+ kind, code: String(value.code || value.resourceCode || value.id),
218
+ title: String(value.name || value.label || value.title || value.code || value.resourceCode || value.id),
219
+ value,
220
+ })));
221
+ const selector = input.selector || 'index';
222
+ const offset = input.offset ?? 0;
223
+ const limit = input.limit ?? 30;
224
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
225
+ throw new Error('CONTRACT_SELECTION_INVALID: offset 必须为非负整数,limit 必须在 1 到 100 之间');
226
+ }
227
+ let selection;
228
+ if (selector === 'index')
229
+ selection = {
230
+ total: entries.length, offset, limit, nextOffset: offset + limit < entries.length ? offset + limit : null,
231
+ sections: ['navigation', 'permissions', 'all'],
232
+ entries: entries.slice(offset, offset + limit).map(({ value: _, ...entry }) => entry),
233
+ };
234
+ else if (selector === 'navigation')
235
+ selection = { adminNavigationAuthoring: data.adminNavigationAuthoring };
236
+ else if (selector === 'permissions')
237
+ selection = { permissionReview: data.permissionReview };
238
+ else if (selector === 'all')
239
+ selection = data;
240
+ else {
241
+ const entry = entries.find(item => item.selector === selector);
242
+ if (!entry)
243
+ throw new Error(`CONTRACT_SELECTION_NOT_FOUND: 未知契约选择器 ${selector},先读取 index`);
244
+ selection = entry.value;
245
+ }
246
+ if (Buffer.byteLength(JSON.stringify(selection)) > 256 * 1024)
247
+ throw new Error('CONTRACT_SELECTION_TOO_LARGE: 选择结果超过 256 KiB,请按模型或能力读取');
248
+ return { ...result, data: { configDigest: data.configDigest, contractDigest: data.contractDigest, aiCatalogDigest: data.aiCatalogDigest, counts: data.counts, selector, selection } };
249
+ }
183
250
  async platformCapabilities(root) {
184
251
  const workspace = await this.workspace(root);
185
252
  const capabilities = await (await this.client(workspace.root)).capabilities();
@@ -530,12 +597,14 @@ export class OpenXiangdaApplicationServices {
530
597
  async check(root, environmentKey) {
531
598
  const workspace = await this.workspace(root);
532
599
  const appSpec = inspectAppSpec(workspace.root, this.appSpecContractIndex(workspace, false));
600
+ const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, false));
533
601
  const diagnostics = [
534
602
  ...validateAppConfig(workspace.config),
535
603
  ...this.versionTrainDiagnostics(workspace),
536
604
  ...validateNestInjectionContract(workspace.root),
537
605
  ...validateApplicationUiContract(workspace.root, workspace.config.frontend.root),
538
606
  ...advisoryAppSpecDiagnostics(appSpec.diagnostics),
607
+ ...advisoryAppSpecDiagnostics(lifecycle.diagnostics),
539
608
  ];
540
609
  if (diagnostics.some(item => item.severity === "error")) {
541
610
  const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, false);
@@ -544,10 +613,28 @@ export class OpenXiangdaApplicationServices {
544
613
  compatibility: null,
545
614
  stages: [],
546
615
  sealedArtifact,
547
- appSpec: summarizeAppSpecContext(appSpec),
616
+ appSpec: { ...summarizeAppSpecContext(appSpec), lifecycle: summarizeLifecycle(lifecycle) },
548
617
  }, diagnostics);
549
618
  }
550
- const generated = await this.generate({ root: workspace.root });
619
+ let compatibility = null;
620
+ try {
621
+ const local = await operationStage('local-configuration', '本地完整配置校验', async () => compileLocalConfiguration(workspace.config, this.toolchainVersion));
622
+ if (environmentKey) {
623
+ compatibility = await operationStage('target-preflight', '目标平台只读预检', () => this.validateTargetConfigurationCompatibility(workspace, environmentKey, local));
624
+ }
625
+ }
626
+ catch (error) {
627
+ diagnostics.push(this.configurationCompatibilityDiagnostic(error));
628
+ const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, false);
629
+ return this.result("check", workspace.context.workspace, {
630
+ generated: undefined,
631
+ compatibility: null,
632
+ stages: [],
633
+ sealedArtifact,
634
+ appSpec: { ...summarizeAppSpecContext(appSpec), lifecycle: summarizeLifecycle(lifecycle) },
635
+ }, diagnostics);
636
+ }
637
+ const generated = await operationStage('generate', '生成应用契约', () => this.generate({ root: workspace.root }));
551
638
  diagnostics.push(...generated.diagnostics);
552
639
  if (!generated.ok) {
553
640
  const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, false);
@@ -556,38 +643,45 @@ export class OpenXiangdaApplicationServices {
556
643
  compatibility: null,
557
644
  stages: [],
558
645
  sealedArtifact,
559
- appSpec: summarizeAppSpecContext(appSpec),
646
+ appSpec: { ...summarizeAppSpecContext(appSpec), lifecycle: summarizeLifecycle(lifecycle) },
560
647
  }, diagnostics, generated.nextActions);
561
648
  }
562
649
  const backendDiagnostics = this.initializeBackend(workspace);
563
650
  if (backendDiagnostics.length) {
564
651
  return this.result("check", workspace.context.workspace, undefined, backendDiagnostics);
565
652
  }
566
- let compatibility = null;
567
- if (environmentKey) {
568
- try {
569
- compatibility = await this.validateTargetConfigurationCompatibility(workspace, environmentKey);
653
+ const stages = [];
654
+ const cache = this.deliveryCacheContext(workspace);
655
+ const inputDigest = deliveryInputDigest(cache);
656
+ const reusable = reusableValidation(cache, inputDigest);
657
+ for (const name of ["check", "test", "build"]) {
658
+ if (reusable) {
659
+ await operationStage(name, `${this.packageScriptLabel(name)}(复用已验证结果)`, async () => {
660
+ updateOperationStage(name, `${this.packageScriptLabel(name)}(复用已验证结果)`, { reused: true, inputDigest, outputDigest: reusable.outputDigest });
661
+ });
662
+ stages.push({ name, state: 'passed', ok: true, exitCode: 0, output: '输入与构建输出摘要未变,复用已通过的本地检查', reused: true });
663
+ continue;
570
664
  }
571
- catch (error) {
572
- diagnostics.push(this.configurationCompatibilityDiagnostic(error));
573
- const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, false);
574
- return this.result("check", workspace.context.workspace, {
575
- generated: generated.data,
576
- compatibility: null,
577
- stages: [],
578
- sealedArtifact,
579
- appSpec: summarizeAppSpecContext(appSpec),
580
- }, diagnostics);
665
+ if (stages.some(stage => stage.state === 'failed')) {
666
+ stages.push({ name, state: 'skipped', ok: false, exitCode: null, output: '前置阶段失败,未执行' });
667
+ skippedOperationStage(name, this.packageScriptLabel(name));
668
+ continue;
581
669
  }
670
+ const result = await this.runPackageScript(workspace.root, name);
671
+ stages.push({ name, ...result, state: result.ok ? 'passed' : 'failed' });
582
672
  }
583
- const stages = ["check", "test", "build"].map(name => ({
584
- name,
585
- ...this.runPackageScript(workspace.root, name),
586
- }));
587
- for (const stage of stages.filter(item => !item.ok)) {
673
+ for (const stage of stages.filter(item => item.state === 'failed')) {
588
674
  const code = `WORKSPACE_${stage.name.toUpperCase()}_FAILED`;
589
675
  diagnostics.push(this.diagnostic(code, `工作区 ${stage.name} 脚本失败`, `package.json#scripts.${stage.name}`, "修复失败后重新运行 openxiangda check", { output: stage.output }));
590
676
  }
677
+ if (!reusable && stages.every(stage => stage.state === 'passed')) {
678
+ try {
679
+ recordValidation(cache, inputDigest);
680
+ }
681
+ catch (error) {
682
+ diagnostics.push(this.diagnostic('OPENXIANGDA_VALIDATION_INPUT_CHANGED', error.message, 'workspace', '等待源码修改结束后重新运行 openxiangda check'));
683
+ }
684
+ }
591
685
  const passed = !diagnostics.some(item => item.severity === "error");
592
686
  const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, passed);
593
687
  return this.result("check", workspace.context.workspace, {
@@ -595,7 +689,7 @@ export class OpenXiangdaApplicationServices {
595
689
  compatibility,
596
690
  stages,
597
691
  sealedArtifact,
598
- appSpec: summarizeAppSpecContext(appSpec),
692
+ appSpec: { ...summarizeAppSpecContext(appSpec), lifecycle: summarizeLifecycle(lifecycle) },
599
693
  }, diagnostics, passed
600
694
  ? [
601
695
  {
@@ -612,7 +706,7 @@ export class OpenXiangdaApplicationServices {
612
706
  if (diagnostics.length > 0) {
613
707
  return this.result("test", workspace.context.workspace, undefined, diagnostics);
614
708
  }
615
- const command = this.runPackageScript(workspace.root, "test");
709
+ const command = await this.runPackageScript(workspace.root, "test");
616
710
  const commandDiagnostics = command.ok
617
711
  ? []
618
712
  : [
@@ -662,7 +756,7 @@ export class OpenXiangdaApplicationServices {
662
756
  if (backendDiagnostics.length) {
663
757
  return this.result("build", workspace.context.workspace, undefined, backendDiagnostics);
664
758
  }
665
- const command = this.runPackageScript(workspace.root, "build");
759
+ const command = await this.runPackageScript(workspace.root, "build");
666
760
  if (!command.ok) {
667
761
  diagnostics.push(this.diagnostic("WORKSPACE_BUILD_FAILED", "前后端生产构建失败", "package.json#scripts.build", "修复构建错误后重试", { output: command.output }));
668
762
  return this.result("build", workspace.context.workspace, undefined, diagnostics);
@@ -782,6 +876,10 @@ export class OpenXiangdaApplicationServices {
782
876
  if (diagnostics.length > 0) {
783
877
  return this.result("deployment.plan", workspace.context.workspace, undefined, diagnostics);
784
878
  }
879
+ const source = await operationStage('source', '核对远端主线与源码', () => publishedDeliverySource(workspace.root));
880
+ const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true));
881
+ if (!lifecycle.readyForTest)
882
+ return this.result('deployment.plan', workspace.context.workspace, { lifecycle: summarizeLifecycle(lifecycle) }, lifecycle.diagnostics);
785
883
  const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
786
884
  return this.ok("deployment.plan", workspace.context.workspace, {
787
885
  environment: input.environment,
@@ -789,6 +887,7 @@ export class OpenXiangdaApplicationServices {
789
887
  sealed: false,
790
888
  packageDigest: null,
791
889
  buildOwner: "openxiangda deploy",
890
+ source,
792
891
  frontendRoot: workspace.config.frontend.root,
793
892
  backendRoot: workspace.config.backend.root,
794
893
  configDigest: sources.config.digest,
@@ -806,6 +905,7 @@ export class OpenXiangdaApplicationServices {
806
905
  this.diagnostic("DEPLOY_PREPRODUCTION_ONLY", "Native 2.0 只允许直接部署到预发环境", String(input.environment), "使用 openxiangda deploy;生产环境必须显式复用成功的测试 DeploymentRun"),
807
906
  ]);
808
907
  }
908
+ const source = await operationStage('source', '核对远端主线与源码', () => publishedDeliverySource(workspace.root));
809
909
  const client = await this.client(workspace.root);
810
910
  const capabilities = await client.capabilities();
811
911
  const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
@@ -818,32 +918,59 @@ export class OpenXiangdaApplicationServices {
818
918
  });
819
919
  assertRequiredCapabilitiesAvailable(capabilities, requiredPlatformCapabilities(workspace.config));
820
920
  const hasBackend = backendRuntimeRequired(workspace.config);
821
- const target = !hasBackend || input.backendImage
822
- ? undefined
823
- : backendImageBuildTarget(capabilities, workspace.config.app.code);
921
+ const generated = await this.generate({ root: workspace.root, check: true });
922
+ if (!generated.ok)
923
+ return { ...generated, operation: 'deploy' };
924
+ const lifecycle = await operationStage('development-records', '核对需求、架构与测试验收计划', async () => {
925
+ const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true));
926
+ return { ...lifecycle, ok: lifecycle.readyForTest };
927
+ });
928
+ if (!lifecycle.readyForTest)
929
+ return this.result('deploy', workspace.context.workspace, { lifecycle: summarizeLifecycle(lifecycle) }, lifecycle.diagnostics);
824
930
  const checked = await this.check(workspace.root, input.environment);
825
931
  if (!checked.ok)
826
932
  return { ...checked, operation: "deploy" };
827
- const backendImage = hasBackend
933
+ await assertDeliverySourceUnchanged(workspace.root, source);
934
+ const cache = this.deliveryCacheContext(workspace);
935
+ const preparedInputDigest = deliveryInputDigest(cache);
936
+ const reused = await operationStage('candidate', '核对可复用的不可变制品', async () => {
937
+ const candidate = await reusableSealedCandidate(cache, source, input.backendImage);
938
+ updateOperationStage('candidate', candidate ? '复用已验证的不可变制品' : '准备构建新制品', { reused: !!candidate, ...(candidate ? { packageDigest: candidate.package.digest } : {}) });
939
+ return candidate;
940
+ });
941
+ const backendImage = !reused && hasBackend
828
942
  ? input.backendImage
829
943
  ? input.backendImage
830
- : (await publishBackendImage({
944
+ : (await operationStage('backend-image', '构建并推送后端镜像', () => publishBackendImage({
831
945
  root: workspace.root,
832
946
  backendRoot: workspace.config.backend.root,
833
- target: target,
947
+ target: backendImageBuildTarget(capabilities, workspace.config.app.code),
834
948
  ...(workspace.context.workspace.revision
835
949
  ? { sourceRevision: workspace.context.workspace.revision }
836
950
  : {}),
837
- })).reference
951
+ }))).reference
838
952
  : undefined;
839
- const built = await this.buildSealed({
953
+ await assertDeliverySourceUnchanged(workspace.root, source);
954
+ const built = reused ? this.ok('build', workspace.context.workspace, {
955
+ ...reused, sealedArtifact: this.sealedArtifactStatus(workspace, reused.package.digest, true),
956
+ }) : await operationStage('seal', '校验并冻结不可变制品', () => this.buildSealed({
840
957
  ...input,
841
958
  root: workspace.root,
842
959
  ...(backendImage ? { backendImage } : {}),
843
960
  skipWorkspaceBuild: true,
844
- });
961
+ }));
845
962
  if (!built.ok || !built.data)
846
963
  return built;
964
+ if (preparedInputDigest && preparedInputDigest !== deliveryInputDigest(cache)) {
965
+ throw new DeliverySourceError('OPENXIANGDA_VALIDATION_INPUT_CHANGED', '密封期间源码、依赖或构建环境发生变化,已停止上传', '等待修改结束后重新运行 openxiangda deploy');
966
+ }
967
+ if (!reused)
968
+ recordSealedCandidate(cache, built.data.package);
969
+ const retained = await publishedDeliverySource(workspace.root, source.commit);
970
+ if (retained.repository !== source.repository) {
971
+ throw new DeliverySourceError('DELIVERY_REPOSITORY_CHANGED', '发布准备期间绑定远端发生变化', '恢复并核对原来源仓库,再发布同一候选');
972
+ }
973
+ await assertDeliverySourceUnchanged(workspace.root, source);
847
974
  const deployment = await (await import("./deployment.js")).submitAppPackage({
848
975
  client,
849
976
  capabilities,
@@ -972,6 +1099,20 @@ export class OpenXiangdaApplicationServices {
972
1099
  : []),
973
1100
  ];
974
1101
  }
1102
+ async productionDeploymentPlan(root, deploymentId) {
1103
+ const workspace = await this.workspace(root);
1104
+ const client = await this.client(workspace.root);
1105
+ const source = await client.deployment(workspace.config.app.code, deploymentId);
1106
+ const appVersionId = String(source.result?.applicationVersionId || '');
1107
+ if (!appVersionId)
1108
+ throw new Error('DEPLOYMENT_APP_VERSION_NOT_AVAILABLE: 测试运行尚无可晋级版本');
1109
+ const diagnostics = await this.productionPromotionDiagnostics(workspace, client, source, appVersionId);
1110
+ return this.result('deployment.plan', workspace.context.workspace, {
1111
+ environment: 'production', sourceDeploymentId: deploymentId, appVersionId,
1112
+ packageDigest: source.packageDigest, sealed: true, submitted: false,
1113
+ rebuild: false, businessAcceptance: '需要核对该测试版本的业务验收记录',
1114
+ }, diagnostics);
1115
+ }
975
1116
  async deployProduction(root, deploymentId) {
976
1117
  const workspace = await this.workspace(root);
977
1118
  const capsuleDiagnostics = this.toolchainDiagnostics(workspace);
@@ -988,12 +1129,12 @@ export class OpenXiangdaApplicationServices {
988
1129
  if (diagnostics.length > 0) {
989
1130
  return this.result("deploy", workspace.context.workspace, undefined, diagnostics);
990
1131
  }
991
- const deployment = await client.promote({
1132
+ const deployment = await operationStage('submit', '提交生产晋级', () => client.promote({
992
1133
  appCode: workspace.config.app.code,
993
1134
  appVersionId,
994
1135
  environmentKind: environment,
995
1136
  idempotencyKey: `promote:${source.packageDigest}:${environment}`,
996
- });
1137
+ }));
997
1138
  return this.ok("deploy", workspace.context.workspace, deployment, [
998
1139
  { code: "status", label: "查看生产部署状态", command: `openxiangda status ${deployment.id}` },
999
1140
  ]);
@@ -1005,15 +1146,27 @@ export class OpenXiangdaApplicationServices {
1005
1146
  diagnostics.push(this.diagnostic("PRODUCTION_PREPRODUCTION_RUN_REQUIRED", "生产晋级来源必须是成功的预发 DeploymentRun", "deploymentId", "等待预发部署成功,并使用该 DeploymentRun ID 晋级", { status: source.status, environment: source.environment.kind }));
1006
1147
  return diagnostics;
1007
1148
  }
1008
- const versions = await client.appVersions(workspace.config.app.code, 100);
1009
- const version = versions.items.find(item => item.id === appVersionId);
1010
- if (!version) {
1011
- return [
1012
- this.diagnostic("PRODUCTION_APP_VERSION_NOT_FOUND", "预发运行引用的 AppVersion 不在当前应用版本集中", "appVersionId"),
1013
- ];
1149
+ let version;
1150
+ try {
1151
+ const capabilities = await client.capabilities();
1152
+ const { assertApplicationContractCompatible, assertProductionPromotionPreflight } = await import('./deployment.js');
1153
+ assertApplicationContractCompatible(capabilities, CURRENT_APPLICATION_CONTRACT);
1154
+ version = await operationStage('promotion-preflight', '按原测试制品预检生产环境', () => client.productionPromotionPreflight(workspace.config.app.code, source.id));
1155
+ assertProductionPromotionPreflight(version, {
1156
+ appCode: workspace.config.app.code, sourceDeploymentId: source.id, appVersionId, packageDigest: source.packageDigest,
1157
+ });
1158
+ const testedLifecycle = lifecycleAtCommit(workspace.root, version.source.commit, this.appSpecContractIndex(workspace, true));
1159
+ if (!testedLifecycle.changeId || !testedLifecycle.acceptanceIds.length) {
1160
+ diagnostics.push(this.diagnostic('APPSPEC_TESTED_PLAN_REQUIRED', '指定测试版本缺少可追溯的需求与验收计划', 'appspec', '补齐真实记录并发布新的测试候选,再执行验收和生产晋级'));
1161
+ }
1162
+ const acceptance = await operationStage('business-acceptance', '核对原测试版本的业务验收报告', async () => verifyBusinessAcceptance(workspace.root, source, testedLifecycle));
1163
+ diagnostics.push(...acceptance.diagnostics);
1164
+ }
1165
+ catch (error) {
1166
+ return [this.configurationCompatibilityDiagnostic(error)];
1014
1167
  }
1015
1168
  if (workspace.context.workspace.dirty || version.source.dirty) {
1016
- diagnostics.push(this.diagnostic("PRODUCTION_SOURCE_DIRTY", "生产晋级只接受来源干净且当前工作区无未提交变更的 AppVersion", "git", "提交并推送变更后重新部署预发,再晋级该 AppVersion"));
1169
+ diagnostics.push(this.diagnostic("PRODUCTION_SOURCE_DIRTY", "生产晋级只接受来源干净且当前工作区无未提交变更的 AppVersion", "git", "提交并推送当前改动,或在干净主线目录中晋级该测试版本"));
1017
1170
  }
1018
1171
  const repository = git(workspace.root, ["config", "--get", "remote.origin.url"]);
1019
1172
  const currentRepository = this.repositoryIdentity(repository);
@@ -1023,25 +1176,15 @@ export class OpenXiangdaApplicationServices {
1023
1176
  currentRepository !== sourceRepository) {
1024
1177
  diagnostics.push(this.diagnostic("PRODUCTION_SOURCE_REPOSITORY_MISMATCH", "当前工作区仓库与 AppVersion 来源仓库不一致", "git.remote.origin.url", "在构建该 AppVersion 的同一仓库中执行晋级", { current: repository || null, source: version.source.repository }));
1025
1178
  }
1026
- const upstream = git(workspace.root, [
1027
- "rev-parse",
1028
- "--abbrev-ref",
1029
- "--symbolic-full-name",
1030
- "@{upstream}",
1031
- ]);
1032
- if (!upstream) {
1033
- diagnostics.push(this.diagnostic("PRODUCTION_UPSTREAM_MISSING", "当前 Git 分支没有 upstream,无法证明 AppVersion 来源已进入远端主线", "git", "为当前分支配置 upstream 并推送来源 commit"));
1179
+ if (diagnostics.length)
1180
+ return diagnostics;
1181
+ try {
1182
+ await operationStage('promotion-source', '核对测试制品的主线来源', () => publishedDeliverySource(workspace.root, version.source.commit));
1034
1183
  }
1035
- else {
1036
- const refreshed = spawnSync("git", ["-C", workspace.root, "fetch", "--quiet"], { encoding: "utf8" });
1037
- if (refreshed.status !== 0) {
1038
- diagnostics.push(this.diagnostic("PRODUCTION_UPSTREAM_REFRESH_FAILED", "无法刷新远端引用,生产晋级不能依赖可能过期的本地 upstream", "git", "恢复远端访问并重新执行晋级"));
1039
- return diagnostics;
1040
- }
1041
- const contained = spawnSync("git", ["-C", workspace.root, "merge-base", "--is-ancestor", version.source.commit, upstream], { encoding: "utf8" });
1042
- if (contained.status !== 0) {
1043
- diagnostics.push(this.diagnostic("PRODUCTION_SOURCE_NOT_IN_UPSTREAM", "AppVersion 来源 commit 尚未包含在当前 upstream 中", "git", "推送来源 commit,确认 upstream 更新后再晋级", { commit: version.source.commit, upstream }));
1044
- }
1184
+ catch (error) {
1185
+ if (!(error instanceof DeliverySourceError))
1186
+ throw error;
1187
+ diagnostics.push(this.diagnostic(error.code, error.message, 'git', String(error.data.remediation), error.data));
1045
1188
  }
1046
1189
  return diagnostics;
1047
1190
  }
@@ -1231,10 +1374,10 @@ export class OpenXiangdaApplicationServices {
1231
1374
  : {}),
1232
1375
  };
1233
1376
  }
1234
- async validateTargetConfigurationCompatibility(workspace, environmentKey) {
1377
+ async validateTargetConfigurationCompatibility(workspace, environmentKey, local) {
1235
1378
  const client = await this.client(workspace.root);
1236
1379
  const capabilities = await client.capabilities();
1237
- const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
1380
+ const { sources } = local;
1238
1381
  const required = {
1239
1382
  ...CURRENT_APPLICATION_CONTRACT,
1240
1383
  configurationBundleSchemaVersion: sources.config.value.schemaVersion,
@@ -1264,9 +1407,21 @@ export class OpenXiangdaApplicationServices {
1264
1407
  configurationDigest: sources.config.digest,
1265
1408
  contractDigest: sources.contracts.digest,
1266
1409
  }, requiredPlatformCapabilities(workspace.config));
1410
+ if (result.projectionDigest !== local.compiled.aggregateDigest) {
1411
+ throw new ControlPlaneError(409, 'OPENXIANGDA_CONFIGURATION_PROJECTION_MISMATCH', '相同校验规则与输入生成了不同投影,已停止构建,请检查平台配套版本', {
1412
+ pointer: '/projectionDigest', expectedProjectionDigest: local.compiled.aggregateDigest,
1413
+ actualProjectionDigest: result.projectionDigest, validatorDigest: NATIVE_CONFIGURATION_VALIDATOR_DIGEST,
1414
+ });
1415
+ }
1267
1416
  return result;
1268
1417
  }
1269
1418
  configurationCompatibilityDiagnostic(error) {
1419
+ if (error instanceof NativeConfigurationCompilerError) {
1420
+ return this.diagnostic(error.code, '应用声明未通过本地完整配置校验', error.pointer, '按错误指针修正应用声明,运行 openxiangda check 复核', {
1421
+ pointer: error.pointer, identifiers: error.identifiers, validatorDigest: NATIVE_CONFIGURATION_VALIDATOR_DIGEST,
1422
+ validationOwner: 'local-shared-compiler',
1423
+ });
1424
+ }
1270
1425
  const data = error instanceof ControlPlaneError &&
1271
1426
  error.data &&
1272
1427
  typeof error.data === "object" &&
@@ -1337,6 +1492,13 @@ export class OpenXiangdaApplicationServices {
1337
1492
  get toolchainVersion() {
1338
1493
  return this.options.toolchainVersion || OPENXIANGDA_TOOLCHAIN_VERSION;
1339
1494
  }
1495
+ deliveryCacheContext(workspace) {
1496
+ return {
1497
+ root: workspace.root, frontendRoot: workspace.config.frontend.root,
1498
+ ...(backendRuntimeRequired(workspace.config) ? { backendRoot: workspace.config.backend.root } : {}),
1499
+ toolchain: { version: this.toolchainVersion, capsule: this.toolchainCapsule, validatorDigest: NATIVE_CONFIGURATION_VALIDATOR_DIGEST },
1500
+ };
1501
+ }
1340
1502
  get toolchainCapsule() {
1341
1503
  return this.options.toolchainCapsule || defaultDevkitCoreToolchainCapsule();
1342
1504
  }
@@ -1356,17 +1518,14 @@ export class OpenXiangdaApplicationServices {
1356
1518
  }];
1357
1519
  }
1358
1520
  }
1359
- runPackageScript(root, script) {
1360
- const result = spawnSync("pnpm", ["run", script], {
1361
- cwd: root,
1362
- encoding: "utf8",
1363
- env: process.env,
1521
+ packageScriptLabel(script) {
1522
+ return { check: '类型与规范检查', test: '应用测试', build: '前后端构建' }[script] || script;
1523
+ }
1524
+ async runPackageScript(root, script) {
1525
+ return operationStage(script, this.packageScriptLabel(script), async () => {
1526
+ const result = await runCommandProcess('pnpm', ['run', script], { cwd: root });
1527
+ return { ok: result.ok, exitCode: result.status ?? 1, output: result.output.trim(), outputTruncated: result.outputTruncated };
1364
1528
  });
1365
- return {
1366
- ok: result.status === 0,
1367
- exitCode: result.status ?? 1,
1368
- output: `${result.stdout || ""}${result.stderr || ""}`.trim(),
1369
- };
1370
1529
  }
1371
1530
  repositoryIdentity(value) {
1372
1531
  const normalized = String(value || "").trim();
@@ -1642,7 +1801,7 @@ export class OpenXiangdaApplicationServices {
1642
1801
  operation,
1643
1802
  workspace,
1644
1803
  ...(data === undefined ? {} : { data }),
1645
- diagnostics,
1804
+ diagnostics: [...diagnostics].sort((left, right) => ({ error: 0, warning: 1, info: 2 })[left.severity] - ({ error: 0, warning: 1, info: 2 })[right.severity]),
1646
1805
  nextActions,
1647
1806
  };
1648
1807
  }