openxiangda 1.0.213 → 1.0.215

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 (31) hide show
  1. package/README.md +6 -2
  2. package/lib/application-environments.js +227 -3
  3. package/lib/cli.js +1521 -136
  4. package/lib/policy.js +1 -0
  5. package/lib/release-mainline.js +67 -8
  6. package/lib/resource-binding-contract.js +210 -0
  7. package/lib/sdd.js +216 -74
  8. package/lib/workspace-init.js +9 -3
  9. package/lib/worktree-cleanup.js +631 -0
  10. package/lib/worktree-owner.js +121 -5
  11. package/openxiangda-skills/SKILL.md +11 -4
  12. package/openxiangda-skills/references/connector-resources.md +1 -1
  13. package/openxiangda-skills/references/openxiangda-api.md +1 -1
  14. package/openxiangda-skills/references/workflow-v3.md +1 -1
  15. package/openxiangda-skills/references/workspace-state.md +1 -1
  16. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +9 -1
  17. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +2 -2
  18. package/package.json +3 -1
  19. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  20. package/packages/sdk/dist/runtime/index.d.mts +1 -1
  21. package/packages/sdk/dist/runtime/index.d.ts +1 -1
  22. package/packages/sdk/dist/runtime/react.cjs.map +1 -1
  23. package/packages/sdk/dist/runtime/react.d.mts +37 -1
  24. package/packages/sdk/dist/runtime/react.d.ts +37 -1
  25. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +6 -0
  26. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +6 -0
  27. package/templates/openxiangda-react-spa/AGENTS.md +8 -1
  28. package/templates/openxiangda-react-spa/scripts/deploy.mjs +1 -1
  29. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +6 -1
  30. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +6 -0
  31. package/templates/sy-lowcode-app-workspace/AGENTS.md +9 -1
package/README.md CHANGED
@@ -82,7 +82,7 @@ openxiangda app snapshot APP_XXXX --profile dev --json
82
82
 
83
83
  User tokens are stored in `~/.openxiangda/profiles.json` with `0600` permissions. Shared workspace environment values, including `APP_OSS_*`, can live in `~/.openxiangda/.env` and are inherited by new workspaces. Project `.env` files still work and override the global defaults. Project state is stored in `.openxiangda/state.json` and contains only durable profile-specific resource IDs and environment bindings; volatile candidate/deployment progress lives in the private `.openxiangda/releases/` journal so a release does not dirty the reviewed Git state. Durable CLI writes use a workspace lock plus atomic merge/rename so concurrent processes do not truncate another profile's state.
84
84
 
85
- An environment-managed workspace keeps one logical application with independent `preproduction` and `production` targets. Each target owns its own `appType`, resource IDs, release heads, data, and side-effect policy; IDs must never be copied across targets. `release ship` is deliberately two-phase: the first invocation seals the clean authoritative Git revision and deploys one immutable candidate only to preproduction; it always stops at `awaiting_production_confirmation`. A later invocation includes `--confirm-production` and promotes that exact candidate to production without rebuilding it. Real human acceptance is the recommended default; an optional `--acceptance-note` records it in the deployment evidence, but the platform does not force every low-risk or emergency release through a rigid approval gate. It is impossible to prepare and promote in one invocation. For an audited catch-up whose exact non-delete targets are already merged but whose active resources came from several historical release lineages, the first invocation may explicitly add `--adopt-online-baseline --adoption-reason "..."`; ship validates and freezes this pair before creating a candidate/deployment, then automatically reuses it during the later production confirmation and forwards it only to exact scoped resource stages. A reviewed complete Function/Automation manifest replacement may similarly add the inseparable `--replace-manifest --reason "..."` pair; ship forwards it only to an exact Backend stage and requires the production confirmation to repeat the exact preproduction intent. Frozen online heads, change/lease ownership, server CAS, staged children, and the single App finalize remain mandatory. Supported configuration resources use exact `resourceSelectors`; a legacy `resources=true` category marker is narrowed by those selectors, while unknown, wildcard, destructive, and genuinely unscoped generic resources remain blocked. The lower-level `candidate/deploy/test/promote` commands remain available for recovery and diagnostics. Direct `release publish` is retained only for legacy, unmanaged workspaces and fails closed for either target of an environment-managed application.
85
+ An environment-managed workspace keeps one logical application with independent `preproduction` and `production` targets. Each target owns its own `appType`, resource IDs, release heads, data, and side-effect policy; IDs must never be copied across targets. `release ship` always executes the same ordered candidate preproduction production protocol. The normal first invocation seals the candidate, deploys only to preproduction, and stops at `awaiting_production_confirmation`; a later invocation with `--confirm-production` promotes it. For an explicitly authorized emergency, supplying `--confirm-production` on the first invocation runs both phases in one command without bypassing preproduction, CAS, evidence, or production confirmation. Candidate sealing covers source, `public/`, build controls/scripts, stable environment/resource bindings, and target-specific hashed Runtime artifacts. Both deployments upload those artifacts with `--no-build`; each deployment is completed with evidence and reaches terminal `succeeded`, so it cannot leave the target slot blocked. Unrelated commits may land on authoritative mainline between phases only while the sealed commit remains an ancestor and every sealed input/binding/artifact still validates. Real human acceptance remains the recommended default and `--acceptance-note` records it. For audited historical-lineage adoption or reviewed Backend manifest replacement, the existing paired flags and exact-scope gates remain mandatory. Supported configuration resources use exact `resourceSelectors`; unknown, wildcard, destructive, and genuinely unscoped generic resources remain blocked. Lower-level candidate/deploy/test/promote commands are recovery primitives. Direct `release publish` is retained only for legacy unmanaged workspaces.
86
86
 
