openxiangda 1.0.170 → 1.0.172

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,11 +39,12 @@ openxiangda sdd approve add-customer-page --summary "用户确认"
39
39
  openxiangda sdd verify add-customer-page --changed --stage implementation --profile dev
40
40
  openxiangda resource validate function --only customer_get,customer_save --profile dev
41
41
  openxiangda resource plan function --only customer_get,customer_save --profile dev
42
- openxiangda runtime deploy --change add-customer-page --profile dev --no-activate --json
43
- openxiangda sdd verify add-customer-page --changed --stage prepublish --profile dev
44
- openxiangda release begin --change add-customer-page --profile dev
45
- openxiangda resource publish function --only customer_get,customer_save --stage-only --change add-customer-page --profile dev
46
- openxiangda release app-finalize --staged-resources-json .openxiangda/releases/add-customer-page/staged-resources.json --change add-customer-page --profile dev
42
+ # merge/push approved task commits to main, then create and push one release bundle
43
+ openxiangda sdd bundle mainline-release --changes add-customer-page,fix-customer-api
44
+ openxiangda sdd verify mainline-release --changed --stage prepublish --profile dev
45
+ openxiangda release begin --change mainline-release --profile dev
46
+ # execute the canonical staged commands emitted by the release plan
47
+ openxiangda release integration-status --profile dev
47
48
  openxiangda release end --profile dev
48
49
  openxiangda resource explain public-access --json
49
50
  openxiangda inspect app --profile dev --json
@@ -56,9 +57,9 @@ User tokens are stored in `~/.openxiangda/profiles.json` with `0600` permissions
56
57
 
57
58
  React SPA workspaces publish their frontend with `openxiangda runtime deploy`. Every finalized Runtime release is built from a clean, committed Git `HEAD`; the CLI freezes `sourceRevision`, the current active release, and its source revision before any build/upload. Deploy fails with `RUNTIME_SOURCE_BASE_DIVERGED` when its `HEAD` does not descend from the online Runtime source, including with `--no-activate`, so an old isolated worktree cannot stage and later activate a silent rollback. `.openxiangda/`, `openspec/`, `dist/`, and other pure generated/governance/state paths do not make the source dirty, but `--no-build` cannot bypass the lineage gate. An intentional rollback requires `--allow-runtime-rollback --reason "<at least 8 characters>"`, which is persisted for audit and never bypasses dirty/non-Git checks.
58
59
 
59
- Concurrent development may still upload immutable previews with `--no-activate`, but the preview must also freeze a real clean Git source revision before it can become a release. Final promotion uses `release begin/end` so one `(tenant, app)` has one writer while other worktrees keep developing and testing. `release begin --change <change>` binds the per-task `clientSessionId` (Codex uses `CODEX_THREAD_ID`), keeps the earlier change base separate, and freezes the actual clean publish `HEAD` plus the selected remote resource-field heads. That publish commit must already contain the current authoritative remote default branch, and it cannot change during the release. Before the first live write, the CLI preflights the complete selected set. A stale resource worktree fails with `SOURCE_BASE_DIVERGED`/`RELEASE_SOURCE_BEHIND_MAIN`; a selected remote field that moved after the baseline fails with `RESOURCE_FIELD_CONFLICT`. Resolve the divergence, rebuild/re-plan, and start a new release instead of retrying a stale payload. The only exception is a historical catch-up where approved code was already merged to main and the live app demonstrably consists of several older release lineages: exact, non-delete publishes may use `--adopt-online-baseline --adoption-reason "<at least 8 characters>"` together with the same `--change` (or its already-held release lease). This adopts the server-frozen online Function/Automation heads instead of inventing one Git base; Function/Automation still run the server remote-head preflight, and every resource remains inside the app lease, staged-release/CAS path supported by its publisher. It rejects `--all`, `--force`, `--prune`, and every delete plan. The default upload mode is `auto`: it tries staged multipart first, then falls back to built-in OSS direct upload if the staged file API returns HTTP 403. Use `--upload-mode oss-direct` to force built-in OSS direct upload, or `--upload-mode legacy-json` only as a compatibility fallback for older platforms. Upload progress, timeout, and `traceId` are written to stderr; `--json` stdout remains the final JSON result.
60
+ Parallel tasks develop and test in isolated worktrees, but feature worktrees do not publish. After approved commits are merged and pushed, `sdd bundle <release-change> --changes ...` unions their exact structured scope. Commit/push the bundle, then run one promotion from a clean local main/master whose commit exactly equals the authoritative remote tip. `release begin` rejects feature branches and unpushed/diverged mainline before any platform write. Runtime build readiness is also checked before lease acquisition; the CLI executes the declared build through `npm run build`, which works with pnpm worktree symlinked dependencies without reinstalling or changing the lock file. The protected upload plan still precedes the actual build because its provider-specific `assetBaseUrl` is compiled into the bundle.
60
61
 
