openxiangda 1.0.252 → 1.0.254

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
@@ -1,4 +1,4 @@
1
- <!-- OpenXiangda-Policy-Version: 6 -->
1
+ <!-- OpenXiangda-Policy-Version: 7 -->
2
2
  # OpenXiangda
3
3
 
4
4
  OpenXiangda is a lightweight CLI and skill package for private low-code platforms.
@@ -38,7 +38,12 @@ path is used.
38
38
  `status` and `retry` locate the run across preproduction and production when
39
39
  `--environment` is omitted. Retry resumes only unfinished checkpoints, safely
40
40
  replays local Form bindings on another machine, and uses a server-side attempt
41
- fence so an older executor cannot continue writing after takeover.
41
+ fence so an older executor cannot continue writing after takeover. Runtime
42
+ build IDs include both the Runtime layer and sealed package digests, so a newly
43
+ sealed package cannot collide with an older package that happens to contain the
44
+ same Runtime bytes. An exact same-package retry may reuse an `uploaded` release
45
+ only after verifying its content hash, source revision, and Runtime parent;
46
+ immutable storage objects are never overwritten.
42
47
 
43
48
  The lower-level commands later in this README remain available for V1
44
49
  compatibility and diagnostics; do not use them to assemble a normal V2 release.
@@ -123,6 +128,8 @@ User tokens are stored in `~/.openxiangda/profiles.json` with `0600` permissions
123
128
 
124
129
  An environment-managed workspace keeps one logical application with independent `preproduction` and `production` targets. Each target owns its own `appType`, resource IDs, release heads, data, and side-effect policy; IDs must never be copied across targets. `release ship` always executes the same ordered candidate → preproduction → production protocol. The normal first invocation seals the candidate, deploys only to preproduction, and stops at `awaiting_production_confirmation`; a later invocation with `--confirm-production` promotes it. For an explicitly authorized emergency, supplying `--confirm-production` on the first invocation runs both phases in one command without bypassing preproduction, CAS, evidence, or production confirmation. Candidate sealing covers source, `public/`, build controls/scripts, stable environment/resource bindings, and target-specific hashed Runtime artifacts. Both deployments upload those artifacts with `--no-build`; each deployment is completed with evidence and reaches terminal `succeeded`, so it cannot leave the target slot blocked. Unrelated commits may land on authoritative mainline between phases only while the sealed commit remains an ancestor and every sealed input/binding/artifact still validates. Real human acceptance remains the recommended default and `--acceptance-note` records it. For audited historical-lineage adoption or reviewed Backend manifest replacement, the existing paired flags and exact-scope gates remain mandatory. Supported configuration resources use exact `resourceSelectors`; unknown, wildcard, destructive, and genuinely unscoped generic resources remain blocked. Lower-level candidate/deploy/test/fail/promote commands are recovery primitives. `release fail` requires an explicit preproduction target, deployment ID, and audit message; it verifies the deployment belongs to that preproduction environment before writing optional code/details to the platform failure audit. Direct `release publish` is retained only for legacy unmanaged workspaces.
125
130
 
131
+ DataView `status` is a last-observed lifecycle value, not a stable candidate binding: the same managed deployment legitimately moves it between `draft` and `active`. Candidate creation and validation therefore omit only `resources.dataViews.<code>.status`, including when resuming a candidate sealed by an older CLI. `dataViewId`, `materializedViewName`, `storageMode`, candidate hashes, environment identity, CAS, lease, source and every non-DataView status remain fail-closed.
132
+
126
133
  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.
127
134
 
128
135
  Existing workspaces connect to a server-side environment set with `environment attach`. If the legacy `profiles.<profile>` binding has the same `appType` as one environment, its resource mappings are copied only into that matching target (normally preproduction). Production starts with an empty mapping, and later writes update only the selected target even when both targets reuse one login profile.
@@ -33,6 +33,15 @@ const CANDIDATE_RESOURCE_OBSERVATION_FIELDS = new Set([
33
33
  'bundlePublishedAt',
34
34
  ]);
35
35
 
36
+ function isCandidateResourceObservationField(pathParts, key) {
37
+ if (CANDIDATE_RESOURCE_OBSERVATION_FIELDS.has(key)) return true;
38
+ return (
39
+ key === 'status' &&
40
+ pathParts.length === 2 &&
41
+ pathParts[0] === 'dataViews'
42
+ );
43
+ }
44
+
36
45
  function canonicalJson(value) {
37
46
  if (Array.isArray(value)) {
38
47
  return `[${value.map(item => canonicalJson(item)).join(',')}]`;
@@ -707,18 +716,22 @@ function normalizeCandidateProjectStateForHash(input, hashPolicy) {
707
716
  return state;
708
717
  }
709
718
 
710
- function normalizeCandidateResourceBindings(value) {
719
+ function normalizeCandidateResourceBindings(value, pathParts = []) {
711
720
  if (Array.isArray(value)) {
712
- return value.map(item => normalizeCandidateResourceBindings(item));
721
+ return value.map((item, index) =>
722
+ normalizeCandidateResourceBindings(item, [...pathParts, String(index)])
723
+ );
713
724
  }
714
725
  if (!value || typeof value !== 'object') return value;
715
726
  return Object.fromEntries(
716
727
  Object.entries(value)
717
- .filter(([key]) => !CANDIDATE_RESOURCE_OBSERVATION_FIELDS.has(key))
728
+ .filter(
729
+ ([key]) => !isCandidateResourceObservationField(pathParts, key)
730
+ )
718
731
  .sort(([left], [right]) => left.localeCompare(right))
719
732
  .map(([key, item]) => [
720
733
  key,
721
- normalizeCandidateResourceBindings(item),
734
+ normalizeCandidateResourceBindings(item, [...pathParts, key]),
722
735
  ])
723
736
  );
724
737
  }
@@ -819,18 +832,19 @@ function inspectCandidateEnvironmentBindings(candidate, state) {
819
832
  });
820
833
  }
821
834
  }