87
87
  Exact, non-destructive configuration selectors such as Data Views and permission groups are now sequenced automatically inside the same ship journal instead of requiring separate SDD changes. New forms are idempotently ensured per environment before their immutable FormRelease is staged. Unscoped resources and destructive configuration deletes remain fail-closed.
88
88
 
@@ -108,6 +108,8 @@ Parallel tasks develop and test in isolated worktrees, but feature worktrees do
108
108
 
109
109
  Because promotion starts from an already-pushed authoritative mainline commit, `openxiangda release integration-status --change <change> --profile <name>` should pass immediately after activation. After a managed ship has ended, `--change` recovers lineage directly from the private `ship.json` or follows its production/preproduction deployment IDs to the matching `execution.json`; a recovery failure names the missing file or field. Run it and `release end`; there is no post-release merge step.
110
110
 
111
+ Task worktrees are disposable only after the release lifecycle is closed. From the synchronized canonical main checkout, run `openxiangda workspace cleanup` for a dry-run inventory. A worktree is `SAFE` only when it is OpenXiangda/Codex-managed, its commit is contained in live remote mainline, it is clean and unowned, and it has no private candidate/release journal or promotion lease/baseline. `--apply` is bound to the exact hashed dry-run plan, revalidates under the worktree owner lock, and removes only those reviewed entries plus their unchanged local branches. Newly-safe worktrees are left for the next review. Malformed owner state fails closed. Stale records are reported for explicit manual review because the CLI never runs repository-wide `git worktree prune`. Remote branches are retained.
112
+
111
113
  `openxiangda task status --change <id>` is the compact, read-only answer to
112
114
  “现在到哪一步”:it combines SDD、TaskResult、IntegrationBundle、execution
113
115
  journal、lease 和 post-commit outbox,输出进度、ETA、是否已经发生平台写入、
@@ -175,6 +177,8 @@ L1 quick records are generated in a compact form and should not be expanded into
175
177
 
176
178
  Every concurrent task uses its own Git worktree/branch and development change. The canonical main checkout is reserved for integration and release; tasks never stash/restore each other's files to make it publishable. The mainline release coordinator bundles selected approved changes after merge; dependency impact outside the approved scope is reported as a warning rather than silently widening a small release. Live commands still require canonical exact selectors, and app-wide/delete operations need explicit authority.
177
179
 
180
+ After the mainline release and `release end`, the coordinator runs `workspace cleanup` from canonical main and applies only `SAFE` entries. Dirty/unmerged/owned worktrees and worktrees with private release or promotion state remain blocked for recovery.
181
+
178
182
  Agent investigation is bounded to one complete CodeGraph survey plus at most one focused follow-up, and ordinary work loads one domain skill. Unchanged lease/build waits use `task status --watch` and produce updates only on material transitions.
179
183
 
180
184
  Before confirmation, agents may read, inspect, snapshot, dry-run, ask questions, and output/write the architecture document. They must not edit source files, mutate platform resources, publish, deploy, send notifications, or call live write/delete endpoints.
