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 +9 -2
- package/lib/application-environments.js +26 -12
- package/lib/cli.js +181 -0
- package/lib/delivery-v2-executor.js +26 -1
- package/lib/policy.js +5 -1
- package/openxiangda-skills/SKILL.md +7 -1
- package/openxiangda-skills/references/workspace-state.md +1 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +7 -1
- package/package.json +1 -1
- package/policy/openxiangda-policy.json +3 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +1 -1
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +1 -1
- package/templates/openxiangda-react-spa/AGENTS.md +4 -2
- package/templates/openxiangda-react-spa/DELIVERY.md +5 -1
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +1 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +1 -1
- package/templates/sy-lowcode-app-workspace/AGENTS.md +3 -2
- package/templates/sy-lowcode-app-workspace/DELIVERY.md +5 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- OpenXiangda-Policy-Version:
|
|
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 =>
|
|
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(
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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:
|
|
833
|
-
current:
|
|
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
|
-
|
|
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
|
+
<!-- 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
|
"schemaVersion": "openxiangda_engineering_policy_v1",
|
|
3
|
-
"policyVersion":
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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.
|