61
- After a live promotion, merge or fast-forward the exact frozen publish commit into the authoritative default branch and push it before closing the task. `openxiangda release integration-status --profile <name>` checks the live remote tip; normal `release end` keeps the reconciliation evidence and returns `RELEASE_MAIN_MERGE_PENDING` until that exact SHA is present. Squash/rebase does not preserve the published commit and therefore does not pass. The authoritative target is always the live default `main`/`master` of `origin` (or the repository's sole remote); CLI flags, environment variables, local-only branches, and lease expiry cannot bypass it. OpenXiangda's own `npm publish` and the platform image release script apply the stronger pre-release form of the same rule: root and pinned submodule commits must already be in their remote default branches.
62
+ Because promotion starts from an already-pushed authoritative mainline commit, `openxiangda release integration-status --profile <name>` should pass immediately after activation. Run it and `release end`; there is no post-release merge step.
62
63
 
63
64
  Source-triggered Function and Automation publishing is source-only by default. Backend Release v2 can mix create, source-only update, and manifest replacement update in one immutable child; a stale target therefore causes zero resource writes instead of failing halfway through 88 Functions and 11 Automations. `--stage-only` stops after verification, reports all handled selectors, and never falls back to direct Function/Automation writes when the platform is missing or incompatible. The release preserves online `resourceBindings`, input/output contracts, descriptive metadata, trigger/view configuration, and enabled/published state, and planned noops do not advance resource versions or timestamps. To intentionally make a complete local manifest replace the online definition, select exact codes and opt in explicitly with `--replace-manifest --reason "<why this replacement is safe>"`; `--sdd-bypass` does not widen that scope. Standard workspaces use the CLI-bundled scoped builder without spawning workspace `pnpm`, and `source_lineage_v1` compares authored source/dependencies independently from generated bundle bytes.
64
65
 
@@ -94,9 +95,9 @@ Domestic npm mirrors may lag and return an older OpenXiangda version. `openxiang
94
95
 
95
96
  Architecture-class requests are plan-gated by default. For new apps, complex pages, login/register, public/no-login access, role/data-scope design, workflow/automation, App Function, connector, notification, and external integration work, AI agents must plan first and implement only after the user confirms the design.
96
97
 
97
- Risk is tiered. Read-only/docs/tests are L0. Narrow reversible copy/style or single existing binding fixes are L1 and use `openxiangda sdd quick` to record exact files/resources plus the user's already-explicit intent; do not add a second proposal/approval ceremony to such a small change. Function quick fixes additionally require the complete negative risk assessment. Schema, business Functions, Automation/Workflow, permissions, auth/public access, data writes, and runtime/config are L2; irreversible production or app-wide changes are L3. L2/L3 use an explicit SDD change from context through plan/check/verify/publish/archive. `verify --stage implementation` deliberately does not block coding on unfinished prose/checklists; plain `verify` stops at the prepublish boundary and never demands future postpublish/archive evidence. Exact argv and staged-child scope become strict only at the write/finalize boundary. Emergency releases can still use `--sdd-bypass --reason "..."` and remain auditable.
98
+ Risk is tiered. Read-only/docs/tests are L0. Narrow reversible fixes are L1 and use `sdd quick`; L2/L3 retain explicit approval and exact structured coverage. SDD is streamlined by default: incomplete task/evidence/spec prose warns but does not block. Workspaces that intentionally require prose completion may set `strictDocumentation: true`. Actual argv, exact scope, mainline identity, lease, CAS, destructive operations, Secrets, and Root App atomic activation remain hard gates.
98
99
 
99
- Every concurrent task should use its own Git worktree/branch. `sdd propose|quick|context` automatically claims the current worktree for the active `CODEX_THREAD_ID`; a second task receives `WORKTREE_OWNED_BY_ANOTHER_TASK` before it can publish through the shared directory. Inspect or explicitly release ownership with `openxiangda workspace ownership status|release`; forced takeover requires a reason. Live `resource publish` requires exact `--only <codes>` or `--code <code>`; an app-wide publish needs explicit `--all --reason`, even when SDD is bypassed. The approved change scope, not the checkout's global dirty set, is the release boundary.
100
+ Every concurrent task uses its own Git worktree/branch and development change. 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.
100
101
 
101
102
  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.
102
103
 
package/lib/cli.js CHANGED
@@ -105,6 +105,7 @@ const {
105
105
  approveSddChange,
106
106
  buildSddGovernanceReport,
107
107
  buildSddStatus,
108
+ createMainlineSddBundle,
108
109
  createQuickSddChange,
109
110
  getSddChangeScope,
110
111
  initSddWorkspace,
@@ -219,7 +220,7 @@ Usage:
219
220
  openxiangda open-api spec tags|list|describe [operationId|path] [--method post] [--tag name] [--search text] [--json]
220
221
  openxiangda open-api credential list|get|create|update|rotate-secret [id] [--profile name] [--show-secret] [--yes] [--json]
221
222
  openxiangda design gates|template|review [--topic code] [--json]
222
- openxiangda sdd init|migrate|propose|quick|approve|status|context|verify|sync|archive [change] [--change id] [--json]
223
+ openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive [change] [--change id] [--json]
223
224
  openxiangda release begin|status|integration-status|renew|end [--change id] [--profile name] [--json]
224
225
  openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]
225
226
  openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--profile name] [--json]
@@ -287,7 +288,7 @@ Usage:
287
288
 
288
289
  OpenXiangda 应用开发使用普通用户登录 token,不需要 AK/SK;外部后端集成使用 open-api skill 与 AK/SK。
289
290
  所有直接配置写入必须携带 --change <id>,或先执行 release begin --change <id>;dry-run、schema-plan、validate、query/invoke 等只读/数据面命令不会获取发布租约。
290
- 正式发布会冻结 clean committed HEAD;上线后必须将该 SHA merge/fast-forward 并 push 到权威远端主分支,integration-status 通过后 normal release end 才会清理会话。
291
+ 正式发布只接受已经合并并 push、且与权威远端默认主分支完全一致的 clean HEAD;激活后直接运行 integration-status release end,不再补做发布后合并。
291
292
  表单页、流程表单页和代码页的主链路是 sy-lowcode-app-workspace + openxiangda workspace publish。
292
293
  新 React SPA 公开访问使用 public-access 命令和 src/resources/public-access;settings public-access 仅用于旧表单公开设置兼容/修复。
293
294
  JS_CODE V2 使用 trusted_node;AI 源码必须写在 src/js-code-nodes/<scriptCode>/index.ts,代码自动化源码写在 src/automations/<resourceCode>/index.ts。definition-json 中的 sourceFile.localPath 会在 validate/create/publish 时先 TS 校验、再构建并上传为快照。`);