822
- if (
823
- !candidateBindingValueContains(
824
- expectedBinding?.resources || {},
825
- currentBinding.resources || {}
826
- )
827
- ) {
835
+ const expectedResources = normalizeCandidateResourceBindings(
836
+ expectedBinding?.resources || {}
837
+ );
838
+ const currentResources = normalizeCandidateResourceBindings(
839
+ currentBinding.resources || {}
840
+ );
841
+ if (!candidateBindingValueContains(expectedResources, currentResources)) {
828
842
  mismatches.push({
829
843
  targetName,
830
844
  field: 'resources',
831
845
  reason: 'sealed-resource-binding-changed',
832
- expected: expectedBinding?.resources || {},
833
- current: currentBinding.resources || {},
846
+ expected: expectedResources,
847
+ current: currentResources,
834
848
  });
835
849
  }
836
850
  }
package/lib/cli.js CHANGED
@@ -15674,7 +15674,13 @@ async function runtime(args) {
15674
15674
  const files = collectRuntimeDistFiles(distDir, {
15675
15675
  includeSourceMaps: Boolean(flags['include-sourcemaps']),
15676
15676
  });
15677
+ const declaredReleaseFiles = runtimeReleaseFileDescriptors(files);
15677
15678
  const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
15679
+ const reusableRuntimeRelease = await telemetry.runPhase(
15680
+ 'reuse-check',
15681
+ async () =>
15682
+ findRuntimeReleaseByBuildId(config, target, buildId)
15683
+ );
15678
15684
  printRuntimeProgress(
15679
15685
  `runtime release upload: mode=${effectiveUploadMode} traceId=${traceId} files=${files.length} size=${formatBytes(totalBytes)} timeout=${uploadTimeoutMs}ms`
15680
15686
  );
@@ -15682,6 +15688,20 @@ async function runtime(args) {
15682
15688
  'upload',
15683
15689
  { fileCount: files.length, totalBytes, mode: effectiveUploadMode },
15684
15690
  async () => {
15691
+ if (reusableRuntimeRelease) {
15692
+ assertRuntimeReleaseReusable(reusableRuntimeRelease, {
15693
+ appType: target.appType,
15694
+ buildId,
15695
+ files: declaredReleaseFiles,
15696
+ sourceRevision: runtimeLineage.sourceRevision,
15697
+ parentReleaseId: runtimeLineage.parentReleaseId,
15698
+ });
15699
+ effectiveUploadMode = 'reuse-existing';
15700
+ printRuntimeProgress(
15701
+ `runtime immutable release reused: releaseId=${reusableRuntimeRelease.id} buildId=${buildId} traceId=${traceId}`
15702
+ );
15703
+ return declaredReleaseFiles;
15704
+ }
15685
15705
  if (uploadMode === 'legacy-json') {
15686
15706
  effectiveUploadMode = 'legacy-json';
15687
15707
  return files.map(file => ({
@@ -15714,6 +15734,27 @@ async function runtime(args) {
15714
15734
  timeoutMs: uploadTimeoutMs,
15715
15735
  });
15716
15736
  } catch (error) {
15737
+ if (isRuntimeBuildImmutableConflict(error)) {
15738
+ const racedRuntimeRelease = await findRuntimeReleaseByBuildId(
15739
+ config,
15740
+ target,
15741
+ buildId
15742
+ );
15743
+ if (racedRuntimeRelease) {
15744
+ assertRuntimeReleaseReusable(racedRuntimeRelease, {
15745
+ appType: target.appType,
15746
+ buildId,
15747
+ files: declaredReleaseFiles,
15748
+ sourceRevision: runtimeLineage.sourceRevision,
15749
+ parentReleaseId: runtimeLineage.parentReleaseId,
15750
+ });
15751
+ effectiveUploadMode = 'reuse-existing';
15752
+ printRuntimeProgress(
15753
+ `runtime immutable release won upload race and was reused: releaseId=${racedRuntimeRelease.id} buildId=${buildId} traceId=${traceId}`
15754
+ );
15755
+ return declaredReleaseFiles;
15756
+ }
15757
+ }
15717
15758
  if (uploadMode !== 'auto' || !isRuntimeStagedUploadBlocked(error)) {
15718
15759
  throw error;
15719
15760
  }
@@ -16076,6 +16117,134 @@ function collectRuntimeDistFiles(distDir, options = {}) {
16076
16117
  return files;
16077
16118
  }
16078
16119
 
16120
+ function runtimeReleaseFileDescriptors(files) {
16121
+ return (files || []).map(file => ({
16122
+ path: file.path,
16123
+ size: file.size,
16124
+ sha256: file.sha256,
16125
+ contentType: file.contentType,
16126
+ }));
16127
+ }
16128
+
16129
+ function calculateRuntimeReleaseContentHash(files) {
16130
+ const hash = crypto.createHash('sha256');
16131
+ for (const file of files || []) {
16132
+ const filePath = String(file?.path || '').trim();
16133
+ const fileSha256 = String(file?.sha256 || '').trim().toLowerCase();
16134
+ if (!filePath || !/^[a-f0-9]{64}$/.test(fileSha256)) {
16135
+ throw runtimeReleaseReuseError(
16136
+ 'RUNTIME_RELEASE_REUSE_LOCAL_MANIFEST_INVALID',
16137
+ `本地 Runtime 文件描述不完整: ${filePath || '(unknown)'}`,
16138
+ { filePath: filePath || null }
16139
+ );
16140
+ }
16141
+ hash.update(filePath);
16142
+ hash.update(fileSha256);
16143
+ }
16144
+ return hash.digest('hex');
16145
+ }
16146
+
16147
+ async function findRuntimeReleaseByBuildId(config, target, buildId) {
16148
+ const appPath = `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/runtime/releases`;
16149
+ try {
16150
+ const exact = await requestWithAuth(
16151
+ config,
16152
+ target.profileName,
16153
+ `${appPath}/by-build/${encodeURIComponent(buildId)}`
16154
+ );
16155
+ return exact?.id ? exact : null;
16156
+ } catch (error) {
16157
+ if (Number(error?.status) !== 404) throw error;
16158
+ }
16159
+ const releases = await requestWithAuth(
16160
+ config,
16161
+ target.profileName,
16162
+ appPath
16163
+ );
16164
+ const items = Array.isArray(releases)
16165
+ ? releases
16166
+ : Array.isArray(releases?.items)
16167
+ ? releases.items
16168
+ : [];
16169
+ return (
16170
+ items.find(item => String(item?.buildId || '') === String(buildId)) ||
16171
+ null
16172
+ );
16173
+ }
16174
+
16175
+ function assertRuntimeReleaseReusable(existing, expected) {
16176
+ const context = {
16177
+ appType: expected.appType,
16178
+ buildId: expected.buildId,
16179
+ existingReleaseId: existing?.id || null,
16180
+ };
16181
+ if (String(existing?.status || '') !== 'uploaded') {
16182
+ throw runtimeReleaseReuseError(
16183
+ 'RUNTIME_RELEASE_REUSE_STATUS_INVALID',
16184
+ `相同 buildId 的 Runtime Release 状态为 ${existing?.status || '(unknown)'},仅 uploaded 状态可作为失败发布的暂存恢复点`,
16185
+ { ...context, existingStatus: existing?.status || null }
16186
+ );
16187
+ }
16188
+ const expectedContentHash = calculateRuntimeReleaseContentHash(
16189
+ expected.files
16190
+ );
16191
+ if (
16192
+ String(existing?.contentHash || '').trim().toLowerCase() !==
16193
+ expectedContentHash
16194
+ ) {
16195
+ throw runtimeReleaseReuseError(
16196
+ 'RUNTIME_RELEASE_REUSE_CONTENT_MISMATCH',
16197
+ '相同 buildId 的历史 Runtime Release 内容与当前密封包不一致,拒绝复用',
16198
+ {
16199
+ ...context,
16200
+ expectedContentHash,
16201
+ existingContentHash: existing?.contentHash || null,
16202
+ }
16203
+ );
16204
+ }
16205
+ if (
16206
+ !runtimeSourceRevisionEquals(
16207
+ existing?.sourceRevision,
16208
+ expected.sourceRevision
16209
+ )
16210
+ ) {
16211
+ throw runtimeReleaseReuseError(
16212
+ 'RUNTIME_RELEASE_REUSE_SOURCE_MISMATCH',
16213
+ '相同 buildId 的历史 Runtime Release 源码血缘与当前密封包不一致,拒绝复用',
16214
+ context
16215
+ );
16216
+ }
16217
+ const existingParentReleaseId =
16218
+ String(existing?.parentReleaseId || '').trim() || null;
16219
+ const expectedParentReleaseId =
16220
+ String(expected.parentReleaseId || '').trim() || null;
16221
+ if (existingParentReleaseId !== expectedParentReleaseId) {
16222
+ throw runtimeReleaseReuseError(
16223
+ 'RUNTIME_RELEASE_REUSE_PARENT_MISMATCH',
16224
+ '相同 buildId 的历史 Runtime Release 父版本已与当前 Runtime head 不一致,拒绝复用',
16225
+ {
16226
+ ...context,
16227
+ existingParentReleaseId,
16228
+ expectedParentReleaseId,
16229
+ }
16230
+ );
16231
+ }
16232
+ return existing;
16233
+ }
16234
+
16235
+ function runtimeSourceRevisionEquals(left, right) {
16236
+ return ['repo', 'baseCommit', 'treeHash'].every(
16237
+ key => String(left?.[key] || '').trim() === String(right?.[key] || '').trim()
16238
+ );
16239
+ }
16240
+
16241
+ function runtimeReleaseReuseError(code, message, data = {}) {
16242
+ const error = new Error(`${code}: ${message}`);
16243
+ error.code = code;
16244
+ error.data = data;
16245
+ return error;
16246
+ }
16247
+
16079
16248
  function walkRuntimeDist(rootDir, currentDir, files, options) {
16080
16249
  const entries = fs.readdirSync(currentDir, { withFileTypes: true });
16081
16250
  for (const entry of entries) {
@@ -16311,6 +16480,16 @@ function isRuntimeStagedUploadBlocked(error) {
16311
16480
  return /^HTTP 403\b/.test(String(error?.message || ''));
16312
16481
  }
16313
16482
 
16483
+ function isRuntimeBuildImmutableConflict(error) {
16484
+ return (
16485
+ Number(error?.status) === 409 &&
16486
+ (String(error?.code || '') === 'RUNTIME_BUILD_IMMUTABLE' ||
16487
+ /Runtime buildId .*禁止覆盖|RUNTIME_BUILD_IMMUTABLE/.test(
16488
+ String(error?.message || '')
16489
+ ))
16490
+ );
16491
+ }
16492
+
16314
16493
  function normalizeRuntimeUploadTimeoutMs(value) {
16315
16494
  const candidate =
16316
16495
  value === undefined || value === null || value === ''
@@ -30664,12 +30843,14 @@ function buildWorkspacePublishEnv(
30664
30843
  }
30665
30844
 
30666
30845
  module.exports = {
30846
+ assertRuntimeReleaseReusable,
30667
30847
  buildResourceManifestSddTargets,
30668
30848
  buildScopeGrantSourceSyncDecision,
30669
30849
  deploymentEvidencePendingError,
30670
30850
  deploymentQueueWaitDecision,
30671
30851
  inspectSucceededPreproductionDeployment,
30672
30852
  main,
30853
+ calculateRuntimeReleaseContentHash,
30673
30854
  recoveredPreproductionShipPatch,
30674
30855
  resolveReleaseCommandScopedFiles,
30675
30856
  selectSucceededPreproductionDeployment,
@@ -1096,7 +1096,7 @@ function decorateSteps(
1096
1096
  '--dist',
1097
1097
  path.join(executionRoot, 'dist'),
1098
1098
  '--build-id',
1099
- `pkg-${runtime.digest.slice(0, 20)}`,
1099
+ deliveryV2RuntimeBuildId(runtime?.digest, run?.packageDigest),
1100
1100
  '--upload-mode',
1101
1101
  'staged'
1102
1102
  );
@@ -1129,6 +1129,30 @@ function decorateSteps(
1129
1129
  return result;
1130
1130
  }
1131
1131
 
1132
+ function deliveryV2RuntimeBuildId(runtimeDigest, packageDigest) {
1133
+ const normalizedRuntimeDigest = String(runtimeDigest || '')
1134
+ .trim()
1135
+ .toLowerCase();
1136
+ const normalizedPackageDigest = String(packageDigest || '')
1137
+ .trim()
1138
+ .toLowerCase();
1139
+ if (!/^[a-f0-9]{64}$/.test(normalizedRuntimeDigest)) {
1140
+ throw deliveryError(
1141
+ 'DELIVERY_RUNTIME_LAYER_DIGEST_INVALID',
1142
+ 'Runtime layer digest 必须是 64 位 SHA-256',
1143
+ false
1144
+ );
1145
+ }
1146
+ if (!/^[a-f0-9]{64}$/.test(normalizedPackageDigest)) {
1147
+ throw deliveryError(
1148
+ 'DELIVERY_PACKAGE_DIGEST_INVALID',
1149
+ 'package digest 必须是 64 位 SHA-256',
1150
+ false
1151
+ );
1152
+ }
1153
+ return `pkg-${normalizedRuntimeDigest.slice(0, 20)}-${normalizedPackageDigest.slice(0, 12)}`;
1154
+ }
1155
+
1132
1156
  function packageSummary(manifest, previousManifest) {
1133
1157
  return {
1134
1158
  sourceFileCount: layerByKind(manifest, 'source')?.fileCount || 0,
@@ -1668,6 +1692,7 @@ module.exports = {
1668
1692
  compactStepResult,
1669
1693
  createDeliveryV2Executor,
1670
1694
  decorateSteps,
1695
+ deliveryV2RuntimeBuildId,
1671
1696
  normalizeLegacyPackageTargetsForExecution,
1672
1697
  packageWorkspaceRoot,
1673
1698
  prepareAppFinalizeStep,
package/lib/policy.js CHANGED
@@ -60,10 +60,14 @@ function validateEngineeringPolicy(policy) {
60
60
  policy?.release?.deletionsFailClosed !== true ||
61
61
  policy?.release?.attemptFencing !== true ||
62
62
  policy?.release?.crossMachineBindingReplay !== true ||
63
+ policy?.release?.deterministicRuntimeBuildIdentity !==
64
+ 'runtime-layer-plus-package-digest' ||
65
+ policy?.release?.deterministicRuntimeUploadedReleaseReuse !==
66
+ 'exact-content-source-parent-only' ||
63
67
  policy?.release?.statusAutoResolvesEnvironment !== true
64
68
  ) {
65
69
  errors.push(
66
- 'Delivery V2 必须启用密封构建、精确差异、删除关闭、attempt 隔离、跨机器绑定恢复和环境自动解析'
70
+ 'Delivery V2 必须启用密封构建、精确差异、删除关闭、attempt 隔离、跨机器绑定恢复、Runtime 包级身份与严格恢复,以及环境自动解析'
67
71
  );
68
72
  }
69
73
  const ttl = Number(policy?.ownership?.defaultTtlSeconds);
@@ -3,7 +3,7 @@ name: openxiangda
3
3
  description: "Use OpenXiangda for private low-code platform work: app workspaces, forms, pages, resources, functions, automations, workflows, permissions, publishing, deployment, diagnosis, profiles, and the openxiangda CLI."
4
4
  ---
5
5
 
6
- <!-- OpenXiangda-Policy-Version: 6 -->
6
+ <!-- OpenXiangda-Policy-Version: 7 -->
7
7
  # OpenXiangda
8
8
 
9
9
  OpenXiangda connects an AI coding workspace to the private low-code platform through a normal-user profile and `/openxiangda-api/v1`. External backends using AK/SK are a separate `openxiangda-open-api` flow.
@@ -37,6 +37,10 @@ Keep the `runId`; failures preserve successful checkpoints. `status` and
37
37
  `retry` auto-locate preproduction or production when no environment is given.
38
38
  Retry fences the previous attempt and safely replays local Form bindings, so it
39
39
  can resume from another machine without recreating completed platform writes.
40
+ Deterministic Runtime build IDs combine the Runtime layer and sealed package
41
+ digests, preventing cross-package provenance collisions. An exact same-package
42
+ retry reuses an existing `uploaded` release only after exact
43
+ content/source/parent verification and never overwrites immutable storage.
40
44
 
41
45
  ## Keep the agent loop small
42
46
 
@@ -137,6 +141,8 @@ openxiangda release ship --change <release-change> --profile <name> \
137
141
 
138
142
  For a workspace registered by `environment init` or connected by `environment attach`, production is never a direct publish target. `release ship` always executes candidate → preproduction → production. The normal first invocation stops after preproduction; a later `--confirm-production` promotes the same candidate. If the user explicitly authorizes an emergency, putting `--confirm-production` on the first invocation runs both phases in one command without skipping preproduction, evidence, CAS, or confirmation. The sealed candidate contains source/build/public/script inputs, stable environment/resource bindings, and hashed target-specific Runtime artifacts; deployment uploads those exact artifacts with no rebuild and closes each server deployment as `succeeded`. Human acceptance remains recommended and may be recorded with `--acceptance-note`. Lower-level candidate/deploy/test/fail/promote commands are recovery primitives. `release fail` is preproduction-only: pass an explicit deployment and audit message, and the CLI verifies the deployment belongs to the selected preproduction environment before it writes optional code/details. Keep preproduction and production identities isolated, and retain the existing authorization/CAS rules for environment swap and policy changes. Use `openxiangda studio` for bindings, drift, evidence, and safe next actions.
139
143
 
144
+ Candidate binding checks treat only `resources.dataViews.<code>.status` as a mutable platform lifecycle observation, so a candidate survives its own `active → draft → active` staging sequence and older sealed candidates remain resumable. DataView IDs, materialized-view names, storage modes, other resource statuses, candidate hashes, environment identity, CAS, lease, and source/mainline checks remain exact.
145
+
140
146
  A sealed candidate may be promoted from a later clean, pushed authoritative mainline commit only when the candidate commit remains its Git ancestor and every sealed candidate input file still has the exact recorded hash. This permits unrelated parallel merges without allowing stale candidate inputs to overwrite newer work. Do not edit private candidate metadata to bypass `CANDIDATE_INPUTS_CHANGED`. The CLI waits for both the app lease and target deployment slot; each target permits only one running or evidence-pending deployment. Emergency fixes stay on the same candidate → preproduction → production path with a narrow L1 scope. `--wait-seconds 0` is fail-fast, not a binding-contract, CAS, or production-confirmation bypass.
141
147
 
142
148
  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 and freezes the pair before candidate/deployment creation; the later `--confirm-production` invocation automatically reuses the same intent from the private ship journal 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.
@@ -102,6 +102,7 @@ Environment-managed workspaces add a logical application and target-specific bin
102
102
  - Local resource keys are logical codes.
103
103
  - Live IDs and lightweight runtime aliases are nested under the profile that produced them.
104
104
  - `resources.dataViews` is keyed by data view `code` and stores only profile-local platform metadata such as `dataViewId`, `materializedViewName`, and last known `status`.
105
+ - The stored DataView `status` is observational: managed staging legitimately moves it between `draft` and `active`. Candidate binding validation ignores only that field, including for older sealed candidates, while keeping `dataViewId`, `materializedViewName`, `storageMode`, and all non-DataView status fields strict.
105
106
  - Data view definitions, refresh config, permissions, and source `formCode` references belong in `src/resources/data-views/*.json`, not in state.
106
107
  - Do not store business configuration or secrets in `.openxiangda/state.json`; store those in `src/resources/`.
107
108
  - CLI writes use a lock, a three-way merge at profile/resource-key granularity, and fsync + atomic rename. Concurrent writes to different profiles or logical resource keys are preserved; competing writes to the same field fail with `OPENXIANGDA_STATE_CONFLICT` and must be retried from a freshly loaded state.
@@ -30,7 +30,11 @@ unsupported and fail before remote writes. V2 executes authored builds with the
30
30
  CLI-sealed toolchain, never a workspace `node_modules` or custom builder.
31
31
  `status`/`retry` auto-resolve the run environment if omitted. Retry preserves
32
32
  completed checkpoints, fences the older attempt, and replays only local Form
33
- bindings when resuming on a different machine.
33
+ bindings when resuming on a different machine. Deterministic Runtime build IDs
34
+ combine the Runtime layer and sealed package digests, preventing provenance
35
+ collisions across packages with identical Runtime bytes. An `uploaded` release
36
+ is reused only for an exact same-package retry when its content hash, source
37
+ revision, and parent Runtime release match; all other identities fail closed.
34
38
 
35
39
  ## Resolve the boundary
36
40
 
@@ -140,6 +144,8 @@ openxiangda release ship --change <release-change> --profile <name> \
140
144
 
141
145
  Once `environment init` registers a logical application, or `environment attach` connects an existing workspace, `preproduction` and `production` are separate target bindings with separate app/resource/data IDs. Never copy IDs between them. Existing legacy resource mappings may seed only the appType-matching target. `release ship` always runs candidate → preproduction → production. Normally the first invocation stops at `awaiting_production_confirmation` and a later `--confirm-production` invocation promotes the same candidate. When the user explicitly authorizes an emergency release, `--confirm-production` may be supplied on the first invocation to execute both phases in one command; it bypasses no preproduction, evidence, CAS, or confirmation gate. Candidate sealing includes source/build/public/script inputs, stable environment/resource bindings, and hashed target-specific Runtime artifacts. Deployment reuses those artifacts without rebuilding and completes both server deployment records to `succeeded`. Human acceptance remains recommended and an optional `--acceptance-note` records it. Direct `release publish` to either managed target fails closed. Environment swap and policy updates retain their existing explicit authorization, CAS, and permission rules. `openxiangda studio` is the local loopback-only developer view and exposes only registered safe actions.
142
146
 
147
+ Treat DataView `status` as a platform lifecycle observation during candidate binding checks. The same deployment may change `resources.dataViews.<code>.status` between `draft` and `active`, including between preproduction and production confirmation, without invalidating the sealed candidate. Continue to validate the DataView ID, materialized-view name, storage mode, every other resource status, hashes, environment identity, CAS, lease, and source/mainline evidence exactly.
148
+
143
149
  A sealed candidate may continue from a later clean, pushed authoritative mainline commit only when its commit remains a Git ancestor and all sealed release inputs retain their exact hashes. If any input changed, create a new candidate; never edit the private candidate file. The CLI waits for the app lease and target deployment slot before writes, and the platform permits one running or evidence-pending deployment per target. Emergency releases still use a narrow L1 scope and the same candidate → preproduction → production path; `--wait-seconds 0` only fails fast.
144
150
 
145
151
  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 and freezes the pair before candidate/deployment creation. The later `--confirm-production` invocation automatically reuses the same intent from `ship.json` 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.252",
3
+ "version": "1.0.254",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "openxiangda_engineering_policy_v1",
3
- "policyVersion": 6,
3
+ "policyVersion": 7,
4
4
  "risk": {
5
5
  "levels": ["L0", "L1", "L2", "L3"],
6
6
  "quickMaxLevel": "L1",
@@ -39,6 +39,8 @@
39
39
  "deletionsFailClosed": true,
40
40
  "attemptFencing": true,
41
41
  "crossMachineBindingReplay": true,
42
+ "deterministicRuntimeBuildIdentity": "runtime-layer-plus-package-digest",
43
+ "deterministicRuntimeUploadedReleaseReuse": "exact-content-source-parent-only",
42
44
  "statusAutoResolvesEnvironment": true,
43
45
  "v2RequiresSdd": false,
44
46
  "v2RequiresGitMainline": false,
@@ -21,7 +21,7 @@ This is an OpenXiangda React SPA workspace using Delivery V2. See [DELIVERY.md](
21
21
  ## Always
22
22
 
23
23
  - Delivery V2 derives exact scope and does not require SDD, Git clean, `--change`, `--only`, candidate, or ship.
24
- - Treat `check --json` as the authoritative exact delta and execution plan. Deletions and unsealed third-party build dependencies fail before writes; package execution uses only the CLI toolchain and never links workspace `node_modules`. `status`/`retry` auto-locate the environment, while retry fences the old attempt and replays only cross-machine local Form bindings.
24
+ - Treat `check --json` as the authoritative exact delta and execution plan. Deletions and unsealed third-party build dependencies fail before writes; package execution uses only the CLI toolchain and never links workspace `node_modules`. `status`/`retry` auto-locate the environment, while retry fences the old attempt and replays only cross-machine local Form bindings. Runtime build IDs combine the Runtime layer and sealed package digests; only an exact same-package `uploaded` release with matching content, source, and parent may be reused, and immutable storage is never overwritten.
25
25
  - Writes and deploys must pass `--profile <name>` explicitly.
26
26
  - Architecture-class work is plan-gated: run `openxiangda design gates --topic <code> --json` and wait for confirmation.
27
27
  - Give every task an isolated worktree/branch and one explicit development change; feature worktrees never publish. Merge approved commits to the remote default branch, create one `sdd bundle <release-change> --changes ...`, commit/push it, then publish once from the synchronized clean main/master checkout.
@@ -21,7 +21,7 @@ This is an OpenXiangda React SPA workspace using Delivery V2. Read [DELIVERY.md]
21
21
  ## Always
22
22
 
23
23
  - Delivery V2 自动计算精确范围,不要求 SDD、Git clean、`--change`、`--only` 或 candidate/ship。
24
- - `check --json` 是 V2 权威预检:确认精确资源差异和执行计划。删除与未密封第三方构建依赖在写入前失败;包执行只用 CLI 工具链且不链接工作区 `node_modules`。`status`/`retry` 自动定位环境,retry 以 attempt 隔离旧执行器并仅重放跨机器所需的本地 Form 绑定。
24
+ - `check --json` 是 V2 权威预检:确认精确资源差异和执行计划。删除与未密封第三方构建依赖在写入前失败;包执行只用 CLI 工具链且不链接工作区 `node_modules`。`status`/`retry` 自动定位环境,retry 以 attempt 隔离旧执行器并仅重放跨机器所需的本地 Form 绑定。Runtime buildId 同时包含 Runtime layer 与密封包摘要;仅同包 `uploaded` Release 在内容、源码、父版本完全一致时复用,不覆盖不可变存储对象。
25
25
  - 发布和写平台资源必须显式传 `--profile <name>`。
26
26
  - 架构类需求先跑 `openxiangda design gates --topic <code> --json` 并等用户确认。
27
27
  - 每个任务使用独立 worktree/branch 和一个开发 change,但 feature worktree 不发布。批准提交先合并并 push 到远端默认主分支,再创建一个 `sdd bundle <release-change> --changes ...`,从同步且干净的 main/master 一次发布。
@@ -1,11 +1,11 @@
1
- <!-- OpenXiangda-Policy-Version: 6 -->
1
+ <!-- OpenXiangda-Policy-Version: 7 -->
2
2
  # AGENTS.md — OpenXiangda React SPA 应用工作区
3
3
 
4
4
  本工作区是标准 React 18 + Vite + React Router 应用。默认模板只提供应用壳、账号菜单和一个首页,不是开发验证控制台。
5
5
 
6
6
  ## Delivery V2(发布唯一入口)
7
7
 
8
- 本工作区声明 `deliveryVersion: 2`,正常发布只使用 `openxiangda check`、`openxiangda deploy`、`openxiangda status`、`openxiangda retry`、`openxiangda rollback`,完整约定见 [DELIVERY.md](DELIVERY.md)。V2 按资源指纹计算精确范围,使用不依赖工作区 `node_modules` 的 CLI 密封工具链,并在服务端持久化 ReleaseRun、attempt 和检查点;跨机器重试只重放本地 Form 绑定,删除和未密封构建依赖在写入前失败关闭。`status`/`retry` 未指定环境时会自动定位预发或生产。V2 不要求 SDD、Git clean、主线 ancestry、`--change`、`--only` 或生产确认参数。本文后续出现的 `resource publish`、`runtime deploy`、`release publish/ship`、candidate、SDD/mainline 发布门禁均为 V1 底层兼容说明,不得用于 V2 正常发布。
8
+ 本工作区声明 `deliveryVersion: 2`,正常发布只使用 `openxiangda check`、`openxiangda deploy`、`openxiangda status`、`openxiangda retry`、`openxiangda rollback`,完整约定见 [DELIVERY.md](DELIVERY.md)。V2 按资源指纹计算精确范围,使用不依赖工作区 `node_modules` 的 CLI 密封工具链,并在服务端持久化 ReleaseRun、attempt 和检查点;跨机器重试只重放本地 Form 绑定。Runtime buildId 同时包含 Runtime layer 与密封包摘要,避免相同 Runtime 字节在不同包之间发生来源碰撞;同包重试仅在历史 Release 为 `uploaded` 且内容、源码、父版本完全一致时复用,不覆盖不可变存储对象。删除和未密封构建依赖在写入前失败关闭。`status`/`retry` 未指定环境时会自动定位预发或生产。V2 不要求 SDD、Git clean、主线 ancestry、`--change`、`--only` 或生产确认参数。本文后续出现的 `resource publish`、`runtime deploy`、`release publish/ship`、candidate、SDD/mainline 发布门禁均为 V1 底层兼容说明,不得用于 V2 正常发布。
9
9
 
10
10
  ## 开发原则
11
11
 
@@ -60,6 +60,8 @@ openxiangda commands --json
60
60
 
61
61
  工作区一旦通过 `environment init` 登记或 `environment attach` 接入,`release publish` 即不再是入口。`release ship` 始终按 candidate → preproduction → production 执行:日常首条命令在预发停止,后续 `--confirm-production` 晋级;用户明确授权紧急发布时,首条命令可直接携带 `--confirm-production`,在一个命令内顺序执行两阶段,但不跳过预发、证据、CAS 或确认。candidate 封存源码、public、构建配置/脚本、稳定环境/资源绑定和两目标 Runtime 哈希产物;部署不再现场构建,并将两条服务端 deployment 都闭环为 `succeeded`。两套环境身份仍完全隔离,人工验收默认建议且可用 `--acceptance-note` 留痕;swap、policy 和权限规则保持不变。`openxiangda studio` 用于查看绑定、漂移、候选、部署和证据。
62
62
 
63
+ DataView 的 `status` 是平台生命周期观察值;同一托管部署在预发暂存和正式确认之间发生 `active → draft → active` 不构成候选漂移。候选仍严格校验 `dataViewId`、`materializedViewName`、`storageMode`、其他资源状态、哈希、环境身份、CAS、租约和来源主线。
64
+
63
65
  预发 UAT 未通过时,使用 `release fail --deployment <id> --message "..." [--code <code>] [--details-json <JSON|file>] --environment preproduction --profile <name>` 留下平台审计记录。CLI 会先核对 deployment 属于所选预发环境;禁止对 production target 或不匹配的 deployment 使用。
64
66
 
65
67
  托管发布完成后运行 `release integration-status --change <change> --profile <name> --check`。CLI 会从私有 `ship.json` 恢复血缘,必要时自动沿 production/preproduction deployment ID 查找对应 `execution.json`;失败信息必须指出实际缺失的日志或字段。
@@ -30,7 +30,11 @@ Rules:
30
30
  dependencies fail preflight.
31
31
  - On failure, keep the `runId`. `status` and `retry` auto-locate its environment
32
32
  when omitted. Retry preserves successful checkpoints, fences the previous
33
- attempt, and safely replays local Form bindings on another machine.
33
+ attempt, and safely replays local Form bindings on another machine. Runtime
34
+ build IDs include the Runtime layer and sealed package digests, preventing
35
+ cross-package provenance collisions. A same-package retry reuses an existing
36
+ Runtime release only in `uploaded` state after exact content, source, and
37
+ parent verification; storage is never overwritten.
34
38
  - Project `.env`, credentials, private keys, `.git`, `node_modules`, `dist`,
35
39
  and generated `.openxiangda` state are never included in the authored source
36
40
  layer. Runtime output is a separate immutable layer.
@@ -27,7 +27,7 @@ This is a `sy-lowcode-app-workspace` managed by the `openxiangda` CLI. See [AGEN
27
27
 
28
28
  ## Always
29
29
 
30
- - For `deliveryVersion: 2`, use only Delivery V2 commands. Later V1 SDD/mainline/`--only`/candidate/ship text is compatibility guidance and must not be mixed into V2.
30
+ - For `deliveryVersion: 2`, use only Delivery V2 commands. Runtime build IDs combine the Runtime layer and sealed package digests; retry may reuse only an exact same-package `uploaded` release whose content, source, and parent match. Immutable storage is never overwritten. Later V1 SDD/mainline/`--only`/candidate/ship text is compatibility guidance and must not be mixed into V2.
31
31
  - Treat V2 `check --json` as the authoritative exact delta and execution plan. Deletions and unsealed third-party build dependencies fail before writes; package execution uses only the CLI toolchain and never links workspace `node_modules`. `status`/`retry` auto-locate the environment, while retry fences the old attempt and replays only cross-machine local Form bindings.
32
32
  - Give every task an isolated worktree/branch and one development change; feature worktrees never publish. Merge approved commits to the remote default branch, create one `sdd bundle`, commit/push it, and publish once from synchronized clean main/master.
33
33
  - Close tasks from canonical main only. Review `openxiangda workspace cleanup`, then run `openxiangda workspace cleanup --apply` for that exact hashed `SAFE` plan; it revalidates managed worktrees under the owner lock, never picks up newly-safe entries, and never runs global `git worktree prune`. Review stale records separately.
@@ -27,7 +27,7 @@ This is a `sy-lowcode-app-workspace` managed by the `openxiangda` CLI. Read [AGE
27
27
 
28
28
  ## Always
29
29
 
30
- - `deliveryVersion: 2` 的正常发布只允许 Delivery V2 命令;后续 V1 SDD/mainline/`--only`/candidate/ship 内容仅作兼容说明,不得混入 V2。
30
+ - `deliveryVersion: 2` 的正常发布只允许 Delivery V2 命令;Runtime buildId 同时包含 Runtime layer 与密封包摘要,仅同包 `uploaded` Release 在内容、源码、父版本完全一致时复用,不覆盖不可变存储对象;后续 V1 SDD/mainline/`--only`/candidate/ship 内容仅作兼容说明,不得混入 V2。
31
31
  - V2 `check --json` 是权威预检:确认精确资源差异和执行计划。删除与未密封第三方构建依赖在写入前失败;包执行只用 CLI 工具链且不链接工作区 `node_modules`。`status`/`retry` 自动定位环境,retry 以 attempt 隔离旧执行器并仅重放跨机器所需的本地 Form 绑定。
32
32
  - 每个任务使用独立 worktree/branch 和一个开发 change,但 feature worktree 不发布。批准提交先合并并 push 到远端默认主分支,再创建一个 `sdd bundle`,从同步且干净的 main/master 一次发布。
33
33
  - 合并、push、发布和 `release end` 后,回到 canonical 主工作区先审阅 `openxiangda workspace cleanup`,再执行 `openxiangda workspace cleanup --apply` 应用精确哈希计划。CLI 在 owner lock 内复核,只删除已审阅的 managed `SAFE` 项,不顺带处理后来才安全的项,也不执行全局 `git worktree prune`;stale 记录单独人工审阅。
@@ -1,4 +1,4 @@
1
- <!-- OpenXiangda-Policy-Version: 6 -->
1
+ <!-- OpenXiangda-Policy-Version: 7 -->
2
2
  # AGENTS.md — OpenXiangda 工作区 AI 强约束
3
3
 
4
4
  > 任何 AI(Qoder / Claude / Codex / Cursor / Copilot 等)在本工作区操作前 **必须先读完本文件**。
@@ -8,7 +8,7 @@
8
8
 
9
9
  **本工作区声明 `deliveryVersion: 2`。所有“发布 / 上线 / 部署 / publish / deploy / ship / release”请求只使用 `openxiangda check`、`openxiangda deploy`、`openxiangda status`、`openxiangda retry`、`openxiangda rollback`,完整约定见 [DELIVERY.md](DELIVERY.md)。**
10
10
 
11
- Delivery V2 自动从期望状态按资源指纹计算精确范围,使用 CLI 密封工具链封存内容寻址 App Package,并在服务端持久化 ReleaseRun、attempt 和检查点;包执行不链接工作区 `node_modules`。`status`/`retry` 可自动定位预发或生产环境,跨机器重试只重放本地 Form 绑定并跳过已完成的平台写入。删除和未密封的第三方构建依赖会在远程写入前失败关闭。它不要求 SDD、Git clean、主线 ancestry、`--change`、`--only` 或生产确认参数。本文后续出现的 `workspace publish`、`resource publish`、`runtime deploy`、`release publish/ship`、candidate、SDD/mainline 发布门禁均属于 V1 底层兼容说明,不得用于 V2 正常发布。
11
+ Delivery V2 自动从期望状态按资源指纹计算精确范围,使用 CLI 密封工具链封存内容寻址 App Package,并在服务端持久化 ReleaseRun、attempt 和检查点;包执行不链接工作区 `node_modules`。`status`/`retry` 可自动定位预发或生产环境,跨机器重试只重放本地 Form 绑定并跳过已完成的平台写入。Runtime buildId 同时包含 Runtime layer 与密封包摘要,避免相同 Runtime 字节在不同包之间发生来源碰撞;同包重试仅在历史 Release 为 `uploaded` 且内容、源码、父版本完全一致时复用,绝不覆盖不可变存储对象。删除和未密封的第三方构建依赖会在远程写入前失败关闭。它不要求 SDD、Git clean、主线 ancestry、`--change`、`--only` 或生产确认参数。本文后续出现的 `workspace publish`、`resource publish`、`runtime deploy`、`release publish/ship`、candidate、SDD/mainline 发布门禁均属于 V1 底层兼容说明,不得用于 V2 正常发布。
12
12
 
13
13
  **架构类需求先过设计门。** 新应用、复杂页面、登录注册、公开访问、权限数据范围、流程自动化、连接器/通知等需求,先 `openxiangda doctor --json` + `openxiangda design gates --topic <code> --json`。只有仍存在会改变实现方向的业务、安全或数据选择时才输出设计并等待确认;用户已经给出具体需求与验收标准时,直接记录结构化 SDD 范围并实现,不再写长篇设计或重复确认。
14
14
 
@@ -65,6 +65,7 @@ Delivery V2 自动从期望状态按资源指纹计算精确范围,使用 CLI
65
65
  - ✅ 未登记环境的旧工作区,正式 promotion 先聚合 mainline bundle 并 commit/push,再运行 `release publish --change <id> --profile <name>`。
66
66
  - ✅ 旧工作区已有 Root、且操作者明确授权无条件恢复时,可执行 `release app-activate <releaseId> --force-activate-without-validation --profile <name>`。该命令不读取 detail/capture,不要求 change、租约、baseline、源码 lineage、状态、parent、hash、resource head 或环境发布门禁;服务端直接在目标 tenant/appType 内以单事务切换 Root 与可识别的 staged children。
67
67
  - ✅ 已通过 `environment init` 或 `environment attach` 接入的工作区使用 `release ship`,始终按 candidate → preproduction → production 执行。日常首条命令在预发停止,后续 `--confirm-production` 晋级;用户明确授权紧急发布时,首条命令可携带 `--confirm-production` 在一个命令内顺序完成两阶段,但不跳过预发、证据、CAS 或确认。candidate 封存源码、public、构建配置/脚本、稳定环境/资源绑定和两目标 Runtime 哈希产物;部署不现场重建,并将两条服务端 deployment 闭环为 `succeeded`。两套环境身份仍完全隔离,人工验收建议和 swap/policy/权限门禁保持不变。
68
+ - ✅ DataView `status` 仅是平台生命周期观察值;同一托管部署导致的 `active → draft → active` 不应让候选失效。`dataViewId`、`materializedViewName`、`storageMode`、其他资源状态、哈希、环境身份、CAS、租约和来源主线仍严格校验。
68
69
  - ✅ 预发 UAT 未通过时,使用 `release fail --deployment <id> --message "..." [--code <code>] [--details-json <JSON|file>] --environment preproduction --profile <name>` 写入平台审计。CLI 会先核对 deployment 属于所选预发环境;production target 或不匹配的 deployment 必须在写入前拒绝。
69
70
  - ✅ 托管发布完成后运行 `release integration-status --change <change> --profile <name> --check`。CLI 会从私有 `ship.json` 恢复血缘,必要时自动沿 production/preproduction deployment ID 查找对应 `execution.json`;失败信息必须指出实际缺失的日志或字段。
70
71
  - ✅ 只有已审计目标早已进入权威主线、线上却由多次历史 lineage 组成且无法对应单一 Git 基线时,第一次 `release ship` 才可增加 `--adopt-online-baseline --adoption-reason "..."`;该意图冻结进私有 `ship.json` 并由后续 `--confirm-production` 自动复用。仅允许精确非删除 selectors,冻结 Head、change/lease、服务端 CAS、staged children 与单次 App finalize 仍是硬门禁。
@@ -30,7 +30,11 @@ Rules:
30
30
  dependencies fail preflight.
31
31
  - On failure, keep the `runId`. `status` and `retry` auto-locate its environment
32
32
  when omitted. Retry preserves successful checkpoints, fences the previous
33
- attempt, and safely replays local Form bindings on another machine.
33
+ attempt, and safely replays local Form bindings on another machine. Runtime
34
+ build IDs include the Runtime layer and sealed package digests, preventing
35
+ cross-package provenance collisions. A same-package retry reuses an existing
36
+ Runtime release only in `uploaded` state after exact content, source, and
37
+ parent verification; storage is never overwritten.
34
38
  - Project `.env`, credentials, private keys, `.git`, `node_modules`, `dist`,
35
39
  and generated `.openxiangda` state are never included in the authored source
36
40
  layer. Runtime output is a separate immutable layer.