openxiangda 1.0.203 → 1.0.206

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
@@ -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 profile-specific resource IDs; 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 this pair before creating a candidate/deployment, then forwards it only to exact scoped resource stages. Frozen online heads, change/lease ownership, server CAS, staged children, and the single App finalize remain mandatory. 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` 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 this pair before creating a candidate/deployment, then 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.
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
 
@@ -135,7 +135,7 @@ openxiangda policy render
135
135
  entrypoints. Generated documents carry `OpenXiangda-Policy-Version`; edit the
136
136
  canonical policy first instead of independently changing duplicate instructions.
137
137
 
138
- 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.
138
+ Source-triggered Function and Automation publishing is source-only by default. Backend Release v2 can mix source-backed create, source-free declarative Automation manifest 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. A new Automation with a complete `definitionJson.version="v3"` definition and no `sourceFile` automatically uses manifest create and does not require `--replace-manifest`; incomplete definitions still fail closed. `--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 an existing 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.
139
139
 
140
140
  React SPA templates include stable Vite manual chunk grouping for React, antd, ECharts, editor dependencies, and OpenXiangda runtime/component entrypoints. The SDK keeps large UI dependencies such as `antd-mobile` and `dayjs` external so the application bundler can split them by route. Applications should import from public package entrypoints such as `openxiangda`, `openxiangda/runtime`, and `openxiangda/runtime/react`; do not import `openxiangda/packages/sdk/dist/...` internals to chase bundle size.
141
141
 
@@ -536,18 +536,24 @@ function withManagedReleaseForwardedFlags(stepId, args = [], flags = {}) {
536
536
  forwarded[0] === 'resource' &&
537
537
  forwarded[1] === 'publish' &&
538
538
  (forwarded.includes('--only') || forwarded.includes('--code'));
539
- if (
540
- !isExactResourcePublish ||
541
- !flags['adopt-online-baseline']
542
- ) {
539
+ if (!isExactResourcePublish) {
543
540
  return forwarded;
544
541
  }
545
- forwarded.push('--adopt-online-baseline');
546
- const reason = String(
547
- flags['adoption-reason'] || flags.reason || ''
548
- ).trim();
549
- if (reason) {
550
- forwarded.push('--adoption-reason', reason);
542
+ if (stepId === 'backend-stage' && flags['replace-manifest']) {
543
+ forwarded.push('--replace-manifest');
544
+ const replacementReason = String(flags.reason || '').trim();
545
+ if (replacementReason) {
546
+ forwarded.push('--reason', replacementReason);
547
+ }
548
+ }
549
+ if (flags['adopt-online-baseline']) {
550
+ forwarded.push('--adopt-online-baseline');
551
+ const adoptionReason = String(
552
+ flags['adoption-reason'] || flags.reason || ''
553
+ ).trim();
554
+ if (adoptionReason) {
555
+ forwarded.push('--adoption-reason', adoptionReason);
556
+ }
551
557
  }
552
558
  return forwarded;
553
559
  }
package/lib/cli.js CHANGED
@@ -68,6 +68,7 @@ const {
68
68
  buildWorkspaceReleaseCommands,
69
69
  buildWorkspaceReleaseSteps,
70
70
  commandFromArgs,
71
+ directResourceTypeForKey,
71
72
  normalizeReleaseTargets,
72
73
  releasePlanHash,
73
74
  } = require('./release-plan');
@@ -302,7 +303,7 @@ Usage:
302
303
  openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive [change] [--change id] [--json]
303
304
  openxiangda environment init|attach|bind|swap|policy update|list|status|diff|use [preproduction|production] [--logical-app code] [--environment target] [--json]
304
305
  openxiangda release publish|begin|status|integration-status|renew|end [--change id] [--profile name] [--watch] [--json]
305
- openxiangda release ship|candidate|deploy|test|promote|rollback [--candidate id] [--environment target] [--adopt-online-baseline --adoption-reason text] [--json]
306
+ openxiangda release ship|candidate|deploy|test|promote|rollback [--candidate id] [--environment target] [--replace-manifest --reason text] [--adopt-online-baseline --adoption-reason text] [--json]
306
307
  openxiangda task status --change <id> [--profile name] [--watch] [--json]
307
308
  openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]
308
309
  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]