@@ -644,12 +645,13 @@ async function sdd(args) {
644
645
  const { flags, positional } = parseArgs(rest);
645
646
  if (wantsSubcommandHelp(subcommand, flags)) {
646
647
  print([
647
- '用法: openxiangda sdd init|migrate|propose|quick|approve|status|context|verify|sync|archive [change] [--change id] [--json]',
648
+ '用法: openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive [change] [--change id] [--json]',
648
649
  '常用流程:',
649
650
  ' openxiangda sdd init --upgrade',
650
651
  ' openxiangda sdd propose add-customer-list --title "客户列表" --forms customer --pages customer_list --resources',
651
652
  ' openxiangda sdd quick tune-customer-copy --kind copy --pages customer_list --files src/pages/customer_list/index.tsx --summary "用户确认小范围文案调整"',
652
653
  ' openxiangda sdd approve add-customer-list --summary "用户确认设计"',
654
+ ' openxiangda sdd bundle mainline-release --changes add-customer-list,fix-order-log',
653
655
  ' openxiangda sdd context --change add-customer-list --json',
654
656
  ' openxiangda sdd verify --change add-customer-list --stage prepublish --json',
655
657
  ' openxiangda sdd archive --change add-customer-list',
@@ -661,7 +663,9 @@ async function sdd(args) {
661
663
  ' - 在 Codex 中 propose/quick/context 会按 CODEX_THREAD_ID 占用当前 worktree;其他任务必须切换到独立 worktree。',
662
664
  ' - quick 仅用于 L1 精确小改动;Function quick 还必须提供完整 --risk-json,不能用 riskTier 标签绕过。',
663
665
  ' - verify stage 可选 implementation、prepublish、postpublish、archive;不传保持旧版全量校验。',
664
- ' - release 会把实际 publish HEAD 写入 release.json;archive(包括 --force)要求该 SHA 已进入权威远端主分支。',
666
+ ' - 默认 streamlined 校验只把 approval、精确资源/文件范围、实际发布命令作为硬门禁;任务、证据和规格文案仅告警。配置 strictDocumentation: true 可恢复严格文档门禁。',
667
+ ' - bundle 聚合多个 approved change;提交并推送该 bundle 后,在权威主分支执行一次 staged + atomic 发布。',
668
+ ' - 新 release 只能从已推送且与权威远端默认主分支一致的 clean HEAD 开始。',
665
669
  ].join('\n'));
666
670
  return;
667
671
  }
@@ -720,6 +724,23 @@ async function sdd(args) {
720
724
  return;
721
725
  }
722
726
 
727
+ if (subcommand === 'bundle') {
728
+ const changeId = positional[0] || flags.change;
729
+ assertOrClaimWorktreeOwner({ cwd: process.cwd(), changeId });
730
+ const result = createMainlineSddBundle({
731
+ cwd: process.cwd(),
732
+ changeId,
733
+ title: flags.title || flags.name,
734
+ changes: flags.changes,
735
+ force: Boolean(flags.force),
736
+ });
737
+ if (flags.json) return writeJson(result);
738
+ print(
739
+ `已创建主线发布 bundle: ${result.change.id}\nchanges=${result.bundledChanges.join(', ')}\n目录: ${result.dir}`
740
+ );
741
+ return;
742
+ }
743
+
723
744
  if (subcommand === 'approve') {
724
745
  const changeId = positional[0] || flags.change;
725
746
  assertOrClaimWorktreeOwner({ cwd: process.cwd(), changeId });
@@ -837,7 +858,7 @@ async function sdd(args) {
837
858
  return;
838
859
  }
839
860
 
840
- fail('用法: openxiangda sdd init|migrate|propose|quick|approve|status|context|verify|sync|archive [change] [--json]');
861
+ fail('用法: openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive [change] [--json]');
841
862
  }
842
863
 
843
864
  function parseSddAffectedFlags(flags) {
@@ -886,8 +907,8 @@ async function release(args) {
886
907
  ' - 带 --change 的 resource/runtime 写命令在没有本地 lease 时会自动 begin,并在后续命令复用。',
887
908
  ' - 子 Runtime/Page/Backend/Form Release 默认先 stage;app-finalize 用 --staged-resources-json 覆盖 capture 中的 changed children,并在同一事务中原子激活。',
888
909
  ' - 默认 TTL 为 1800 秒;长异步发布每 30 秒检查并在到期前自动续租,续租失败会以 PUBLISH_LEASE_LOST 阻断后续写入。',
889
- ' - begin 冻结 clean committed HEAD,并要求它包含权威远端主分支;发布期间 HEAD/源码不可变化。',
890
- ' - 发布后先把冻结提交以保留 SHA 的 merge/fast-forward 合并并 push 到主分支;end 会在线验证后才清理会话。',
910
+ ' - begin 仅接受 clean 且与权威远端默认 main/master 完全一致的 HEAD;feature branch 先合并并 push,再由主分支一次发布。',
911
+ ' - 因发布源已在远端主线,成功激活后 integration-status 会立即通过,end 不再等待补合并。',
891
912
  ' - integration-status --check 可作为 CI 门禁;尚未回合时返回非零。',
892
913
  ' - 主线目标只能来自权威 Git remote 的实时默认 main/master;参数和环境变量不能改写。',
893
914
  ' - 租约过期也会保留待回合证据;完成 merge/push 后仍可执行 end 清理。',
@@ -9120,7 +9141,7 @@ async function runtime(args) {
9120
9141
  ' - runtime 只发布前端产物;表单、流程、通知等资源仍走 resource publish。',
9121
9142
  ' - deploy/activate 会读取当前 Runtime head 并携带 CAS preconditions;head 已变化时平台返回 409,避免静默覆盖。',
9122
9143
  ' - 所有 deploy(含 --no-activate)先获取应用 publish lease;在任何构建/上传前要求 Git clean committed HEAD,并校验当前 HEAD 继承线上 Runtime 源码血缘。',
9123
- ' - release begin 还会冻结实际 publish HEAD 并要求它包含当前权威远端主分支;上线后 merge/push SHA,通过 release integration-status 后再 release end。',
9144
+ ' - release begin 只接受与权威远端默认主分支完全一致的 clean HEAD;先合并/push 所有 approved changes,再一次发布。',
9124
9145
  ' - Runtime 同步构建前把 lease 续到平台最大 TTL;构建/上传后每次写入仍会重新校验,失租不会继续激活。',
9125
9146
  ' - build 不能安全前移到 lease 之前:直传模式的 assetBase 由受保护的 upload-plan 返回,不能用 gateway 路径静默替换。',
9126
9147
  ' - deploy 会输出 lease/head/preflight/build/upload/activate 阶段进度,JSON 结果包含 timings。',
@@ -9190,6 +9211,9 @@ async function runtime(args) {
9190
9211
  }
9191
9212
 
9192
9213
  if (subcommand === 'deploy') {
9214
+ const runtimeBuildCommand = flags['no-build']
9215
+ ? null
9216
+ : assertRuntimeBuildReady(flags['build-command']);
9193
9217
  const scopedRuntimeFiles = flags.change
9194
9218
  ? buildWorkspaceReleasePlan({
9195
9219
  changeId: flags.change,
@@ -9324,7 +9348,7 @@ async function runtime(args) {
9324
9348
  buildId,
9325
9349
  appType: target.appType,
9326
9350
  assetBaseUrl,
9327
- command: flags['build-command'],
9351
+ command: runtimeBuildCommand,
9328
9352
  jsonOutput: Boolean(flags.json),
9329
9353
  })
9330
9354
  );
@@ -9630,9 +9654,34 @@ function runRuntimeBuild(options) {
9630
9654
  if (result.status !== 0) fail(`runtime build 失败: exit ${result.status}`);
9631
9655
  }
9632
9656
 
9657
+ function assertRuntimeBuildReady(explicitCommand) {
9658
+ if (explicitCommand) return String(explicitCommand);
9659
+ const packageFile = path.join(process.cwd(), 'package.json');
9660
+ if (!fs.existsSync(packageFile)) {
9661
+ fail('RUNTIME_BUILD_NOT_READY: 缺少 package.json;尚未获取发布租约');
9662
+ }
9663
+ let packageJson;
9664
+ try {
9665
+ packageJson = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
9666
+ } catch (error) {
9667
+ fail(`RUNTIME_BUILD_NOT_READY: package.json 无法解析: ${error.message}`);
9668
+ }
9669
+ if (!packageJson?.scripts?.build) {
9670
+ fail('RUNTIME_BUILD_NOT_READY: package.json 缺少 scripts.build;尚未获取发布租约');
9671
+ }
9672
+ const modulesDir = path.join(process.cwd(), 'node_modules');
9673
+ if (!fs.existsSync(modulesDir)) {
9674
+ fail(
9675
+ 'RUNTIME_BUILD_DEPENDENCIES_MISSING: 缺少 node_modules;请在开发阶段完成依赖安装,发布命令不会临时安装或修改锁文件'
9676
+ );
9677
+ }
9678
+ return defaultRuntimeBuildCommand();
9679
+ }
9680
+
9633
9681
  function defaultRuntimeBuildCommand() {
9634
- if (fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml'))) return 'pnpm build';
9635
- if (fs.existsSync(path.join(process.cwd(), 'yarn.lock'))) return 'yarn build';
9682
+ // npm run only executes the already-declared script and resolves
9683
+ // node_modules/.bin. Unlike pnpm build, it does not reject a valid worktree
9684
+ // whose node_modules is a symlink into the primary checkout.
9636
9685
  return 'npm run build';
9637
9686
  }
9638
9687
 
@@ -10130,7 +10179,7 @@ async function commands(args) {
10130
10179
  'open-api spec tags|list|describe [operationId|path]',
10131
10180
  'open-api credential list|get|create|update|rotate-secret [id]',
10132
10181
  'design gates|template|review [--topic code]',
10133
- 'sdd init|migrate|propose|approve|status|context|verify|sync|archive',
10182
+ 'sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive',
10134
10183
  'release begin|status|integration-status|renew|end|backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry|app-capture|app-head|app-list|app-detail|app-diff|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort',
10135
10184
  'env',
10136
10185
  'workspace init|bind|plan|check|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
@@ -10168,7 +10217,7 @@ async function commands(args) {
10168
10217
  'Direct live mutation commands should use --dry-run first and --write-manifest when the repository should remain source of truth.',
10169
10218
  'App Function Secret values never belong in a manifest or workspace file. Use top-level function secretRefs, ctx.secrets.get(name), and secret create/rotate via hidden TTY or --value-stdin.',
10170
10219
  'OpenXiangda native SDD uses openspec/ records; high-risk publishes require an approved change with coverage.json/release.json verification unless --sdd-bypass --reason is used.',
10171
- 'A formal release freezes the clean publish HEAD separately from the change base; after activation merge/fast-forward and push that exact SHA to the authoritative default branch, verify release integration-status, then run release end.',
10220
+ 'A formal release only starts from a clean local main/master that exactly equals the authoritative remote tip. Merge and push approved changes first; after activation verify release integration-status, then run release end without a post-release merge.',
10172
10221
  ],
10173
10222
  };
10174
10223
  if (flags.json) return writeJson(manifest);
@@ -20849,9 +20898,9 @@ function getChangeScopedWorkspaceFiles(changeScope, runtimeMode, options = {}) {
20849
20898
  );
20850
20899
  };
20851
20900
  return unique([
20852
- ...(changeScope.ownedFiles || []),
20853
20901
  ...unique([
20854
20902
  ...(changeScope.recordedChangedFiles || []),
20903
+ ...(changeScope.ownedFiles || []),
20855
20904
  ...dirtyFiles,
20856
20905
  ]).filter(file =>
20857
20906
  fileBelongsToChangeScope(file, changeScope, runtimeMode) || dependencyBelongsToScope(file)
@@ -21042,17 +21091,47 @@ function buildWorkspaceReleasePlan(options = {}) {
21042
21091
  runtime: Boolean(changeScope.targets.runtime),
21043
21092
  other: [...(changeScope.targets.other || [])],
21044
21093
  }
21045
- : classifyWorkspaceFiles(files, runtimeMode, {
21094
+ : classifyWorkspaceFiles(files, runtimeMode, {
21046
21095
  baseRef: options.since || 'HEAD',
21047
21096
  });
21097
+ let dependencyImpact = {};
21048
21098
  if (changeScope) {
21049
21099
  const dependencyTargets = getWorkspaceImpactedResourceTargets(files);
21100
+ dependencyImpact = Object.fromEntries(
21101
+ ['functions', 'automations', 'workflows', 'jsCodeNodes'].map(key => [
21102
+ key,
21103
+ (dependencyTargets[key] || []).filter(
21104
+ code => !(targets[key] || []).includes(code)
21105
+ ),
21106
+ ])
21107
+ );
21108
+ // The approved structured scope is authoritative. Shared-source impact is
21109
+ // reported for review, but no longer silently broadens a release into
21110
+ // dozens of unrelated resources. Callers may opt in when the intended
21111
+ // change genuinely requires the whole dependency closure.
21112
+ if (options.expandDependencies) {
21050
21113
  for (const key of ['functions', 'automations', 'workflows', 'jsCodeNodes']) {
21051
21114
  targets[key] = unique([...(targets[key] || []), ...(dependencyTargets[key] || [])]).sort();
21052
21115
  }
21116
+ }
21053
21117
  }
21054
21118
  if (options.includeResources) targets.resources = true;
21055
- const issues = scanWorkspaceReliabilityIssues(files);
21119
+ const issues = [
21120
+ ...scanWorkspaceReliabilityIssues(files),
21121
+ ...Object.entries(dependencyImpact).flatMap(([key, codes]) =>
21122
+ codes.length > 0
21123
+ ? [
21124
+ {
21125
+ rule: 'shared-source-outside-approved-scope',
21126
+ severity: 'warn',
21127
+ file: '(release scope)',
21128
+ line: 0,
21129
+ message: `${key} shared-source impact not included unless explicitly approved: ${codes.join(', ')}`,
21130
+ },
21131
+ ]
21132
+ : []
21133
+ ),
21134
+ ];
21056
21135
  const errors = issues
21057
21136
  .filter(issue => issue.severity === 'error')
21058
21137
  .map(issue => ({
@@ -21065,11 +21144,19 @@ function buildWorkspaceReleasePlan(options = {}) {
21065
21144
  options.profile,
21066
21145
  changeScope?.changeId
21067
21146
  );
21147
+ const knownTargetCount =
21148
+ targets.forms.length +
21149
+ targets.pages.length +
21150
+ (targets.functions || []).length +
21151
+ (targets.automations || []).length +
21152
+ (targets.workflows || []).length +
21153
+ (targets.jsCodeNodes || []).length +
21154
+ (targets.runtime ? 1 : 0);
21068
21155
  const commands =
21069
- changeScope?.commands?.length > 0
21156
+ targets.resources && knownTargetCount === 0 && changeScope?.commands?.length > 0
21070
21157
  ? [...changeScope.commands]
21071
21158
  : generatedCommands;
21072
- if (!changeScope?.commands?.length && targets.resources) {
21159
+ if (commands.length === generatedCommands.length && targets.resources && knownTargetCount === 0) {
21073
21160
  errors.push({
21074
21161
  name: 'sdd-release-generic-resource-selector-required',
21075
21162
  message:
@@ -21077,7 +21164,7 @@ function buildWorkspaceReleasePlan(options = {}) {
21077
21164
  });
21078
21165
  }
21079
21166
  if (
21080
- !changeScope?.commands?.length &&
21167
+ commands.length === generatedCommands.length &&
21081
21168
  (targets.jsCodeNodes || []).length > 0 &&
21082
21169
  (targets.functions || []).length === 0 &&
21083
21170
  (targets.automations || []).length === 0 &&
@@ -21147,6 +21234,7 @@ function buildWorkspaceReleasePlan(options = {}) {
21147
21234
  checks,
21148
21235
  sdd,
21149
21236
  issues,
21237
+ dependencyImpact,
21150
21238
  errors: [...errors, ...sddErrors],
21151
21239
  };
21152
21240
  }
@@ -35,6 +35,20 @@ function prepareReleaseSourceRevision(options = {}) {
35
35
  mainline.remoteName,
36
36
  mainline.branchName
37
37
  );
38
+ if (branch !== mainline.branchName) {
39
+ throw releaseMainlineError(
40
+ 'RELEASE_SOURCE_MAINLINE_REQUIRED',
41
+ `正式发布只能从权威主分支 ${formatMainline(mainline)} 执行;当前分支为 ${branch}。请先把已批准变更合并到主分支并切换过去`,
42
+ { branch, mainline }
43
+ );
44
+ }
45
+ if (sourceRevision.baseCommit !== mainline.tipCommit) {
46
+ throw releaseMainlineError(
47
+ 'RELEASE_SOURCE_MAINLINE_NOT_PUSHED',
48
+ `本地主分支 ${shortCommit(sourceRevision.baseCommit)} 尚未与远端 ${formatMainline(mainline)}@${shortCommit(mainline.tipCommit)} 完全一致;请先 fast-forward/push,再从该提交一次发布`,
49
+ { sourceRevision, mainline }
50
+ );
51
+ }
38
52
  if (
39
53
  !isGitAncestor(cwd, mainline.tipCommit, sourceRevision.baseCommit, {
40
54
  remoteName: mainline.remoteName,
@@ -57,6 +71,7 @@ function prepareReleaseSourceRevision(options = {}) {
57
71
  mainBranch: mainline.branchName,
58
72
  mainTipCommit: mainline.tipCommit,
59
73
  mainlineSource: mainline.source,
74
+ mainlinePolicy: 'publish-from-main-v1',
60
75
  capturedAt: new Date().toISOString(),
61
76
  };
62
77
  }