@@ -502,7 +506,7 @@ const PublicAccessError = ({ error }: { error: { message?: string } }) => (
502
506
 
503
507
  多表只读查询和固定口径统计优先声明 `src/resources/data-views/*.json` 数据视图,而不是在页面里手写多次单表查询再拼数据。默认 `storageMode: "materialized"` 会创建 PostgreSQL materialized view,适合读多写少和可接受刷新延迟的列表/报表;`storageMode: "live"` 每次查询实时编译逻辑视图,适合强实时但数据量可控的复杂查询。`viewType: "aggregate"` 是统计聚合视图,适合按客户、状态、月份等维度聚合 count/sum/avg/min/max。发布时 CLI 会把 `formCode` 解析为当前 profile 的 `formUuid`;页面通过 `sdk.dataView.query(code, params)` 查询行级视图,通过 `sdk.dataView.stats(code, params)` 查询聚合视图,也可以用 `sdk.dataSource.run()` 路由 `dataView.query` / `dataView.stats`。materialized 模式应为常用筛选、排序、统计维度和时间桶声明 `indexes`,并确认用户能接受的刷新延迟;live 模式忽略 `indexes`,不需要刷新。
504
508
 
505
- 后端业务逻辑优先声明为 App Function:源码放在 `src/functions/<functionCode>/index.ts`,资源 manifest 放在 `src/resources/functions/<functionCode>.json`。函数运行在 trusted_node 中,通过 `ctx.form.queryOne/queryMany/getById/createOne/updateOne/updateById`、`ctx.dataView`、`ctx.connector`、`ctx.notification`、`ctx.organization`、`ctx.platform.roles`、`ctx.platform.api` 等受控 API 访问平台能力;自动化、流程和运行时接口都可以调用同一个 function。角色查询和角色成员维护优先使用 `ctx.platform.roles.list/findByCode/addUsers/removeUser`,底层 `ctx.platform.api` 返回 HTTP 包装和平台 envelope,需要业务代码自行解包。运行时页面调用会在 `ctx.operator`、`ctx.currentUser`、`ctx.permissions` 中注入可信的 `roleCodes`、`currentRoleCode`、`hasFullAccess`、`isAppAdmin`、`isPlatformAdmin`,敏感动作必须读取这些服务端上下文做授权,不要信任页面 input 里传入的角色字段。`ctx.form.createOne/updateOne/updateById` 是后端受控写入,授权边界是函数 manifest 的 `resources.forms` 绑定和函数调用授权,不是页面用户对目标表单的直接提交入口;报名、签到、福利选择等内部多表写入应走 App Function,不要为了写内部表单给普通用户开放原始 submit 权限。JS_CODE V2 仍兼容,但新逻辑建议写成 function,再由 `function_call` 节点、`sdk.function.invoke(code, { input })` 或 `/:appType/v1/functions/:code/invoke.json` 调用。直接运行时接口默认要求调用者具备应用自动化管理权限;普通页面调用必须在函数 `definitionJson.runtimeInvoke.audience` 声明 `authenticated`、`page_permission_group`、`app_roles` 或 `scope_policy`,`roleCodes` 只用于当前应用角色精确匹配,不支持 `"*"` / `"all-app-roles"`。自动化/流程内部调用走服务端受控上下文。内部业务表单需要关闭原始写入接口时,在表单 settings 中设置 `runtimeWrite.mode="function_only"`。
509
+ 后端业务逻辑优先声明为 App Function:源码放在 `src/functions/<functionCode>/index.ts`,资源 manifest 放在 `src/resources/functions/<functionCode>.json`。函数运行在 trusted_node 中,通过 `ctx.form.queryOne/queryMany/getById/createOne/updateOne/updateById`、`ctx.process.startFromExistingInstance/resolveCapabilities/resubmitTask/withdraw/transferTask`、`ctx.dataView`、`ctx.connector`、`ctx.notification`、`ctx.organization`、`ctx.platform.roles`、`ctx.platform.api` 等受控 API 访问平台能力;自动化、流程和运行时接口都可以调用同一个 function。`ctx.process` 复用正式工作流服务,以真实运行时 operator 执行,并严格限制在当前应用及 Function `resources.forms` 已绑定的流程表单;流程实例、任务授权、操作日志、事件和 replay 语义与 PageSdk 后端接口一致。角色查询和角色成员维护优先使用 `ctx.platform.roles.list/findByCode/addUsers/removeUser`,底层 `ctx.platform.api` 返回 HTTP 包装和平台 envelope,需要业务代码自行解包。运行时页面调用会在 `ctx.operator`、`ctx.currentUser`、`ctx.permissions` 中注入可信的 `roleCodes`、`currentRoleCode`、`hasFullAccess`、`isAppAdmin`、`isPlatformAdmin`,敏感动作必须读取这些服务端上下文做授权,不要信任页面 input 里传入的角色字段。`ctx.form.createOne/updateOne/updateById` 是后端受控写入,授权边界是函数 manifest 的 `resources.forms` 绑定和函数调用授权,不是页面用户对目标表单的直接提交入口;报名、签到、福利选择等内部多表写入应走 App Function,不要为了写内部表单给普通用户开放原始 submit 权限。JS_CODE V2 仍兼容,但新逻辑建议写成 function,再由 `function_call` 节点、`sdk.function.invoke(code, { input })` 或 `/:appType/v1/functions/:code/invoke.json` 调用。直接运行时接口默认要求调用者具备应用自动化管理权限;普通页面调用必须在函数 `definitionJson.runtimeInvoke.audience` 声明 `authenticated`、`page_permission_group`、`app_roles` 或 `scope_policy`,`roleCodes` 只用于当前应用角色精确匹配,不支持 `"*"` / `"all-app-roles"`。自动化/流程内部调用走服务端受控上下文。内部业务表单需要关闭原始写入接口时,在表单 settings 中设置 `runtimeWrite.mode="function_only"`。
506
510
 
507
511
  App Function 访问第三方凭据时使用 `app_function_secrets_v1`:manifest 顶层只声明 `secretRefs: [{ "name": "dingtalk_org_app_key", "required": true }]`,同时使用 `definitionJson.version="function_v2"`、`runtimeContractVersion="trusted_node_v2"`;源码通过 `await ctx.secrets.get(name)` 解析,并通过 `ctx.utils.http` 访问受控公网 HTTPS(该桥接不会携带平台 Runtime token)。值只能经 `openxiangda secret create|rotate --value-stdin --change <id> --profile <name>` 或隐藏 TTY 输入,禁止进入 Git、manifest、源码、构建产物、plan、日志或异常。带 `secretRefs` 的 Function 必须走 `backend_release_v2`;需要整应用原子发布时先执行 `resource publish function --only <code> --stage-only`,再把返回的真实 `stagedResource` 交给 `release app-finalize --staged-resources-json ...` 完成 `atomic_staged_children_v2`。默认直接激活的 Backend Release 只返回 `activeResource`,不会伪装成 staged;旧平台 capability 不完整时 CLI 会失败关闭,绝不忽略绑定。
508
512
 
@@ -570,20 +570,163 @@ function hashWorkspaceFile(cwd, relativePath) {
570
570
  if (
571
571
  !normalized ||
572
572
  relative.startsWith('..') ||
573
- path.isAbsolute(relative) ||
574
- !fs.existsSync(absolute) ||
575
- !fs.statSync(absolute).isFile()
573
+ path.isAbsolute(relative)
576
574
  ) {
577
575
  return null;
578
576
  }
577
+ if (!fs.existsSync(absolute)) {
578
+ return {
579
+ path: normalized,
580
+ exists: false,
581
+ size: 0,
582
+ sha256: null,
583
+ };
584
+ }
585
+ if (!fs.statSync(absolute).isFile()) return null;
579
586
  const buffer = fs.readFileSync(absolute);
580
587
  return {
581
588
  path: normalized,
589
+ exists: true,
582
590
  size: buffer.length,
583
591
  sha256: crypto.createHash('sha256').update(buffer).digest('hex'),
584
592
  };
585
593
  }
586
594
 
595
+ function buildCandidateEnvironmentBindings(state) {
596
+ const targets =
597
+ state?.targets && typeof state.targets === 'object'
598
+ ? state.targets
599
+ : {};
600
+ return Object.fromEntries(
601
+ Object.entries(targets)
602
+ .sort(([left], [right]) => left.localeCompare(right))
603
+ .map(([targetName, binding]) => [
604
+ targetName,
605
+ {
606
+ targetName,
607
+ profile: binding?.profile || null,
608
+ environmentId: binding?.environmentId || null,
609
+ kind: binding?.kind || null,
610
+ appType: binding?.appType || null,
611
+ publicOrigin: binding?.publicOrigin || null,
612
+ sideEffectPolicy:
613
+ cloneJsonValue(binding?.sideEffectPolicy || {}) || {},
614
+ revision:
615
+ binding?.revision === undefined
616
+ ? null
617
+ : binding.revision,
618
+ resources: cloneJsonValue(binding?.resources || {}) || {},
619
+ },
620
+ ])
621
+ );
622
+ }
623
+
624
+ function candidateBindingValueContains(expected, current) {
625
+ if (Array.isArray(expected)) {
626
+ return (
627
+ Array.isArray(current) &&
628
+ expected.length === current.length &&
629
+ expected.every((item, index) =>
630
+ candidateBindingValueContains(item, current[index])
631
+ )
632
+ );
633
+ }
634
+ if (expected && typeof expected === 'object') {
635
+ if (!current || typeof current !== 'object' || Array.isArray(current)) {
636
+ return false;
637
+ }
638
+ return Object.entries(expected).every(([key, value]) =>
639
+ candidateBindingValueContains(value, current[key])
640
+ );
641
+ }
642
+ return expected === current;
643
+ }
644
+
645
+ function inspectCandidateEnvironmentBindings(candidate, state) {
646
+ const expected =
647
+ candidate?.sourceBundleManifest?.environmentBindings ||
648
+ candidate?.environmentBindings ||
649
+ {};
650
+ const current = buildCandidateEnvironmentBindings(state);
651
+ const mismatches = [];
652
+ const expectedNames = Object.keys(expected).sort();
653
+ const currentNames = Object.keys(current).sort();
654
+ if (canonicalJson(expectedNames) !== canonicalJson(currentNames)) {
655
+ mismatches.push({
656
+ targetName: '(targets)',
657
+ reason: 'target-set-changed',
658
+ expected: expectedNames,
659
+ current: currentNames,
660
+ });
661
+ }
662
+ for (const targetName of expectedNames) {
663
+ const expectedBinding = expected[targetName];
664
+ const currentBinding = current[targetName];
665
+ if (!currentBinding) continue;
666
+ for (const field of [
667
+ 'targetName',
668
+ 'profile',
669
+ 'environmentId',
670
+ 'kind',
671
+ 'appType',
672
+ 'publicOrigin',
673
+ 'sideEffectPolicy',
674
+ 'revision',
675
+ ]) {
676
+ if (
677
+ canonicalJson(expectedBinding?.[field]) !==
678
+ canonicalJson(currentBinding?.[field])
679
+ ) {
680
+ mismatches.push({
681
+ targetName,
682
+ field,
683
+ reason: 'changed',
684
+ expected: expectedBinding?.[field],
685
+ current: currentBinding?.[field],
686
+ });
687
+ }
688
+ }
689
+ if (
690
+ !candidateBindingValueContains(
691
+ expectedBinding?.resources || {},
692
+ currentBinding.resources || {}
693
+ )
694
+ ) {
695
+ mismatches.push({
696
+ targetName,
697
+ field: 'resources',
698
+ reason: 'sealed-resource-binding-changed',
699
+ expected: expectedBinding?.resources || {},
700
+ current: currentBinding.resources || {},
701
+ });
702
+ }
703
+ }
704
+ return {
705
+ valid: mismatches.length === 0,
706
+ checked: expectedNames.length,
707
+ mismatches,
708
+ };
709
+ }
710
+
711
+ function assertCandidateEnvironmentBindingsCurrent(candidate, state) {
712
+ const inspection = inspectCandidateEnvironmentBindings(candidate, state);
713
+ if (inspection.valid) return inspection;
714
+ const error = new Error(
715
+ `CANDIDATE_ENVIRONMENT_BINDINGS_CHANGED: sealed candidate 的环境或资源绑定已变化: ${inspection.mismatches
716
+ .slice(0, 8)
717
+ .map(
718
+ item =>
719
+ `${item.targetName}${
720
+ item.field ? `.${item.field}` : ''
721
+ }(${item.reason})`
722
+ )
723
+ .join(', ')}`
724
+ );
725
+ error.code = 'CANDIDATE_ENVIRONMENT_BINDINGS_CHANGED';
726
+ error.details = inspection;
727
+ throw error;
728
+ }
729
+
587
730
  function buildCandidateBundle(input) {
588
731
  const cwd = path.resolve(input.cwd || process.cwd());
589
732
  const files = Array.from(new Set(input.files || []))
@@ -610,10 +753,17 @@ function buildCandidateBundle(input) {
610
753
  sourceRevision: input.sourceRevision,
611
754
  runtimeMode: input.runtimeMode,
612
755
  targets: input.targets,
756
+ inputPolicy: 'candidate-inputs-v2',
613
757
  files,
758
+ environmentBindings:
759
+ cloneJsonValue(input.environmentBindings || {}) || {},
760
+ runtimeArtifacts:
761
+ cloneJsonValue(input.runtimeArtifacts || []) || [],
614
762
  testPlanHash: sha256Canonical(testPlan),
615
763
  prepublishVerificationHash:
616
764
  input.prepublishVerificationHash || null,
765
+ candidatePreflightHash:
766
+ input.candidatePreflightHash || null,
617
767
  openxiangdaVersion: input.openxiangdaVersion,
618
768
  };
619
769
  return {
@@ -624,6 +774,75 @@ function buildCandidateBundle(input) {
624
774
  };
625
775
  }
626
776
 
777
+ function inspectCandidateBundleFiles(candidate, cwd = process.cwd()) {
778
+ const manifest =
779
+ candidate?.sourceBundleManifest &&
780
+ typeof candidate.sourceBundleManifest === 'object'
781
+ ? candidate.sourceBundleManifest
782
+ : candidate;
783
+ const expectedFiles = Array.isArray(manifest?.files)
784
+ ? manifest.files
785
+ : [];
786
+ const mismatches = [];
787
+ const seen = new Set();
788
+ for (const expected of expectedFiles) {
789
+ const file = String(expected?.path || '').replace(/\\/g, '/');
790
+ if (!file || seen.has(file)) {
791
+ mismatches.push({
792
+ path: file || '(missing path)',
793
+ reason: seen.has(file) ? 'duplicate' : 'invalid',
794
+ });
795
+ continue;
796
+ }
797
+ seen.add(file);
798
+ const current = hashWorkspaceFile(path.resolve(cwd), file);
799
+ const expectedExists = expected.exists !== false;
800
+ if (!current || (expectedExists && current.exists === false)) {
801
+ mismatches.push({ path: file, reason: 'missing' });
802
+ continue;
803
+ }
804
+ if (!expectedExists) {
805
+ if (current.exists !== false) {
806
+ mismatches.push({ path: file, reason: 'reappeared', current });
807
+ }
808
+ continue;
809
+ }
810
+ if (
811
+ current.size !== Number(expected.size) ||
812
+ current.sha256 !== String(expected.sha256 || '').toLowerCase()
813
+ ) {
814
+ mismatches.push({
815
+ path: file,
816
+ reason: 'changed',
817
+ expected: {
818
+ size: Number(expected.size),
819
+ sha256: String(expected.sha256 || '').toLowerCase(),
820
+ },
821
+ current,
822
+ });
823
+ }
824
+ }
825
+ return {
826
+ valid: mismatches.length === 0,
827
+ checked: expectedFiles.length,
828
+ mismatches,
829
+ };
830
+ }
831
+
832
+ function assertCandidateBundleFilesCurrent(candidate, cwd = process.cwd()) {
833
+ const inspection = inspectCandidateBundleFiles(candidate, cwd);
834
+ if (inspection.valid) return inspection;
835
+ const error = new Error(
836
+ `CANDIDATE_INPUTS_CHANGED: sealed candidate 的发布输入已变化: ${inspection.mismatches
837
+ .slice(0, 8)
838
+ .map(item => `${item.path}(${item.reason})`)
839
+ .join(', ')}`
840
+ );
841
+ error.code = 'CANDIDATE_INPUTS_CHANGED';
842
+ error.details = inspection;
843
+ throw error;
844
+ }
845
+
627
846
  function releaseExecutionPath(changeId, deploymentId, cwd = process.cwd()) {
628
847
  const base = path.join(
629
848
  cwd,
@@ -706,7 +925,10 @@ function isRecoverablePostActivationDeploymentError(error, deployment) {
706
925
  }
707
926
 
708
927
  module.exports = {
928
+ assertCandidateBundleFilesCurrent,
929
+ assertCandidateEnvironmentBindingsCurrent,
709
930
  bindEnvironmentTarget,
931
+ buildCandidateEnvironmentBindings,
710
932
  buildSideEffectPolicyDiff,
711
933
  buildCandidateBundle,
712
934
  canonicalJson,
@@ -718,6 +940,8 @@ module.exports = {
718
940
  normalizeSideEffectPolicyInput,
719
941
  normalizeTargetName,
720
942
  hasStateResourceMappings,
943
+ inspectCandidateBundleFiles,
944
+ inspectCandidateEnvironmentBindings,
721
945
  managedTargetDeploymentFile,
722
946
  readCandidate,
723
947
  readJsonInput,