@@ -3086,6 +3087,8 @@ const RELEASE_SHIP_SUPPORTED_FLAGS = new Set([
3086
3087
  'app-type',
3087
3088
  'since',
3088
3089
  'expand-dependencies',
3090
+ 'replace-manifest',
3091
+ 'reason',
3089
3092
  'adopt-online-baseline',
3090
3093
  'adoption-reason',
3091
3094
  'confirm-production',
@@ -3108,6 +3111,22 @@ function assertReleaseShipFlags(flags = {}) {
3108
3111
  error.unsupportedFlags = unsupported;
3109
3112
  throw error;
3110
3113
  }
3114
+ const replacementRequested = Boolean(flags['replace-manifest']);
3115
+ const replacementReason = String(flags.reason || '').trim();
3116
+ if (!replacementRequested && replacementReason) {
3117
+ const error = new Error(
3118
+ 'RELEASE_SHIP_REPLACE_MANIFEST_FLAG_REQUIRED: --reason 必须与 --replace-manifest 一起使用;命令已在任何 candidate/deployment 写入前终止'
3119
+ );
3120
+ error.code = 'RELEASE_SHIP_REPLACE_MANIFEST_FLAG_REQUIRED';
3121
+ throw error;
3122
+ }
3123
+ if (replacementRequested && replacementReason.length < 8) {
3124
+ const error = new Error(
3125
+ 'RELEASE_SHIP_REPLACE_MANIFEST_REASON_REQUIRED: --replace-manifest 必须提供至少 8 个字符的 --reason;命令已在任何 candidate/deployment 写入前终止'
3126
+ );
3127
+ error.code = 'RELEASE_SHIP_REPLACE_MANIFEST_REASON_REQUIRED';
3128
+ throw error;
3129
+ }
3111
3130
  const adoptionRequested = Boolean(flags['adopt-online-baseline']);
3112
3131
  const adoptionReason = String(flags['adoption-reason'] || '').trim();
3113
3132
  if (!adoptionRequested && adoptionReason) {
@@ -3137,7 +3156,7 @@ async function runApplicationShipCommand(
3137
3156
  readStringFlag(flags, 'change') || positional[0];
3138
3157
  if (!changeId) {
3139
3158
  fail(
3140
- '用法: openxiangda release ship --change <id> [--adopt-online-baseline --adoption-reason "..."] [--confirm-production] [--acceptance-note "..."]'
3159
+ '用法: openxiangda release ship --change <id> [--replace-manifest --reason "..."] [--adopt-online-baseline --adoption-reason "..."] [--confirm-production] [--acceptance-note "..."]'
3141
3160
  );
3142
3161
  }
3143
3162
  const logicalApp = requireLogicalApplication(initialTarget);
@@ -3153,6 +3172,26 @@ async function runApplicationShipCommand(
3153
3172
  );
3154
3173
  const existingShip = readManagedShip(changeId);
3155
3174
  const confirmProduction = Boolean(flags['confirm-production']);
3175
+ const manifestReplacement = {
3176
+ enabled: Boolean(flags['replace-manifest']),
3177
+ reason: String(flags.reason || '').trim() || null,
3178
+ };
3179
+
3180
+ if (existingShip?.candidateId) {
3181
+ const recordedReplacement = existingShip.manifestReplacement || {
3182
+ enabled: false,
3183
+ reason: null,
3184
+ };
3185
+ if (
3186
+ Boolean(recordedReplacement.enabled) !== manifestReplacement.enabled ||
3187
+ String(recordedReplacement.reason || '') !==
3188
+ String(manifestReplacement.reason || '')
3189
+ ) {
3190
+ fail(
3191
+ 'RELEASE_SHIP_REPLACE_MANIFEST_INTENT_MISMATCH: --replace-manifest/--reason 必须与该 candidate 的预发发布意图完全一致;请使用 ship.json 记录的同一对参数'
3192
+ );
3193
+ }
3194
+ }
3156
3195
 
3157
3196
  if (confirmProduction && !existingShip?.preproductionDeploymentId) {
3158
3197
  fail(
@@ -3184,6 +3223,7 @@ async function runApplicationShipCommand(
3184
3223
  candidateHash: candidate.candidateHash,
3185
3224
  sourceRevision: candidate.sourceRevision,
3186
3225
  candidateFile: created.candidateFile,
3226
+ manifestReplacement,
3187
3227
  });
3188
3228
  } else {
3189
3229
  assertLocalCandidate(candidate);
@@ -3220,12 +3260,18 @@ async function runApplicationShipCommand(
3220
3260
  preproductionAppReleaseId:
3221
3261
  deployment.targetAppReleaseId || null,
3222
3262
  preproductionStatus: deployment.status,
3263
+ manifestReplacement,
3223
3264
  preparedAt:
3224
3265
  existingShip?.preparedAt ||
3225
3266
  new Date().toISOString(),
3226
3267
  nextCommand:
3227
3268
  `openxiangda release ship --change ${changeId} ` +
3228
- '--confirm-production',
3269
+ '--confirm-production' +
3270
+ (manifestReplacement.enabled
3271
+ ? ` --replace-manifest --reason ${JSON.stringify(
3272
+ manifestReplacement.reason
3273
+ )}`
3274
+ : ''),
3229
3275
  });
3230
3276
  return {
3231
3277
  phase: 'preproduction',
@@ -3568,12 +3614,13 @@ async function release(args) {
3568
3614
  if (wantsSubcommandHelp(subcommand, flags)) {
3569
3615
  print([
3570
3616
  '用法: openxiangda release publish|begin|status|explain|integration-status|renew|end [--change id] [--profile name] [--json]',
3571
- ' openxiangda release ship|candidate|deploy|reconcile|test|promote|rollback [--candidate id] [--environment target] [--adopt-online-baseline --adoption-reason text] [--confirm-production] [--json]',
3617
+ ' openxiangda release ship|candidate|deploy|reconcile|test|promote|rollback [--candidate id] [--environment target] [--replace-manifest --reason text] [--adopt-online-baseline --adoption-reason text] [--confirm-production] [--json]',
3572
3618
  ' openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]',
3573
3619
  ' openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-post-commit|app-retry|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--profile name] [--json]',
3574
3620
  '常用流程:',
3575
3621
  ' openxiangda release ship --change <id> --profile <name>',
3576
3622
  ' openxiangda release ship --change <id> --profile <name> --adopt-online-baseline --adoption-reason "已审计目标来自多次历史发布"',
3623
+ ' openxiangda release ship --change <id> --profile <name> --replace-manifest --reason "已审查精确 Function manifest"',
3577
3624
  ' # 确认预发结果后;可选 --acceptance-note 留下人工验收记录:',
3578
3625
  ' openxiangda release ship --change <id> --profile <name> --confirm-production',
3579
3626
  ' openxiangda release publish --change <id> --profile <name>',
@@ -3589,6 +3636,7 @@ async function release(args) {
3589
3636
  '说明:',
3590
3637
  ' - ship 是环境托管应用的两段式快车道:首次只部署预发并停止;确认预发结果后,第二次以同一 candidate 晋级正式。人工验收备注可选。',
3591
3638
  ' - 已合入主线但线上来自多次历史 lineage 的精确非删除恢复可显式使用 --adopt-online-baseline 与 --adoption-reason;仍保留冻结 Head、CAS、租约、staged children 和单次 App finalize。',
3639
+ ' - ship 的 --replace-manifest 与 --reason 必须成对,只透传给精确 Backend Function/Automation stage;正式确认必须复用与预发完全相同的参数。',
3592
3640
  ' - ship 当前不支持 --dry-run;任何未声明参数都会在 candidate/deployment 写入前 fail-closed。需要只读检查时使用 environment status/diff 与 release status。',
3593
3641
  ' - publish 默认等待租约并按私有执行日志恢复;上次写结果不确定时必须只读核对后显式 --resume-after-review。',
3594
3642
  ' - app-head 默认只输出紧凑 head 摘要;需要完整 manifest 时显式追加 --full。',
@@ -12921,8 +12969,21 @@ function buildResourceManifestSddTargets(manifest, planActions = []) {
12921
12969
  'workflows',
12922
12970
  'formSettings',
12923
12971
  ]);
12972
+ const resourceSelectors = Object.fromEntries(
12973
+ RESOURCE_SPECS
12974
+ .filter(
12975
+ spec =>
12976
+ !codeResourceKeys.has(spec.key) &&
12977
+ directResourceTypeForKey(spec.key)
12978
+ )
12979
+ .map(spec => [spec.key, codes(spec.key)])
12980
+ .filter(([, selectedCodes]) => selectedCodes.length > 0)
12981
+ );
12924
12982
  const hasGenericResources = RESOURCE_SPECS.some(
12925
- spec => !codeResourceKeys.has(spec.key) && (manifest[spec.key] || []).length > 0
12983
+ spec =>
12984
+ !codeResourceKeys.has(spec.key) &&
12985
+ !directResourceTypeForKey(spec.key) &&
12986
+ (manifest[spec.key] || []).length > 0
12926
12987
  ) || (planActions || []).some(
12927
12988
  action =>
12928
12989
  action?.action === 'delete' &&
@@ -12939,6 +13000,7 @@ function buildResourceManifestSddTargets(manifest, planActions = []) {
12939
13000
  workflows: unique([...codes('workflows'), ...deletedCodes('workflow')]).sort(),
12940
13001
  jsCodeNodes: [],
12941
13002
  resources: hasGenericResources,
13003
+ resourceSelectors,
12942
13004
  runtime: false,
12943
13005
  other: [],
12944
13006
  };
@@ -17840,6 +17902,66 @@ function buildAutomationBackendReleaseSources(definition, label) {
17840
17902
  return sources;
17841
17903
  }
17842
17904
 
17905
+ function isCompleteDeclarativeAutomationDefinition(definition) {
17906
+ if (
17907
+ !definition ||
17908
+ typeof definition !== 'object' ||
17909
+ Array.isArray(definition) ||
17910
+ definition.version !== 'v3' ||
17911
+ !Array.isArray(definition.nodes) ||
17912
+ definition.nodes.length === 0 ||
17913
+ !Array.isArray(definition.edges)
17914
+ ) {
17915
+ return false;
17916
+ }
17917
+ const nodeIds = new Set();
17918
+ let startCount = 0;
17919
+ for (const node of definition.nodes) {
17920
+ if (
17921
+ !node ||
17922
+ typeof node !== 'object' ||
17923
+ !String(node.id || '').trim() ||
17924
+ !String(node.type || '').trim() ||
17925
+ !node.data ||
17926
+ typeof node.data !== 'object' ||
17927
+ Array.isArray(node.data)
17928
+ ) {
17929
+ return false;
17930
+ }
17931
+ const nodeId = String(node.id);
17932
+ if (nodeIds.has(nodeId)) return false;
17933
+ nodeIds.add(nodeId);
17934
+ if (node.type === 'start') startCount += 1;
17935
+ }
17936
+ if (startCount !== 1) return false;
17937
+ return definition.edges.every(
17938
+ edge =>
17939
+ edge &&
17940
+ typeof edge === 'object' &&
17941
+ String(edge.id || '').trim() &&
17942
+ nodeIds.has(String(edge.source || '')) &&
17943
+ nodeIds.has(String(edge.target || ''))
17944
+ );
17945
+ }
17946
+
17947
+ function resolveAutomationCreateBackendReleaseMode(candidate, definition) {
17948
+ if (
17949
+ candidate.operation !== 'create' ||
17950
+ candidate.mode === 'manifest_replacement'
17951
+ ) {
17952
+ return candidate.mode;
17953
+ }
17954
+ if (buildAutomationSourcePatchSources({}, definition).length > 0) {
17955
+ return 'source_only';
17956
+ }
17957
+ if (isCompleteDeclarativeAutomationDefinition(definition)) {
17958
+ return 'manifest_replacement';
17959
+ }
17960
+ fail(
17961
+ `Automation:${candidate.code} 新建资源既不包含可发布的源码快照,也不包含完整的 definitionJson.version=v3 声明式定义`
17962
+ );
17963
+ }
17964
+
17843
17965
  async function buildBackendReleaseResourceInput(
17844
17966
  config,
17845
17967
  target,
@@ -17930,21 +18052,37 @@ async function buildBackendReleaseResourceInput(
17930
18052
  true,
17931
18053
  uploadCache
17932
18054
  ));
18055
+ const mode = resolveAutomationCreateBackendReleaseMode(
18056
+ candidate,
18057
+ definitionJson
18058
+ );
18059
+ candidate.mode = mode;
17933
18060
  const common = {
17934
18061
  kind: 'Automation',
17935
18062
  code: candidate.code,
17936
- mode: candidate.mode,
18063
+ mode,
17937
18064
  operation: candidate.operation,
17938
18065
  };
17939
- if (candidate.mode === 'manifest_replacement') {
18066
+ if (mode === 'manifest_replacement') {
17940
18067
  return {
17941
18068
  ...common,
17942
- replacement: buildBackendReleaseAutomationMetadata(
17943
- target,
17944
- candidate.item,
17945
- definitionJson,
17946
- viewJson
17947
- ),
18069
+ ...(candidate.operation === 'create'
18070
+ ? {
18071
+ create: buildBackendReleaseAutomationMetadata(
18072
+ target,
18073
+ candidate.item,
18074
+ definitionJson,
18075
+ viewJson
18076
+ ),
18077
+ }
18078
+ : {
18079
+ replacement: buildBackendReleaseAutomationMetadata(
18080
+ target,
18081
+ candidate.item,
18082
+ definitionJson,
18083
+ viewJson
18084
+ ),
18085
+ }),
17948
18086
  };
17949
18087
  }
17950
18088
  return {
@@ -18343,9 +18481,6 @@ async function publishBackendReleaseResources(
18343
18481
  `${candidate.kind === 'Function' ? 'function' : 'automation'}:${candidate.code}`
18344
18482
  )
18345
18483
  .sort();
18346
- const resourceModes = unique(candidates.map(candidate => candidate.mode));
18347
- const releaseMode =
18348
- resourceModes.length > 1 ? 'mixed' : resourceModes[0] || 'source_only';
18349
18484
  const replacementReason = String(
18350
18485
  options.publishLeaseFlags?.reason ||
18351
18486
  options.publishLeaseFlags?.['replace-reason'] ||
@@ -18403,6 +18538,14 @@ async function publishBackendReleaseResources(
18403
18538
  uploadCache
18404
18539
  )
18405
18540
  );
18541
+ const resourceModes = unique(resources.map(resource => resource.mode));
18542
+ const releaseMode =
18543
+ resourceModes.length > 1 ? 'mixed' : resourceModes[0] || 'source_only';
18544
+ const requiresReplacementReason = resources.some(
18545
+ resource =>
18546
+ resource.mode === 'manifest_replacement' &&
18547
+ resource.operation === 'update'
18548
+ );
18406
18549
  const prepared = await requestWithAuth(
18407
18550
  config,
18408
18551
  target.profileName,
@@ -18414,7 +18557,7 @@ async function publishBackendReleaseResources(
18414
18557
  ...(protocolVersion ? { protocolVersion } : {}),
18415
18558
  parentReleaseId: head?.activeBackendReleaseId || null,
18416
18559
  mode: releaseMode,
18417
- ...(releaseMode !== 'source_only'
18560
+ ...(requiresReplacementReason
18418
18561
  ? { replacementReason }
18419
18562
  : {}),
18420
18563
  sourceRepositoryId: releaseContext.sourceRepositoryId,
@@ -26907,5 +27050,6 @@ function buildWorkspacePublishEnv(
26907
27050
  }
26908
27051
 
26909
27052
  module.exports = {
27053
+ buildResourceManifestSddTargets,
26910
27054
  main,
26911
27055
  };
@@ -21,6 +21,14 @@ function normalizeReleaseTargets(targets = {}, runtimeMode = 'legacy') {
21
21
  .map(([type, values]) => [type, uniqueSorted(values || [])])
22
22
  .filter(([, values]) => values.length > 0)
23
23
  );
24
+ const hasExactDirectResourceScope =
25
+ Object.entries(resourceSelectors).length > 0 &&
26
+ Object.entries(resourceSelectors).every(
27
+ ([type, selectors]) =>
28
+ directResourceTypeForKey(type) &&
29
+ type !== 'unknown' &&
30
+ (selectors || []).every(selector => selector !== '*')
31
+ );
24
32
  const logicalTargets = {
25
33
  forms: uniqueSorted(targets.forms || []),
26
34
  pages: uniqueSorted(targets.pages || []),
@@ -28,7 +36,11 @@ function normalizeReleaseTargets(targets = {}, runtimeMode = 'legacy') {
28
36
  automations: uniqueSorted(targets.automations || []),
29
37
  workflows: uniqueSorted(targets.workflows || []),
30
38
  jsCodeNodes: uniqueSorted(targets.jsCodeNodes || []),
31
- resources: Boolean(targets.resources),
39
+ // Older SDD records used resources=true as a category marker even when
40
+ // they also carried an exact selector. Exact supported selectors are the
41
+ // authoritative boundary; only an unscoped/unknown/wildcard declaration
42
+ // remains a generic resource closure.
43
+ resources: Boolean(targets.resources) && !hasExactDirectResourceScope,
32
44
  resourceSelectors,
33
45
  resourceDeletes,
34
46
  runtime: Boolean(targets.runtime),
@@ -56,7 +68,9 @@ function unsupportedAtomicReleaseTargets(targets = {}) {
56
68
  )) {
57
69
  if (
58
70
  (selectors || []).length > 0 &&
59
- (!DIRECT_RESOURCE_TYPE_BY_KEY[type] || type === 'unknown')
71
+ (!directResourceTypeForKey(type) ||
72
+ type === 'unknown' ||
73
+ selectors.includes('*'))
60
74
  ) {
61
75
  unsupported.push(`${type}:${selectors.join(',')}`);
62
76
  }
@@ -114,6 +128,10 @@ const DIRECT_RESOURCE_RELEASE_ORDER = Object.freeze([
114
128
  'notifications',
115
129
  ]);
116
130
 
131
+ function directResourceTypeForKey(key) {
132
+ return DIRECT_RESOURCE_TYPE_BY_KEY[key] || null;
133
+ }
134
+
117
135
  function assertAtomicReleaseTargetsSupported(targets) {
118
136
  const unsupported = unsupportedAtomicReleaseTargets(targets);
119
137
  if (unsupported.length === 0) return;
@@ -238,7 +256,7 @@ function buildDirectConfigurationReleaseSteps(
238
256
  return Object.entries(targets.resourceSelectors || {})
239
257
  .filter(
240
258
  ([type, selectors]) =>
241
- DIRECT_RESOURCE_TYPE_BY_KEY[type] &&
259
+ directResourceTypeForKey(type) &&
242
260
  uniqueSorted(selectors || []).length > 0
243
261
  )
244
262
  .sort(
@@ -248,12 +266,12 @@ function buildDirectConfigurationReleaseSteps(
248
266
  )
249
267
  .map(([type, selectors]) =>
250
268
  createStep(
251
- `config-${DIRECT_RESOURCE_TYPE_BY_KEY[type]}`,
269
+ `config-${directResourceTypeForKey(type)}`,
252
270
  appendProfileAndChange(
253
271
  [
254
272
  'resource',
255
273
  'publish',
256
- DIRECT_RESOURCE_TYPE_BY_KEY[type],
274
+ directResourceTypeForKey(type),
257
275
  '--only',
258
276
  uniqueSorted(selectors).join(','),
259
277
  ],
@@ -476,6 +494,7 @@ module.exports = {
476
494
  buildWorkspaceReleaseCommands,
477
495
  buildWorkspaceReleaseSteps,
478
496
  commandFromArgs,
497
+ directResourceTypeForKey,
479
498
  normalizeReleaseTargets,
480
499
  releasePlanHash,
481
500
  unsupportedAtomicReleaseTargets,
@@ -108,12 +108,16 @@ For a workspace registered by `openxiangda environment init` or connected to an
108
108
 
109
109
  For an audited catch-up whose exact non-delete targets are already merged but whose active resources combine multiple historical release lineages, the first ship invocation may add `--adopt-online-baseline --adoption-reason "..."`. Ship validates the pair before candidate/deployment creation and forwards it only to exact scoped resource stages; frozen online heads, change/lease ownership, delete/prune/force rejection, server CAS, staged-child verification, and the single atomic App finalize remain mandatory.
110
110
 
111
+ When an environment-managed release intentionally replaces complete Function/Automation manifests, `release ship` may add the inseparable `--replace-manifest --reason "..."` pair. The reason must be at least 8 characters; only an exact Backend selector receives it, and the production confirmation must repeat the exact preproduction pair. It never widens Form, Workflow, Runtime, configuration, wildcard, or app-wide stages.
112
+
111
113
  `resource plan` and publish dry-runs are strictly GET/HEAD-only. `READ_ONLY_AUTH_REQUIRED` means the access token expired; run `openxiangda auth refresh --profile <name>` or log in again before retrying. Never add an automatic refresh POST inside a plan.
112
114
 
113
115
  `release publish` is the default promotion entrypoint only for legacy unmanaged workspaces. It verifies without rewriting reviewed `change.json`/`release.json`, waits for the app lease, freezes the App capture after ownership is acquired, executes deterministic exact staged steps, resumes from `.openxiangda/releases/<change>/execution.json`, atomically finalizes, verifies mainline integration, and releases the lease. Environment-managed applications use the two-phase `release ship`; candidate/deploy/test/promote, `release begin`, and child commands remain recovery/diagnostic primitives.
114
116
 
115
117
  Reviewed bundle commands may retain `<profile>` as a template. The explicit real `release publish --profile <name>` value is bound to actual child argv without rewriting tracked SDD. React SPA page codes are logical coverage targets and activate through one Runtime child; they do not require PageRelease. `release app-head` and `runtime releases` are compact by default; use `--full` only when the complete manifest is required.
116
118
 
119
+ Exact `resourceSelectors` are authoritative for supported configuration resources such as `publicAccessPolicies`. A legacy `resources=true` category marker is narrowed by those exact selectors and must not become an app-wide generic-resource release. Missing selectors, unknown resource types, wildcard `*`, deletes, and genuine app-wide resource closures remain fail closed.
120
+
117
121
  `release begin --change` freezes a clean committed `HEAD` only when the current branch is the authoritative default `main`/`master` and its commit exactly equals the live remote tip. Before any live write, the CLI preflights the complete target set and rejects source changes during the release. A feature worktree or unpushed main receives `RELEASE_SOURCE_MAINLINE_REQUIRED` / `RELEASE_SOURCE_MAINLINE_NOT_PUSHED`; merge, test, push, and start the one mainline release instead of forcing it. Optional `.git` remote suffix differences are aliases of the same repository; genuinely different remotes still fail closed.
118
122
 
119
123
  Because promotion begins from the already-pushed authoritative mainline, `release integration-status` should pass immediately after activation. Run it, then `release end`; no post-release branch merge is required.
@@ -122,7 +126,7 @@ Because promotion begins from the already-pushed authoritative mainline, `releas
122
126
 
123
127
  For a whole-app release, keep changed Runtime/Page/Backend/Form children staged and run `openxiangda release app-finalize --change <change> --staged-resources-json <JSON|file> --profile <name>`. The JSON contains only changed immutable child entries; the CLI overlays Runtime/Page/Backend by singleton kind and FormRelease by `formUuid` onto one authoritative read-only capture, preserving every unmodified active child. It then performs `prepare -> verify -> activateStagedChildren=true`, switching child heads and the App head atomically (`atomic_staged_children_v1`, or `atomic_staged_children_v2` when a Backend Release v2 child is present). Never refresh or retry after a conflict. The flow requires the owned stored lease/change baseline and carries the same client session and Git lineage on every write. Omitting the overlay is compatibility-only retrospective aggregation of already-active children.
124
128
 
125
- When a Function or Automation is selected because its TypeScript source changed, publishing is source-only by default. Backend Release v2 accepts per-resource create, source-only update, and manifest replacement update in one immutable child and one database transaction, so a stale member produces zero resource writes and noops do not advance versions/timestamps. `--stage-only` is fail-closed: every selected mutation must enter that child, and a missing/incompatible Backend Release API never falls back to direct writes. Online bindings, input/output contracts, metadata, trigger/view configuration, and enabled/published state remain unchanged unless exact `--replace-manifest --reason "..."` authority was provided.
129
+ When a Function or Automation is selected because its TypeScript source changed, publishing is source-only by default. Backend Release v2 accepts source-backed create, source-free declarative Automation manifest create, source-only update, and manifest replacement update in one immutable child and one database transaction, so a stale member produces zero resource writes and noops do not advance versions/timestamps. A new Automation with a complete `definitionJson.version="v3"` and no `sourceFile` automatically uses manifest create without `--replace-manifest`; an incomplete definition still fails closed. `--stage-only` is fail-closed: every selected mutation must enter that child, and a missing/incompatible Backend Release API never falls back to direct writes. Online bindings, input/output contracts, metadata, trigger/view configuration, and enabled/published state remain unchanged unless exact `--replace-manifest --reason "..."` authority was provided for an existing resource.
126
130
 
127
131
  An App Function may declare metadata-only top-level `secretRefs: [{ name, required }]` only with `function_v2` + `trusted_node_v2`; source resolves values with `await ctx.secrets.get(name)` and uses `ctx.utils.http` for controlled public HTTPS. Create/rotate values through hidden TTY or `openxiangda secret ... --value-stdin --change <change> --profile <name>`. Never put values in arguments, files, manifests, state, plans, logs, errors, or chat. Secret bindings require `backend_release_v2` and whole-app `atomic_staged_children_v2`; a missing capability is fail-closed and never uses the legacy source PATCH. For whole-app activation use exact-scope `resource publish <type> --only <code> --stage-only`, then pass the returned verified `stagedResource` to `release app-finalize`; an active Backend Release is never labeled staged.
128
132
 
@@ -61,7 +61,7 @@ Selectors apply before manifest parsing, source dependency analysis, and JS_CODE
61
61
 
62
62
  Function/Automation source analysis also extracts statically declared Form filter and order fields. `resource plan` compares them with each bound Form's frozen online schema and reports `formFieldContracts`; publish fails before lease/write when a binding or field is missing. Fix the source or stage the Form schema in the same reviewed release instead of waiting for a production SQL-column error. Dynamic field names remain runtime-validated by the platform and return `FORM_FIELD_NOT_FOUND` as a configuration error.
63
63
 
64
- Source-triggered Function/Automation targets use Backend Release v2 when the platform exposes that capability. One child may mix create, source-only update, and manifest replacement update through explicit per-resource `operation/mode`; the CLI freezes the current Backend Release parent plus Git/change baseline, then runs `prepare -> verify -> activate` or stops verified for `--stage-only`. Activation CAS-checks the entire set and applies all updates in one transaction. `--stage-only` and Secret-bound Function publishing fail closed when Backend Release v2 is unavailable; compatibility fallback is limited to non-staged, non-Secret publishing after an explicit Backend head 404. Existing online bindings, contracts, metadata, trigger/view configuration, and enabled/published state remain unchanged; noops do not advance versions/timestamps. A deliberate whole-definition replacement requires exact `--only/--code` and `--replace-manifest --reason "<why>"`; SDD bypass does not imply replacement authority.
64
+ Source-triggered Function/Automation targets use Backend Release v2 when the platform exposes that capability. One child may mix source-backed create, source-free declarative Automation manifest create, source-only update, and manifest replacement update through explicit per-resource `operation/mode`; the CLI freezes the current Backend Release parent plus Git/change baseline, then runs `prepare -> verify -> activate` or stops verified for `--stage-only`. A new Automation with a complete `definitionJson.version="v3"` and no `sourceFile` automatically uses manifest create without `--replace-manifest`; an incomplete definition still fails closed. Activation CAS-checks the entire set and applies all updates in one transaction. `--stage-only` and Secret-bound Function publishing fail closed when Backend Release v2 is unavailable; compatibility fallback is limited to non-staged, non-Secret publishing after an explicit Backend head 404. Existing online bindings, contracts, metadata, trigger/view configuration, and enabled/published state remain unchanged; noops do not advance versions/timestamps. A deliberate whole-definition replacement of an existing resource requires exact `--only/--code` and `--replace-manifest --reason "<why>"`; SDD bypass does not imply replacement authority.
65
65
 
66
66
  Functions with a top-level `secretRefs` field use `backend_release_v2`, including an explicit empty list that removes bindings. This path never falls back to source PATCH. It requires the per-app Secret capability probe to grant `app_function_secrets_v1`, `trusted_node_v2`, `backend_release_v2`, and `atomic_staged_children_v2`; otherwise plan/publish fails closed.
67
67
 
@@ -109,10 +109,14 @@ Once `environment init` registers a logical application, or `environment attach`
109
109
 
110
110
  When a reviewed catch-up contains exact non-delete targets that are already on authoritative mainline but the active application combines several historical release lineages, the first ship invocation may add `--adopt-online-baseline --adoption-reason "..."`. Ship validates the pair before candidate/deployment creation and forwards it only to exact scoped resource stages. It does not relax frozen online heads, change/lease ownership, delete/prune/force rejection, server CAS, staged-child verification, or the single atomic App finalize.
111
111
 
112
+ An environment-managed release may intentionally replace complete Function/Automation manifests with `release ship --replace-manifest --reason "..."`. The two flags are inseparable, the reason is at least 8 characters, only an exact Backend stage receives them, and production confirmation must repeat the exact preproduction intent. They never widen Form, Workflow, Runtime, configuration, wildcard, or app-wide stages.
113
+
112
114
  `release publish` is the normal whole-app entrypoint for legacy unmanaged workspaces: it verifies SDD without mutating reviewed files, waits for the promotion lease, freezes one App capture, stages the exact Form/Backend/Runtime children, resumes from a private execution journal, finalizes once, and releases the lease. Managed applications use two-phase `release ship` and its deployment-scoped journal. Individual candidate/deploy/test/promote commands are recovery/diagnostic primitives.
113
115
 
114
116
  Reviewed bundle commands may retain `<profile>` as a template. The explicit real `release publish --profile <name>` value is bound to actual child argv without rewriting tracked SDD. React SPA page codes remain logical coverage targets and activate through the single Runtime child; they do not require PageRelease. If local lease state disappears, `release end --change <id>` reconciles a self-owned remote lease from the private execution journal and never reports inactive while a remote lease is active.
115
117
 
118
+ Exact `resourceSelectors` are authoritative for supported configuration resources such as `publicAccessPolicies`. A legacy `resources=true` category marker is narrowed by those exact selectors and must not become an app-wide generic-resource release. Missing selectors, unknown resource types, wildcard `*`, deletes, and genuine app-wide resource closures remain fail closed.
119
+
116
120
  `runtime deploy --no-activate` uploads an immutable preview release from a clean committed mainline `HEAD`. It reads a narrow Runtime head instead of the full app snapshot. Before acquiring a lease, the CLI confirms `package.json#scripts.build` and existing dependencies; it uses `npm run build` to execute the declared script so pnpm worktree symlinks do not trigger a reinstall. Immutable Git-base artifact hashes are cached under the Git common directory. `release begin` requires local main/master and the live remote default tip to be identical, so `integration-status` is already satisfied after activation and `release end` does not wait for a later merge.
117
121
 
118
122
  `release app-head` and `runtime releases` return compact summaries by default. Use `--full` only when the complete manifest is required.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.203",
3
+ "version": "1.0.206",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -28,5 +28,5 @@ openxiangda resource typegen --profile <name>
28
28
  - Formal changes should keep Git as the source of truth: edit manifests, validate, plan, then publish.
29
29
  - Exact selectors apply before manifest/source analysis and JS_CODE build: touch only selected targets plus transitive/shared/ambient dependencies; omit selectors only for intentional full-workspace work.
30
30
  - `resource plan` and publish dry-runs are GET/HEAD-only. On `READ_ONLY_AUTH_REQUIRED`, run `openxiangda auth refresh --profile <name>` or log in again before retrying; never refresh inside the plan.
31
- - Source-triggered Function/Automation publishing patches only source fields on the server and preserves online bindings/contracts/metadata/state. Whole-manifest replacement requires `--replace-manifest --reason "..."`.
31
+ - Source-triggered Function/Automation publishing patches only source fields on the server and preserves online bindings/contracts/metadata/state. A new source-free Automation with a complete `definitionJson.version="v3"` automatically uses manifest create; replacing an existing whole manifest requires `--replace-manifest --reason "..."`.
32
32
  - Formal promotion freezes the clean publish HEAD separately from the change base, preflights the complete set, and rejects `SOURCE_BASE_DIVERGED` / `RELEASE_SOURCE_BEHIND_MAIN` / `RESOURCE_FIELD_CONFLICT`. After activation, merge/fast-forward and push the frozen SHA, verify `release integration-status`, then run normal `release end`; squash/rebase does not preserve the released source.
@@ -33,7 +33,8 @@ This is an OpenXiangda React SPA workspace. See [AGENTS.md](mdc:AGENTS.md) for f
33
33
  - Roles that create roles, assign members, grant API permissions, maintain permission groups, or manage organization accounts must declare `apiPermissionCodes`, such as `app:role:manage`, `app:page-permission-group:manage`, `app:form-permission-group:manage`, and `app:organization:manage`.
34
34
  - `src/resources/**` is the resource source of truth; use `validate -> plan -> publish`.
35
35
  - Select logical resource codes with `--only`, or one code with `--code`; type-wide/app-wide release requires an approved dependency closure.
36
- - Source-triggered Function/Automation publishing uses server-side source-field PATCH and preserves online bindings, contracts, metadata, trigger/view configuration, and enabled/published state. Whole-manifest replacement requires `--replace-manifest --reason "..."`. On `SOURCE_BASE_DIVERGED` or `RESOURCE_FIELD_CONFLICT`, reconcile, rebuild, and re-plan.
36
+ - Source-triggered Function/Automation publishing uses server-side source-field PATCH and preserves online bindings, contracts, metadata, trigger/view configuration, and enabled/published state. A new source-free Automation with a complete `definitionJson.version="v3"` automatically uses Backend Release manifest create without `--replace-manifest`; replacing an existing whole manifest requires `--replace-manifest --reason "..."`. On `SOURCE_BASE_DIVERGED` or `RESOURCE_FIELD_CONFLICT`, reconcile, rebuild, and re-plan.
37
+ - Managed `release ship --replace-manifest --reason "..."` requires the pair, reason length >= 8, and exact Backend selectors; production confirmation repeats the exact preproduction pair, with no forwarding to Form/Workflow/Runtime/config/all stages.
37
38
  - React routes live in `src/app/router.tsx`; frontend artifacts are deployed with `openxiangda runtime deploy`.
38
39
  - Use `AttachmentField` / `ImageField` for form-context previews, and `AttachmentPreviewList` / `ImagePreviewGrid` / `useFilePreview` for standalone custom pages. Do not fake form context, import internal preview implementations, or maintain local previewable-extension lists.
39
40
  - Backend permissions, public-access grants, and App Function role/scope checks are authoritative.
@@ -28,5 +28,5 @@ openxiangda resource typegen --profile <name>
28
28
  - Formal changes should keep Git as the source of truth: edit manifests, validate, plan, then publish.
29
29
  - Exact selectors apply before manifest/source analysis and JS_CODE build: touch only selected targets plus transitive/shared/ambient dependencies; omit selectors only for intentional full-workspace work.
30
30
  - `resource plan` and publish dry-runs are GET/HEAD-only. On `READ_ONLY_AUTH_REQUIRED`, run `openxiangda auth refresh --profile <name>` or log in again before retrying; never refresh inside the plan.
31
- - Source-triggered Function/Automation publishing patches only source fields on the server and preserves online bindings/contracts/metadata/state. Whole-manifest replacement requires `--replace-manifest --reason "..."`.
31
+ - Source-triggered Function/Automation publishing patches only source fields on the server and preserves online bindings/contracts/metadata/state. A new source-free Automation with a complete `definitionJson.version="v3"` automatically uses manifest create; replacing an existing whole manifest requires `--replace-manifest --reason "..."`.
32
32
  - Formal promotion freezes the clean publish HEAD separately from the change base, preflights the complete set, and rejects `SOURCE_BASE_DIVERGED` / `RELEASE_SOURCE_BEHIND_MAIN` / `RESOURCE_FIELD_CONFLICT`. After activation, merge/fast-forward and push the frozen SHA, verify `release integration-status`, then run normal `release end`; squash/rebase does not preserve the released source.
@@ -29,7 +29,8 @@ This is an OpenXiangda React SPA workspace. Read [AGENTS.md](AGENTS.md) for full
29
29
  - 角色能新增角色、分配成员、授接口权限、维护权限组或管理组织账号时,角色资源必须声明 `apiPermissionCodes`,例如 `app:role:manage`、`app:page-permission-group:manage`、`app:form-permission-group:manage`、`app:organization:manage`。
30
30
  - `src/resources/**` 是工程化资源来源,正式多资源变更走 `validate -> plan -> publish`。
31
31
  - 默认按逻辑资源 code 使用 `--only` 或单资源 `--code`;全类型/全应用发布必须由批准的依赖闭包明确覆盖。
32
- - Function/Automation 源码触发默认走服务端 source-field PATCH,保留线上 bindings/contracts/metadata/trigger/view/enabled/published state;整包 manifest 替换必须加 `--replace-manifest --reason "..."`。
32
+ - Function/Automation 源码触发默认走服务端 source-field PATCH,保留线上 bindings/contracts/metadata/trigger/view/enabled/published state;无源码且 `definitionJson.version="v3"` 完整的新建 Automation 自动走 manifest create,只有替换已有整包 manifest 才必须加 `--replace-manifest --reason "..."`。
33
+ - 环境托管 `release ship --replace-manifest --reason "..."` 必须成对、reason 至少 8 字符且仅限精确 Backend selector;正式确认复用与预发完全相同的参数,不透传 Form/Workflow/Runtime/配置或全量步骤。
33
34
  - Promotion 必须持有 `release begin/end` 租约;`release begin` 只接受与权威远端 tip 完全一致的 clean main/master。feature branch 或未 push 主线在任何写入前失败。激活后直接运行 `integration-status` 和 `release end`,不再补做发布后合并。
34
35
  - 已有工作区通过 `environment attach` 接入环境组,旧资源映射只迁移到 appType 相同的预发 target,正式 target 必须为空。preproduction / production 分别拥有独立 appType、资源 ID、数据和副作用策略;禁止直接 `release publish`,禁止跨环境复制 ID,使用 `openxiangda studio` 查看状态。只有用户明确授权的投产前重分类可执行 `environment swap --reason "..." --confirm-production`;它不移动应用数据或 Release Head,且默认不放开副作用。
35
36
  - 单独修改某个环境的副作用策略只能使用 `environment policy update <preproduction|production> --side-effect-policy-json <JSON|file> --reason "..."`:先执行 `--dry-run`,正式环境额外要求 `--confirm-production`,不得借用 `environment swap`。patch 只校验本次提交字段并原样保留未知历史字段,`--full-replace` 才按完整目标删除遗漏字段。`organizationWrites=explicit_capability_only` 只解除环境级 deny,仍强制 `app:organization:manage`。发布硬门禁只保留明确 scope/profile/target、权限、干净且已推送主线、不可变版本、CAS/租约与生产确认;文案和人工验收说明默认是建议,只有显式 strict 模式才阻断。
@@ -57,6 +57,10 @@ openxiangda commands --json
57
57
 
58
58
  只有已审计代码早已进入权威主线、而精确非删除目标在线上来自多次历史发布且无法对应单一 Git 基线时,第一次 `release ship` 才可增加 `--adopt-online-baseline --adoption-reason "..."`。该参数不会放松冻结 online heads、change/lease、删除/全量拒绝、服务端 CAS、staged children 或单次 App finalize 原子激活。
59
59
 
60
+ 环境托管发布需要完整替换 Function/Automation manifest 时,`release ship` 可成对增加 `--replace-manifest --reason "..."`;reason 至少 8 字符,只会进入精确 Backend selector,正式确认必须复用与预发完全相同的参数,绝不扩散到表单、流程、Runtime、配置资源或全量范围。
61
+
62
+ 受支持的配置资源(如 `publicAccessPolicies`)以精确 `resourceSelectors` 为发布边界;历史 `resources=true` 类别标记会被精确 selector 收窄。缺失 selector、未知类型、通配符 `*`、删除和真正全量资源仍必须 fail closed。
63
+
60
64
  `resource plan` 与 publish dry-run 严格只允许 GET/HEAD。遇到 `READ_ONLY_AUTH_REQUIRED` 时,先执行 `openxiangda auth refresh --profile <name>` 或重新登录再重试;不得在 plan 内自动 POST 刷新 token。
61
65
 
62
66
  完整发布顺序:
@@ -78,7 +82,7 @@ App Function 第三方凭据只能在 Function manifest 顶层声明 `secretRefs
78
82
 
79
83
  `openxiangda runtime deploy --no-activate` 会构建并上传不可变预览版本;发布前先提交所有可能进入构建的源码/配置。所有 Runtime deploy(包括 `--no-activate`)都会先获取应用发布 lease,并在任何构建和上传前冻结 clean `HEAD` 与当前 active Runtime 父血缘;旧分支返回 `RUNTIME_SOURCE_BASE_DIVERGED`,不能先上传旧 preview 再激活。`openspec/` SDD 证据和生成/状态目录不算源码 dirty。仅审批的回退可使用 `--allow-runtime-rollback --reason "至少 8 个字符"`;`--no-build` 不会跳过守卫。不要手工修改 `dist/index.html`。
80
84
 
81
- Function/Automation 走 Backend Release v2;同一个 child 可以混合 create、source-only update 与显式 manifest replacement,并对整个集合做 CAS。正式多资源发布必须使用 canonical 精确 selector 和 `--stage-only`。`release begin` 只接受与权威远端默认主分支完全一致的 clean HEAD;feature branch 或未 push 的 main 会在任何平台写入前失败。成功激活后主线证据天然成立,不再补做发布后合并。
85
+ Function/Automation 走 Backend Release v2;同一个 child 可以混合源码 create、无 `sourceFile` 的完整 v3 声明式 Automation manifest create、source-only update 与显式 manifest replacement,并对整个集合做 CAS。声明式 create 自动选路且不需要 `--replace-manifest`;替换已有资源才需要该显式授权。正式多资源发布必须使用 canonical 精确 selector 和 `--stage-only`。`release begin` 只接受与权威远端默认主分支完全一致的 clean HEAD;feature branch 或未 push 的 main 会在任何平台写入前失败。成功激活后主线证据天然成立,不再补做发布后合并。
82
86
 
83
87
  ## 应用结构
84
88
 
@@ -31,7 +31,7 @@ Exact `--only/--code` selectors apply before manifest/source analysis and JS_COD
31
31
 
32
32
  App Function manifests may contain only logical names in top-level `secretRefs`; use `function_v2` / `trusted_node_v2`, resolve values with `ctx.secrets.get(name)`, and manage values only with `openxiangda secret ... --value-stdin --change ... --profile ...`. Values never belong in Git, `.env`, manifests, source, builds, plans, logs, or errors.
33
33
 
34
- When a Function/Automation enters scope only through source changes, publishing uses a server-side source-field PATCH and preserves online bindings, contracts, metadata, trigger/view configuration, and enabled/published state. Whole-manifest replacement requires `--replace-manifest --reason "..."`. Formal promotion freezes the clean publish HEAD separately from the change/remote baseline and preflights the whole set; `SOURCE_BASE_DIVERGED`, `RELEASE_SOURCE_BEHIND_MAIN`, and `RESOURCE_FIELD_CONFLICT` require reconciliation. After activation, merge/push the frozen SHA, verify `release integration-status`, then run normal `release end`.
34
+ When a Function/Automation enters scope only through source changes, publishing uses a server-side source-field PATCH and preserves online bindings, contracts, metadata, trigger/view configuration, and enabled/published state. A new source-free Automation with a complete `definitionJson.version="v3"` automatically uses manifest create; replacing an existing whole manifest requires `--replace-manifest --reason "..."`. Formal promotion freezes the clean publish HEAD separately from the change/remote baseline and preflights the whole set; `SOURCE_BASE_DIVERGED`, `RELEASE_SOURCE_BEHIND_MAIN`, and `RESOURCE_FIELD_CONFLICT` require reconciliation. After activation, merge/push the frozen SHA, verify `release integration-status`, then run normal `release end`.
35
35
 
36
36
  Before editing `roles`, `permissions/page-groups`, or `permissions/form-groups` for account/role/data-scope/RBAC/query-param authorization work, run `openxiangda design gates --topic permissions --json`, choose the permission mode, and write the permission matrix.
37
37
 
@@ -29,7 +29,8 @@ This is a `sy-lowcode-app-workspace` managed by the `openxiangda` CLI. See [AGEN
29
29
  - L0 read-only/docs/tests need no SDD; L1 narrow reversible fixes record exact scope without a redundant second confirmation; schema, business Functions, Automation/Workflow, permissions, auth/public access, data writes, and runtime/config are L2/L3 full-SDD work. Live evidence/archive are post-release stages.
30
30
  - Very small copy/style/binding changes may use `openxiangda sdd quick <change> ...`; quick mode is limited to an exact low-risk scope and does not add a redundant proposal/approval loop when the user already requested that exact edit.
31
31
  - SDD is streamlined by default: approval and exact structured scope are hard gates, while unfinished task/evidence/spec prose only warns. Use `strictDocumentation: true` only when prose must block.
32
- - Source-triggered Function/Automation publishing uses server-side source-field PATCH by default and preserves online bindings, contracts, metadata, trigger/view configuration, and enabled/published state. Whole-manifest replacement requires exact `--only/--code` plus `--replace-manifest --reason "..."`.
32
+ - Source-triggered Function/Automation publishing uses server-side source-field PATCH by default and preserves online bindings, contracts, metadata, trigger/view configuration, and enabled/published state. A new source-free Automation with a complete `definitionJson.version="v3"` automatically uses manifest create; replacing an existing whole manifest requires exact `--only/--code` plus `--replace-manifest --reason "..."`.
33
+ - Managed `release ship --replace-manifest --reason "..."` requires the pair, reason length >= 8, and exact Backend selectors; production confirmation repeats the exact preproduction pair, with no forwarding to Form/Workflow/Runtime/config/all stages.
33
34
  - Before platform writes, run `release begin` only from clean main/master exactly equal to the authoritative remote tip. Feature branches and unpushed mainline commits fail before writes. After activation, run `integration-status` and `release end`; no post-release merge is needed.
34
35
  - Environment-managed workspaces never publish directly. Existing workspaces use `environment attach`; only an appType-matching legacy binding may seed preproduction and production starts empty. Run `release candidate`, `release deploy --environment preproduction`, `release test`, then promote that same candidate with `release promote --environment production --confirm-production`. Keep app/resource/data IDs isolated and inspect state with `openxiangda studio`. Only an explicitly authorized commissioning reclassification may use `environment swap --reason "..." --confirm-production`; it preserves app data/Release Heads and keeps side effects restricted by default.
35
36
  - Change one target's side-effect policy only with `environment policy update <preproduction|production> --side-effect-policy-json <JSON|file> --reason "..."`: run `--dry-run` first, add `--confirm-production` for production, and never use `environment swap` for this. Patch mode validates only supplied fields and preserves unrecognized historical fields; `--full-replace` validates the supplied complete target and intentionally removes omissions. `organizationWrites=explicit_capability_only` removes the environment deny but still requires `app:organization:manage`. Keep release hard gates to explicit scope/profile/target, authorization, clean pushed mainline, immutable identity, CAS/lease, and production confirmation; prose and human acceptance notes are advisory unless strict mode is configured.
@@ -41,7 +41,7 @@ openxiangda resource publish <type> --code <code> --profile <name> # 单资源
41
41
  openxiangda resource pull --profile <name> # 平台 → 本地
42
42
  ```
43
43
 
44
- Function/Automation 仅因源码变化进入 scope 时,默认通过服务端字段 PATCH 只更新 source snapshot,并保留线上 bindings、contracts、metadata、trigger/view 配置和 enabled/published state。确需让本地整包 manifest 覆盖线上定义时,必须追加 `--replace-manifest --reason "..."`。正式发布先 `release begin --change <change>` 分别冻结 clean publish HEAD 与 change/远端 baseline 并整体 preflight;`SOURCE_BASE_DIVERGED` / `RELEASE_SOURCE_BEHIND_MAIN` / `RESOURCE_FIELD_CONFLICT` 不能强行重试。上线后 merge/push 冻结 SHA,验证 `release integration-status`,再正常 `release end`。
44
+ Function/Automation 仅因源码变化进入 scope 时,默认通过服务端字段 PATCH 只更新 source snapshot,并保留线上 bindings、contracts、metadata、trigger/view 配置和 enabled/published state。无源码且 `definitionJson.version="v3"` 完整的新建 Automation 自动走 manifest create;确需让本地整包 manifest 覆盖已有线上定义时,必须追加 `--replace-manifest --reason "..."`。正式发布先 `release begin --change <change>` 分别冻结 clean publish HEAD 与 change/远端 baseline 并整体 preflight;`SOURCE_BASE_DIVERGED` / `RELEASE_SOURCE_BEHIND_MAIN` / `RESOURCE_FIELD_CONFLICT` 不能强行重试。上线后 merge/push 冻结 SHA,验证 `release integration-status`,再正常 `release end`。
45
45
 
46
46
  `resource plan` 与 publish dry-run 严格只允许 GET/HEAD。遇到 `READ_ONLY_AUTH_REQUIRED` 时,先执行 `openxiangda auth refresh --profile <name>` 或重新登录再重试;不得在 plan 内自动 POST 刷新 token。
47
47
 
@@ -29,7 +29,8 @@ This is a `sy-lowcode-app-workspace` managed by the `openxiangda` CLI. Read [AGE
29
29
  - L0 只读/文档/测试无需 SDD;L1 窄小可逆改动记录精确范围且不重复确认;表单结构、业务函数、自动化/流程、权限、登录/公开访问、数据写入、runtime/config 等 L2/L3 走完整 SDD,live evidence/archive 放到发布后。
30
30
  - 极小的文案、样式、绑定修正可使用 `openxiangda sdd quick <change> ...`,但必须限制在精确的低风险范围内;用户已明确要求该小改时不再重复 propose/approve。
31
31
  - SDD 默认 streamlined:approval 与结构化精确范围是硬门禁,未完成的 task/evidence/spec 文案只告警;只有 `strictDocumentation: true` 才阻断。
32
- - Function/Automation 源码触发默认走服务端 source-field PATCH,保留线上 bindings/contracts/metadata/trigger/view/enabled/published state;整包 manifest 替换必须精确 `--only/--code` 并加 `--replace-manifest --reason "..."`。
32
+ - Function/Automation 源码触发默认走服务端 source-field PATCH,保留线上 bindings/contracts/metadata/trigger/view/enabled/published state;无源码且 `definitionJson.version="v3"` 完整的新建 Automation 自动走 manifest create,只有替换已有整包 manifest 才必须精确 `--only/--code` 并加 `--replace-manifest --reason "..."`。
33
+ - 环境托管 `release ship --replace-manifest --reason "..."` 必须成对、reason 至少 8 字符且仅限精确 Backend selector;正式确认复用与预发完全相同的参数,不透传 Form/Workflow/Runtime/配置或全量步骤。
33
34
  - 写入平台前只从与权威远端 tip 完全一致的 clean main/master 执行 `release begin`。feature branch 或未 push 主线在任何写入前失败;激活后直接运行 `integration-status` 和 `release end`,无需发布后再合并。
34
35
  - 环境托管工作区禁止直发:已有工作区先用 `environment attach` 接入,旧映射只能迁移到 appType 相同的预发 target,正式 target 必须为空。然后执行 `release candidate`、`release deploy --environment preproduction`、`release test`,最后将同一 candidate 用 `release promote --environment production --confirm-production` 晋级。两套环境的 appType、资源 ID 和数据不可互拷;用 `openxiangda studio` 查看状态。只有用户明确授权的投产前重分类可执行 `environment swap --reason "..." --confirm-production`;它不移动应用数据或 Release Head,且默认不放开副作用。
35
36
  - 单独修改某个环境的副作用策略只能使用 `environment policy update <preproduction|production> --side-effect-policy-json <JSON|file> --reason "..."`:先执行 `--dry-run`,正式环境额外要求 `--confirm-production`,不得借用 `environment swap`。patch 只校验本次提交字段并原样保留未知历史字段,`--full-replace` 才按完整目标删除遗漏字段。`organizationWrites=explicit_capability_only` 只解除环境级 deny,仍强制 `app:organization:manage`。发布硬门禁只保留明确 scope/profile/target、权限、干净且已推送主线、不可变版本、CAS/租约与生产确认;文案和人工验收说明默认是建议,只有显式 strict 模式才阻断。
@@ -55,10 +55,12 @@
55
55
  - ✅ 发现平台缺陷、能力缺口、规则不清、反复 workaround、AI 不确定点、用户可见体验问题时,主动 `openxiangda feedback submit --yes`;发送后告诉用户反馈内容和 fingerprint。
56
56
  - ✅ 正式多资源开发优先写 `src/resources/**` 后执行 `openxiangda resource validate|plan|publish <type> --only <codes>`;单资源可用 `--code <code>`。直接 CLI 写平台资源时先 `--dry-run`,需要避免漂移就加 `--write-manifest`。
57
57
  - ✅ `resource plan` 与 publish dry-run 严格只允许 GET/HEAD;遇到 `READ_ONLY_AUTH_REQUIRED` 时先执行 `openxiangda auth refresh --profile <name>` 或重新登录,不得在 plan 内自动 POST 刷新 token。
58
- - ✅ Function/Automation 使用 Backend Release v2;正式多资源发布用精确 `--only/--code` 加 `--stage-only` 暂存,同一 child 可混合 create、source-only update 和显式 manifest replacement,再由 Root App finalize 原子激活。整包 manifest 替换必须另加 `--replace-manifest --reason "..."`。
58
+ - ✅ Function/Automation 使用 Backend Release v2;正式多资源发布用精确 `--only/--code` 加 `--stage-only` 暂存,同一 child 可混合源码 create、无 `sourceFile` 的完整 v3 声明式 Automation manifest create、source-only update 和显式 manifest replacement,再由 Root App finalize 原子激活。声明式 create 自动选路;替换已有资源的整包 manifest 才需要另加 `--replace-manifest --reason "..."`。
59
59
  - ✅ 未登记环境的旧工作区,正式 promotion 先聚合 mainline bundle 并 commit/push,再运行 `release publish --change <id> --profile <name>`。
60
60
  - ✅ 已通过 `environment init` 登记或 `environment attach` 接入的工作区默认使用两段式 `release ship`。第一次命令只冻结 candidate 并部署 preproduction,停止等待正式晋级确认;确认预发结果后,另一次命令提供 `--confirm-production`,即可晋级同一 candidate。人工验收是默认建议,可用可选的 `--acceptance-note` 留痕,但不是所有低风险或紧急发布的硬审批门禁。preproduction / production 的 appType、资源 ID、数据和副作用策略完全隔离,严禁跨环境复制 ID 或直接 `release publish`。旧单目标映射只允许按相同 appType 迁移到预发。仅在用户明确授权的投产前重分类中使用 `environment swap --reason "..." --confirm-production` 原子交换两个既有应用的环境角色;数据和 Release Head 不移动,副作用默认不放开。单环境副作用策略用 `environment policy update <kind|id> ... --dry-run` 预览后按 revision CAS 写入,禁止借用 swap;patch 只校验本次提交字段并原样保留未知历史字段,`--full-replace` 才按完整目标删除遗漏字段,正式写入需 `--confirm-production`,且 `organizationWrites=explicit_capability_only` 不绕过 `app:organization:manage`。发布硬门禁只保留明确 scope/profile/target、权限、干净且已推送主线、不可变版本、CAS/租约与生产确认;文案和人工验收说明默认是建议,只有显式 strict 模式才阻断。
61
61
  - ✅ 只有已审计目标早已进入权威主线、线上却由多次历史 lineage 组成且无法对应单一 Git 基线时,第一次 `release ship` 才可增加 `--adopt-online-baseline --adoption-reason "..."`;仅允许精确非删除 selectors,冻结 Head、change/lease、服务端 CAS、staged children 与单次 App finalize 仍是硬门禁。
62
+ - ✅ 环境托管发布需要完整替换 Function/Automation manifest 时,`release ship` 可成对增加 `--replace-manifest --reason "..."`;reason 至少 8 字符,只透传精确 Backend selector,正式确认必须复用同一对参数,不扩散到 Form/Workflow/Runtime/配置或全量范围。
63
+ - ✅ 受支持的配置资源(如 `publicAccessPolicies`)以精确 `resourceSelectors` 为边界;历史 `resources=true` 类别标记会被精确 selector 收窄。缺失 selector、未知类型、通配符 `*`、删除和真正全量资源仍 fail closed。
62
64
  - ✅ 本地开发者可运行 `openxiangda studio` 查看两套环境、差异、候选、部署和测试证据;该页面只监听回环地址且只暴露注册动作,生产操作仍需显式确认。
63
65
 
64
66
  ## 严禁