openxiangda 1.0.173 → 1.0.175

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
@@ -94,6 +94,8 @@ Architecture-class requests are plan-gated by default. For new apps, complex pag
94
94
 
95
95
  Risk is tiered. Read-only/docs/tests are L0. Narrow reversible fixes are L1 and use `sdd quick`; L2/L3 retain explicit approval and exact structured coverage. SDD is streamlined by default: incomplete task/evidence/spec prose warns but does not block. Workspaces that intentionally require prose completion may set `strictDocumentation: true`. Actual argv, exact scope, mainline identity, lease, CAS, destructive operations, Secrets, and Root App atomic activation remain hard gates.
96
96
 
97
+ L1 quick records are generated in a compact form and should not be expanded into design essays. Mainline bundles may keep `<profile>` in reviewed command templates; `release publish --profile <name>` binds the real profile to actual argv without rewriting tracked SDD. React SPA page codes remain logical coverage and activate through one Runtime child. `release app-head` and `runtime releases` return compact summaries by default; add `--full` for the complete manifest.
98
+
97
99
  Every concurrent task uses its own Git worktree/branch and development change. The mainline release coordinator bundles selected approved changes after merge; dependency impact outside the approved scope is reported as a warning rather than silently widening a small release. Live commands still require canonical exact selectors, and app-wide/delete operations need explicit authority.
98
100
 
99
101
  Before confirmation, agents may read, inspect, snapshot, dry-run, ask questions, and output/write the architecture document. They must not edit source files, mutate platform resources, publish, deploy, send notifications, or call live write/delete endpoints.
package/lib/cli.js CHANGED
@@ -949,6 +949,60 @@ function summarizeReleaseStepResult(result) {
949
949
  };
950
950
  }
951
951
 
952
+ function releaseExecutionContextFromBegin(result, target, changeId) {
953
+ const value =
954
+ result && typeof result === 'object' && !Array.isArray(result)
955
+ ? result
956
+ : {};
957
+ const releaseSourceRevision =
958
+ value.releaseSourceRevision &&
959
+ typeof value.releaseSourceRevision === 'object' &&
960
+ !Array.isArray(value.releaseSourceRevision)
961
+ ? value.releaseSourceRevision
962
+ : null;
963
+ if (
964
+ !value.leaseId ||
965
+ !value.clientSessionId ||
966
+ !releaseSourceRevision?.baseCommit
967
+ ) {
968
+ return null;
969
+ }
970
+ return {
971
+ appType: target.appType,
972
+ profile: target.profileName,
973
+ changeId,
974
+ leaseId: String(value.leaseId),
975
+ baselineId: value.baselineId ? String(value.baselineId) : null,
976
+ clientSessionId: String(value.clientSessionId),
977
+ releaseSourceRevision,
978
+ capturedAt: new Date().toISOString(),
979
+ };
980
+ }
981
+
982
+ function isDefinitelyPreWriteReleaseError(error) {
983
+ const code = String(error?.code || '');
984
+ if (new Set([
985
+ 'RELEASE_PUBLISH_REVISION_CHANGED',
986
+ 'RELEASE_SOURCE_DIRTY',
987
+ 'RELEASE_SOURCE_BRANCH_REQUIRED',
988
+ 'RELEASE_SOURCE_MAINLINE_REQUIRED',
989
+ 'RELEASE_SOURCE_MAINLINE_NOT_PUSHED',
990
+ 'RELEASE_SOURCE_BEHIND_MAIN',
991
+ 'RELEASE_GIT_AUTH_REQUIRED',
992
+ 'RELEASE_MAIN_BRANCH_UNRESOLVED',
993
+ 'RELEASE_MAIN_REF_UNVERIFIED',
994
+ 'SDD_PREPUBLISH_FAILED',
995
+ 'PUBLISH_CONTEXT_REQUIRED',
996
+ 'APP_RELEASE_STAGED_SCOPE_REQUIRED',
997
+ 'APP_RELEASE_STAGED_SCOPE_MISMATCH',
998
+ ]).has(code)) {
999
+ return true;
1000
+ }
1001
+ return /^(?:SDD_|RELEASE_(?:SOURCE|GIT|MAINLINE|MAIN_BRANCH|MAIN_REF)|RUNTIME_(?:SOURCE|BUILD|PACKAGE|DEPENDENCY))/.test(
1002
+ code
1003
+ );
1004
+ }
1005
+
952
1006
  function parseChildJsonOutput(stdout) {
953
1007
  const text = String(stdout || '').trim();
954
1008
  if (!text) return null;
@@ -1061,6 +1115,138 @@ async function waitForPublishLeaseAvailability(config, target, flags = {}) {
1061
1115
  }
1062
1116
  }
1063
1117
 
1118
+ function readReleaseRecoveryContext(target, changeId) {
1119
+ if (!changeId) return null;
1120
+ const execution = readReleaseExecution(changeId);
1121
+ const context = execution?.releaseContext;
1122
+ if (!context || typeof context !== 'object' || Array.isArray(context)) {
1123
+ return null;
1124
+ }
1125
+ if (
1126
+ context.appType !== target.appType ||
1127
+ context.profile !== target.profileName ||
1128
+ context.changeId !== changeId ||
1129
+ !context.leaseId ||
1130
+ !context.clientSessionId ||
1131
+ !context.releaseSourceRevision
1132
+ ) {
1133
+ return null;
1134
+ }
1135
+ return { execution, context };
1136
+ }
1137
+
1138
+ function remoteLeaseOwnedByRecovery(remote, recovery) {
1139
+ if (!remote?.active || !recovery) return false;
1140
+ const holder = remote.holder;
1141
+ const holderIsSelf =
1142
+ holder === 'self' ||
1143
+ holder?.self === true ||
1144
+ holder?.clientSessionId === recovery.clientSessionId;
1145
+ return Boolean(
1146
+ holderIsSelf &&
1147
+ String(remote.leaseId || '') === String(recovery.leaseId) &&
1148
+ String(remote.changeId || '') === String(recovery.changeId)
1149
+ );
1150
+ }
1151
+
1152
+ async function endReleaseWithoutLocalLease(
1153
+ config,
1154
+ target,
1155
+ flags,
1156
+ baseline
1157
+ ) {
1158
+ const changeId = readStringFlag(flags, 'change');
1159
+ const recovered = readReleaseRecoveryContext(target, changeId);
1160
+ const recovery = recovered?.context || null;
1161
+ const clientSessionId =
1162
+ recovery?.clientSessionId ||
1163
+ baseline?.clientSessionId ||
1164
+ createPublishClientSessionId();
1165
+ const remote = await fetchPublishLeaseStatus(
1166
+ config,
1167
+ target,
1168
+ clientSessionId
1169
+ );
1170
+ const sourceRevision =
1171
+ baseline?.releaseSourceRevision ||
1172
+ recovery?.releaseSourceRevision ||
1173
+ null;
1174
+
1175
+ if (remote?.active) {
1176
+ if (!recovery) {
1177
+ fail(
1178
+ `PUBLISH_LEASE_RECOVERY_REQUIRED: 远端仍有活动发布租约 ${remote.leaseId || '-'};本地状态缺失,禁止误报 inactive。请使用原 change 的私有 execution.json 恢复,或等待租约过期后只读核对`
1179
+ );
1180
+ }
1181
+ if (!remoteLeaseOwnedByRecovery(remote, recovery)) {
1182
+ fail(
1183
+ 'PUBLISH_LEASE_RECOVERY_MISMATCH: 远端活动租约不属于当前 change/session;未释放任何租约'
1184
+ );
1185
+ }
1186
+ const integration = assertReleaseSourceIntegrated(sourceRevision, {
1187
+ cwd: process.cwd(),
1188
+ });
1189
+ const remoteResult = await requestWithAuth(
1190
+ config,
1191
+ target.profileName,
1192
+ publishLeaseApiPath(
1193
+ target,
1194
+ `/${encodeURIComponent(recovery.leaseId)}/release`
1195
+ ),
1196
+ {
1197
+ method: 'POST',
1198
+ body: {
1199
+ completion: 'mainline-integrated-recovered',
1200
+ sourceCommit: integration.sourceCommit,
1201
+ mainBranch: integration.mainBranch,
1202
+ mainTipCommit: integration.mainTipCommit,
1203
+ },
1204
+ }
1205
+ );
1206
+ if (baseline) {
1207
+ clearChangeBaseline(target, baseline.baselineId || baseline.id);
1208
+ }
1209
+ releaseWorktreeOwner({ cwd: process.cwd() });
1210
+ recovered.execution.leaseReleasedAt = new Date().toISOString();
1211
+ recovered.execution.leaseReleaseRecovered = true;
1212
+ recovered.execution.updatedAt = recovered.execution.leaseReleasedAt;
1213
+ writePrivateJsonAtomic(
1214
+ releaseExecutionFile(changeId),
1215
+ recovered.execution
1216
+ );
1217
+ return {
1218
+ ...(remoteResult && typeof remoteResult === 'object'
1219
+ ? remoteResult
1220
+ : {}),
1221
+ active: false,
1222
+ appType: target.appType,
1223
+ recovered: true,
1224
+ integration,
1225
+ };
1226
+ }
1227
+
1228
+ const integration = sourceRevision
1229
+ ? assertReleaseSourceIntegrated(sourceRevision, { cwd: process.cwd() })
1230
+ : null;
1231
+ if (baseline && !integration) {
1232
+ fail(
1233
+ 'RELEASE_SOURCE_LINEAGE_REQUIRED: 本地仍有发布基线,但缺少可验证的 releaseSourceRevision;禁止无证据清理'
1234
+ );
1235
+ }
1236
+ if (baseline) {
1237
+ clearChangeBaseline(target, baseline.baselineId || baseline.id);
1238
+ }
1239
+ releaseWorktreeOwner({ cwd: process.cwd() });
1240
+ return {
1241
+ active: false,
1242
+ appType: target.appType,
1243
+ local: false,
1244
+ remote,
1245
+ recovered: false,
1246
+ integration,
1247
+ };
1248
+ }
1249
+
1064
1250
  async function publishWorkspaceRelease(config, target, flags = {}) {
1065
1251
  const changeId = readStringFlag(flags, 'change');
1066
1252
  if (!changeId) {
@@ -1239,13 +1425,6 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
1239
1425
  }
1240
1426
  journalStep.status = 'running';
1241
1427
  journalStep.startedAt = new Date().toISOString();
1242
- if (
1243
- step.id !== 'lease-and-capture' &&
1244
- step.id !== 'release-lease' &&
1245
- step.writes !== false
1246
- ) {
1247
- execution.writeAttempted = true;
1248
- }
1249
1428
  execution.updatedAt = journalStep.startedAt;
1250
1429
  writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
1251
1430
  warn(`发布步骤开始: ${step.id}`);
@@ -1253,6 +1432,20 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
1253
1432
  journalStep.status = 'completed';
1254
1433
  journalStep.completedAt = new Date().toISOString();
1255
1434
  journalStep.result = summarizeReleaseStepResult(result);
1435
+ if (step.id === 'lease-and-capture') {
1436
+ execution.releaseContext = releaseExecutionContextFromBegin(
1437
+ result,
1438
+ target,
1439
+ changeId
1440
+ );
1441
+ }
1442
+ if (
1443
+ step.id !== 'lease-and-capture' &&
1444
+ step.id !== 'release-lease' &&
1445
+ step.writes !== false
1446
+ ) {
1447
+ execution.writeAttempted = true;
1448
+ }
1256
1449
  if (step.stagedKind) execution.stagedWriteOccurred = true;
1257
1450
  execution.updatedAt = journalStep.completedAt;
1258
1451
  writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
@@ -1282,6 +1475,18 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
1282
1475
  code: error.code || 'RELEASE_STEP_FAILED',
1283
1476
  message: maskText(error.message).slice(0, 1000),
1284
1477
  };
1478
+ const runningDefinition = runnable.find(
1479
+ step => step.id === runningStep.id
1480
+ );
1481
+ if (
1482
+ runningDefinition &&
1483
+ runningDefinition.id !== 'lease-and-capture' &&
1484
+ runningDefinition.id !== 'release-lease' &&
1485
+ runningDefinition.writes !== false &&
1486
+ !isDefinitelyPreWriteReleaseError(error)
1487
+ ) {
1488
+ execution.writeAttempted = true;
1489
+ }
1285
1490
  }
1286
1491
  execution.status = execution.stagedWriteOccurred
1287
1492
  ? 'staged-resumable'
@@ -1332,6 +1537,7 @@ async function release(args) {
1332
1537
  ' openxiangda release app-finalize --change <id> --staged-resources-json <JSON|file> --profile <name>',
1333
1538
  '说明:',
1334
1539
  ' - publish 默认等待租约并按私有执行日志恢复;上次写结果不确定时必须只读核对后显式 --resume-after-review。',
1540
+ ' - app-head 默认只输出紧凑 head 摘要;需要完整 manifest 时显式追加 --full。',
1335
1541
  ' - begin 获取应用级单写者 promotion lease;不同工作区仍可并行开发和验证。',
1336
1542
  ' - 带 --change 的 resource/runtime 写命令在没有本地 lease 时会自动 begin,并在后续命令复用。',
1337
1543
  ' - 子 Runtime/Page/Backend/Form Release 默认先 stage;app-finalize 用 --staged-resources-json 覆盖 capture 中的 changed children,并在同一事务中原子激活。',
@@ -1417,10 +1623,14 @@ async function release(args) {
1417
1623
  const baseline = getStoredChangeBaseline(target, {
1418
1624
  access: 'reconciliation-read',
1419
1625
  });
1420
- const sourceRevision = baseline?.releaseSourceRevision;
1626
+ const changeId = positional[0] || readStringFlag(flags, 'change');
1627
+ const recovered = readReleaseRecoveryContext(target, changeId);
1628
+ const sourceRevision =
1629
+ baseline?.releaseSourceRevision ||
1630
+ recovered?.context?.releaseSourceRevision;
1421
1631
  if (!sourceRevision) {
1422
1632
  fail(
1423
- 'RELEASE_SOURCE_LINEAGE_REQUIRED: 当前 release 没有冻结的 releaseSourceRevision,不能证明发布提交已回到权威主分支'
1633
+ 'RELEASE_SOURCE_LINEAGE_REQUIRED: 当前 release 没有冻结的 releaseSourceRevision;发布已结束时请同时传入 --change <id>,以便从私有执行日志恢复主线证据'
1424
1634
  );
1425
1635
  }
1426
1636
  const result = inspectReleaseSourceIntegration(
@@ -1599,34 +1809,22 @@ async function release(args) {
1599
1809
  return;
1600
1810
  }
1601
1811
  if (!leaseIdFromArgs) assertLocalPublishLeaseOwnership(stored);
1602
- assertOrClaimWorktreeOwner({
1603
- cwd: process.cwd(),
1604
- changeId: stored?.changeId || readStringFlag(flags, 'change') || 'unscoped-publish',
1605
- });
1606
1812
  const leaseId = leaseIdFromArgs || stored?.leaseId;
1607
1813
  if (!leaseId) {
1608
- const integration = baseline?.releaseSourceRevision
1609
- ? assertReleaseSourceIntegrated(baseline.releaseSourceRevision, {
1610
- cwd: process.cwd(),
1611
- })
1612
- : null;
1613
- if (baseline && !integration) {
1614
- fail(
1615
- 'RELEASE_SOURCE_LINEAGE_REQUIRED: 本地仍有发布基线,但缺少可验证的 releaseSourceRevision;禁止无证据清理'
1616
- );
1617
- }
1618
- const result = {
1619
- active: false,
1620
- appType: target.appType,
1621
- local: false,
1622
- integration,
1623
- };
1624
- if (baseline) clearChangeBaseline(target, baseline.baselineId);
1625
- releaseWorktreeOwner({ cwd: process.cwd() });
1814
+ const result = await endReleaseWithoutLocalLease(
1815
+ config,
1816
+ target,
1817
+ flags,
1818
+ baseline
1819
+ );
1626
1820
  if (flags.json) return writeJson(result);
1627
1821
  print(`当前工作区没有活动发布租约: ${target.appType}`);
1628
1822
  return;
1629
1823
  }
1824
+ assertOrClaimWorktreeOwner({
1825
+ cwd: process.cwd(),
1826
+ changeId: stored?.changeId || readStringFlag(flags, 'change') || 'unscoped-publish',
1827
+ });
1630
1828
  const integration = assertReleaseSourceIntegrated(
1631
1829
  baseline?.releaseSourceRevision,
1632
1830
  { cwd: process.cwd() }
@@ -2427,8 +2625,15 @@ function changedAppReleaseResources(detailResources, captureResources) {
2427
2625
  }
2428
2626
 
2429
2627
  function assertAppReleaseActualGate(target, flags, scope, command) {
2628
+ const runtimeMode = getWorkspaceRuntimeModeFromConfig(
2629
+ readWorkspaceConfigText()
2630
+ );
2631
+ const normalized = normalizeReleaseTargets(
2632
+ scope.targets || {},
2633
+ runtimeMode
2634
+ );
2430
2635
  assertSddReleaseGate(
2431
- buildExplicitSddReleasePlan(scope.targets, {
2636
+ buildExplicitSddReleasePlan(normalized.activationTargets, {
2432
2637
  files: scope.changedFiles || [],
2433
2638
  commands: [command],
2434
2639
  changeId: scope.changeId,
@@ -2596,6 +2801,70 @@ async function activateAppReleaseDetail(
2596
2801
  );
2597
2802
  }
2598
2803
 
2804
+ function summarizeAppReleaseHead(data) {
2805
+ const value =
2806
+ data && typeof data === 'object' && !Array.isArray(data) ? data : {};
2807
+ const release =
2808
+ value.release &&
2809
+ typeof value.release === 'object' &&
2810
+ !Array.isArray(value.release)
2811
+ ? value.release
2812
+ : {};
2813
+ const manifest =
2814
+ release.manifestJson &&
2815
+ typeof release.manifestJson === 'object'
2816
+ ? release.manifestJson
2817
+ : value.manifestJson &&
2818
+ typeof value.manifestJson === 'object'
2819
+ ? value.manifestJson
2820
+ : {};
2821
+ const resources = Array.isArray(manifest.resources)
2822
+ ? manifest.resources
2823
+ : Array.isArray(value.resources)
2824
+ ? value.resources
2825
+ : [];
2826
+ const resourcesByKind = Object.fromEntries(
2827
+ Array.from(
2828
+ resources.reduce((counts, resource) => {
2829
+ const kind = String(resource?.kind || 'Unknown');
2830
+ counts.set(kind, (counts.get(kind) || 0) + 1);
2831
+ return counts;
2832
+ }, new Map())
2833
+ ).sort(([left], [right]) => left.localeCompare(right))
2834
+ );
2835
+ const compactRelease = {};
2836
+ for (const key of [
2837
+ 'id',
2838
+ 'status',
2839
+ 'parentReleaseId',
2840
+ 'manifestHash',
2841
+ 'verificationHash',
2842
+ 'protocolVersion',
2843
+ 'createdAt',
2844
+ 'updatedAt',
2845
+ 'activatedAt',
2846
+ ]) {
2847
+ if (release[key] !== undefined) compactRelease[key] = release[key];
2848
+ }
2849
+ return {
2850
+ appType: value.appType,
2851
+ activeAppReleaseId:
2852
+ value.activeAppReleaseId || release.id || null,
2853
+ ...(value.activeRuntimeReleaseId
2854
+ ? { activeRuntimeReleaseId: value.activeRuntimeReleaseId }
2855
+ : {}),
2856
+ ...(value.activePageReleaseId
2857
+ ? { activePageReleaseId: value.activePageReleaseId }
2858
+ : {}),
2859
+ ...(value.activeBackendReleaseId
2860
+ ? { activeBackendReleaseId: value.activeBackendReleaseId }
2861
+ : {}),
2862
+ release: compactRelease,
2863
+ resourceCount: resources.length,
2864
+ resourcesByKind,
2865
+ };
2866
+ }
2867
+
2599
2868
  async function runAppReleaseCommand(
2600
2869
  config,
2601
2870
  target,
@@ -2613,6 +2882,7 @@ async function runAppReleaseCommand(
2613
2882
  target.profileName,
2614
2883
  appReleaseApiPath(target, '/head')
2615
2884
  );
2885
+ if (!flags.full) data = summarizeAppReleaseHead(data);
2616
2886
  } else if (subcommand === 'app-list') {
2617
2887
  data = await requestWithAuth(
2618
2888
  config,
@@ -2835,7 +3105,17 @@ async function runAppReleaseCommand(
2835
3105
  let stateReconcileWarning = null;
2836
3106
  try {
2837
3107
  const appHead = await loadRuntimeHeadApp(config, target);
2838
- saveRuntimeHeadState(target, appHead);
3108
+ const stagedRuntime = stagedResources?.find(
3109
+ resource => resource.kind === 'RuntimeRelease'
3110
+ );
3111
+ saveRuntimeHeadState(target, appHead, {
3112
+ id:
3113
+ stagedRuntime?.identity?.releaseId ||
3114
+ stagedRuntime?.releaseId ||
3115
+ null,
3116
+ buildId: stagedRuntime?.metadata?.buildId || null,
3117
+ assetBaseUrl: stagedRuntime?.metadata?.assetBaseUrl || null,
3118
+ });
2839
3119
  } catch (error) {
2840
3120
  stateReconcileWarning = {
2841
3121
  component: 'RuntimeStateReadback',
@@ -9687,11 +9967,11 @@ async function runtime(args) {
9687
9967
  const { flags, positional } = parseArgs(rest);
9688
9968
  if (wantsSubcommandHelp(subcommand, flags)) {
9689
9969
  print([
9690
- '用法: openxiangda runtime deploy|releases|activate [--profile name] [--json]',
9970
+ '用法: openxiangda runtime deploy|releases [--full]|activate [--profile name] [--json]',
9691
9971
  '常用流程:',
9692
9972
  ' npm run build',
9693
9973
  ' openxiangda runtime deploy --no-activate --change <change> --profile <name> --json',
9694
- ' openxiangda runtime releases --profile <name>',
9974
+ ' openxiangda runtime releases --profile <name> [--full]',
9695
9975
  '说明:',
9696
9976
  ' - deploy 上传当前工作区 dist/ 并激活 React SPA runtime release。',
9697
9977
  ' - 默认 --upload-mode auto;multipart 上传遇到 403 时自动切换到平台内置对象存储直传。',
@@ -9718,8 +9998,9 @@ async function runtime(args) {
9718
9998
  target.profileName,
9719
9999
  `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/runtime/releases`
9720
10000
  );
9721
- if (flags.json) return writeJson(data);
9722
- print(JSON.stringify(data, null, 2));
10001
+ const output = flags.full ? data : summarizeRuntimeReleaseList(data);
10002
+ if (flags.json) return writeJson(output);
10003
+ print(JSON.stringify(output, null, 2));
9723
10004
  return;
9724
10005
  }
9725
10006
 
@@ -10032,6 +10313,7 @@ async function runtime(args) {
10032
10313
  const runtimeContentHash = String(data?.contentHash || '')
10033
10314
  .trim()
10034
10315
  .toLowerCase();
10316
+ const releaseAssetBaseUrl = data?.assetBaseUrl || assetBaseUrl;
10035
10317
  const stagedResource = flags['no-activate']
10036
10318
  ? stripUndefinedValues({
10037
10319
  kind: 'RuntimeRelease',
@@ -10045,6 +10327,7 @@ async function runtime(args) {
10045
10327
  metadata: {
10046
10328
  buildId: data?.buildId || buildId,
10047
10329
  releaseStatus: data?.status || 'staged',
10330
+ assetBaseUrl: releaseAssetBaseUrl,
10048
10331
  },
10049
10332
  })
10050
10333
  : null;
@@ -10069,7 +10352,6 @@ async function runtime(args) {
10069
10352
  target
10070
10353
  )
10071
10354
  : null;
10072
- const releaseAssetBaseUrl = data?.assetBaseUrl || assetBaseUrl;
10073
10355
  const result = {
10074
10356
  appType: target.appType,
10075
10357
  buildId,
@@ -10115,7 +10397,7 @@ async function runtime(args) {
10115
10397
  }
10116
10398
  }
10117
10399
 
10118
- fail('用法: openxiangda runtime deploy|releases|activate [--profile name]');
10400
+ fail('用法: openxiangda runtime deploy|releases [--full]|activate [--profile name]');
10119
10401
  }
10120
10402
 
10121
10403
  async function loadRuntimeHeadApp(config, target) {
@@ -10302,9 +10584,11 @@ async function uploadRuntimeDistFilesStaged(options) {
10302
10584
  async file => {
10303
10585
  const uploaded = await uploadRuntimeDistFile(options, file);
10304
10586
  completed += 1;
10305
- printRuntimeProgress(
10306
- `runtime file uploaded [${completed}/${files.length}] ${file.path} ${formatBytes(file.size)} traceId=${options.traceId}`
10307
- );
10587
+ if (shouldReportRuntimeUploadProgress(completed, files.length)) {
10588
+ printRuntimeProgress(
10589
+ `runtime file uploaded [${completed}/${files.length}] ${file.path} ${formatBytes(file.size)} traceId=${options.traceId}`
10590
+ );
10591
+ }
10308
10592
  return uploaded;
10309
10593
  }
10310
10594
  );
@@ -10359,9 +10643,11 @@ async function uploadRuntimeDistFilesOssDirect(options) {
10359
10643
  );
10360
10644
  const uploaded = await uploadRuntimeDistFileToSignedUrl(options, file, uploadInfo);
10361
10645
  completed += 1;
10362
- printRuntimeProgress(
10363
- `runtime ${formatRuntimeStorageProvider(storageProvider)} file uploaded [${completed}/${files.length}] ${file.path} ${formatBytes(file.size)} traceId=${options.traceId}`
10364
- );
10646
+ if (shouldReportRuntimeUploadProgress(completed, files.length)) {
10647
+ printRuntimeProgress(
10648
+ `runtime ${formatRuntimeStorageProvider(storageProvider)} file uploaded [${completed}/${files.length}] ${file.path} ${formatBytes(file.size)} traceId=${options.traceId}`
10649
+ );
10650
+ }
10365
10651
  return { ...uploaded, storageProvider };
10366
10652
  }
10367
10653
  );
@@ -10623,21 +10909,71 @@ function saveRuntimeReleaseState(target, release) {
10623
10909
  saveProjectState(target.state);
10624
10910
  }
10625
10911
 
10626
- function saveRuntimeHeadState(target, app) {
10912
+ function saveRuntimeHeadState(target, app, fallbackRelease = {}) {
10627
10913
  const activeReleaseId =
10628
- app?.activeRuntimeReleaseId || app?.activeRuntime?.releaseId || null;
10914
+ app?.activeRuntimeReleaseId ||
10915
+ app?.activeRuntime?.releaseId ||
10916
+ fallbackRelease?.id ||
10917
+ fallbackRelease?.releaseId ||
10918
+ null;
10629
10919
  const activeBuildId =
10630
- app?.activeRuntimeBuildId || app?.activeRuntime?.buildId || null;
10920
+ app?.activeRuntimeBuildId ||
10921
+ app?.activeRuntime?.buildId ||
10922
+ fallbackRelease?.buildId ||
10923
+ null;
10631
10924
  if (!activeReleaseId && !activeBuildId) return false;
10632
10925
  saveRuntimeReleaseState(target, {
10633
10926
  id: activeReleaseId,
10634
10927
  buildId: activeBuildId,
10635
10928
  assetBaseUrl:
10636
- app?.runtimeAssetBaseUrl || app?.activeRuntime?.assetBaseUrl || null,
10929
+ app?.runtimeAssetBaseUrl ||
10930
+ app?.activeRuntime?.assetBaseUrl ||
10931
+ fallbackRelease?.assetBaseUrl ||
10932
+ null,
10637
10933
  });
10638
10934
  return true;
10639
10935
  }
10640
10936
 
10937
+ function summarizeRuntimeReleaseList(data) {
10938
+ const items = Array.isArray(data)
10939
+ ? data
10940
+ : Array.isArray(data?.items)
10941
+ ? data.items
10942
+ : [];
10943
+ const summarized = items.map(item =>
10944
+ stripUndefinedValues({
10945
+ id: item?.id,
10946
+ version: item?.version,
10947
+ buildId: item?.buildId,
10948
+ status: item?.status,
10949
+ storageProvider: item?.storageProvider,
10950
+ fileCount: item?.fileCount,
10951
+ sizeBytes: item?.sizeBytes,
10952
+ contentHash: item?.contentHash,
10953
+ sourceRevision: item?.sourceRevision,
10954
+ parentReleaseId: item?.parentReleaseId,
10955
+ parentSourceRevision: item?.parentSourceRevision,
10956
+ lineageBypassReason: item?.lineageBypassReason,
10957
+ lineageBootstrap: item?.lineageBootstrap,
10958
+ releaseNotes: item?.releaseNotes,
10959
+ createdAt: item?.createdAt,
10960
+ activatedAt: item?.activatedAt,
10961
+ })
10962
+ );
10963
+ return Array.isArray(data)
10964
+ ? summarized
10965
+ : {
10966
+ ...(data && typeof data === 'object' ? data : {}),
10967
+ items: summarized,
10968
+ };
10969
+ }
10970
+
10971
+ function shouldReportRuntimeUploadProgress(completed, total) {
10972
+ if (total <= 10) return true;
10973
+ const interval = Math.max(1, Math.ceil(total / 10));
10974
+ return completed === 1 || completed === total || completed % interval === 0;
10975
+ }
10976
+
10641
10977
  function formatBytes(value) {
10642
10978
  const bytes = Number(value || 0);
10643
10979
  if (bytes < 1024) return `${bytes}B`;
@@ -14084,7 +14420,7 @@ async function tryBuildFunctionSourcesWithLegacyWorkspaceBuilder(
14084
14420
 
14085
14421
  let usedLegacyBuilder = false;
14086
14422
  for (const sourceInfo of sourceInfos) {
14087
- const result = runWorkspaceJsCodeBuild(
14423
+ const result = runLegacyWorkspaceJsCodeBuild(
14088
14424
  workspaceRoot,
14089
14425
  sourceInfo.scriptCode,
14090
14426
  sourceInfo.sourceKind
@@ -20971,6 +21307,27 @@ function runWorkspaceJsCodeBuild(workspaceRoot, resolvedScriptCode, sourceKind)
20971
21307
  );
20972
21308
  }
20973
21309
 
21310
+ function runLegacyWorkspaceJsCodeBuild(
21311
+ workspaceRoot,
21312
+ resolvedScriptCode,
21313
+ sourceKind
21314
+ ) {
21315
+ return spawnSync(
21316
+ 'pnpm',
21317
+ [
21318
+ 'build-js-code',
21319
+ '--script',
21320
+ resolvedScriptCode,
21321
+ '--source',
21322
+ sourceKind,
21323
+ ],
21324
+ {
21325
+ cwd: workspaceRoot,
21326
+ encoding: 'utf8',
21327
+ }
21328
+ );
21329
+ }
21330
+
20974
21331
  function isUnsupportedFunctionsBuildScript(result) {
20975
21332
  const output = `${result.stdout || ''}\n${result.stderr || ''}`;
20976
21333
  return (
@@ -264,6 +264,12 @@ function resolveRemoteMainline({ cwd, remoteName }) {
264
264
  `远端 ${remoteName} 同时存在 main 与 master 且无法读取 remote HEAD;请先修复远端默认分支 HEAD`
265
265
  );
266
266
  }
267
+ if (isGitAuthenticationError(remoteHead.stderr)) {
268
+ throw releaseMainlineError(
269
+ 'RELEASE_GIT_AUTH_REQUIRED',
270
+ `无法认证权威远端 ${remoteName}。请先解锁系统钥匙串或配置可用的 Git credential helper,并确认 \`git ls-remote --symref ${remoteName} HEAD\` 可成功执行;发布会在获取平台租约前停止,不会留下远端发布状态`
271
+ );
272
+ }
267
273
  const detail = remoteHead.stderr
268
274
  ? `: ${sanitizeGitError(remoteHead.stderr)}`
269
275
  : '';
@@ -273,6 +279,12 @@ function resolveRemoteMainline({ cwd, remoteName }) {
273
279
  );
274
280
  }
275
281
 
282
+ function isGitAuthenticationError(value) {
283
+ return /(?:authentication failed|could not read (?:username|password)|terminal prompts? disabled|credential|keychain|access denied|http basic: access denied|response:\s*(?:401|403)|returned error:\s*(?:401|403))/i.test(
284
+ String(value || '')
285
+ );
286
+ }
287
+
276
288
  function readRemoteBranchTip(cwd, remoteName, branchName, options = {}) {
277
289
  const result = readGit(
278
290
  cwd,
@@ -577,6 +589,7 @@ module.exports = {
577
589
  assertReleaseSourceIntegrated,
578
590
  assertReleaseSourceRevisionStable,
579
591
  inspectReleaseSourceIntegration,
592
+ isGitAuthenticationError,
580
593
  prepareReleaseSourceRevision,
581
594
  resolveAuthoritativeMainline,
582
595
  };
package/lib/sdd.js CHANGED
@@ -115,9 +115,12 @@ function readJsonFile(file, fallback = null) {
115
115
  }
116
116
  }
117
117
 
118
- function writeJsonFile(file, value) {
118
+ function writeJsonFile(file, value, options = {}) {
119
119
  ensureDir(path.dirname(file));
120
- fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
120
+ fs.writeFileSync(
121
+ file,
122
+ `${JSON.stringify(value, null, options.compact ? 0 : 2)}\n`
123
+ );
121
124
  }
122
125
 
123
126
  function extractSddBlock(configText) {
@@ -578,6 +581,15 @@ function formatAffectedList(items) {
578
581
  }
579
582
 
580
583
  function createProposalContent(meta) {
584
+ if (meta.changeMode === 'quick') {
585
+ return [
586
+ `# Quick Change: ${meta.title}`,
587
+ '',
588
+ `User intent and approval are recorded in \`${CHANGE_META_FILE}\`.`,
589
+ `Exact resource/file scope is recorded in \`${COVERAGE_FILE}\`; no additional proposal prose is required.`,
590
+ '',
591
+ ].join('\n');
592
+ }
581
593
  return `# Proposal: ${meta.title}
582
594
 
583
595
  ## Intent
@@ -606,6 +618,14 @@ Deliver "${meta.title}" as one focused OpenXiangda application change.
606
618
  }
607
619
 
608
620
  function createDesignContent(meta) {
621
+ if (meta.changeMode === 'quick') {
622
+ return [
623
+ '# Design',
624
+ '',
625
+ 'L1 exact-scope change; implementation stays inside coverage.json and uses the generated atomic release plan.',
626
+ '',
627
+ ].join('\n');
628
+ }
609
629
  return `# Design: ${meta.title}
610
630
 
611
631
  ## OpenXiangda Resource Impact
@@ -640,22 +660,19 @@ function createTasksContent(meta = {}) {
640
660
  '',
641
661
  '## Implementation',
642
662
  '',
643
- '- [x] Record exact L1 scope and explicit approval basis',
644
- '- [ ] Implement the focused change',
663
+ '- [ ] Implement the exact covered change',
645
664
  '',
646
665
  '## Prepublish',
647
666
  '',
648
- '- [ ] Record implementation checks and dry-run evidence',
649
- '- [ ] Confirm the exact release plan',
667
+ '- [ ] Run the focused check and confirm the generated release plan',
650
668
  '',
651
669
  '## Postpublish',
652
670
  '',
653
- '- [ ] Record publish/deploy result',
654
- '- [ ] Merge/fast-forward the frozen release SHA and push the authoritative default branch',
671
+ '- [ ] Record atomic publish and mainline integration status',
655
672
  '',
656
673
  '## Archive',
657
674
  '',
658
- '- [ ] Confirm the released change is ready to archive',
675
+ '- [ ] Archive the released change',
659
676
  '',
660
677
  ].join('\n');
661
678
  }
@@ -686,7 +703,21 @@ function createTasksContent(meta = {}) {
686
703
  ].join('\n');
687
704
  }
688
705
 
689
- function createEvidenceContent() {
706
+ function createEvidenceContent(meta = {}) {
707
+ if (meta.changeMode === 'quick') {
708
+ return [
709
+ '# Evidence',
710
+ '',
711
+ '## Verification',
712
+ '',
713
+ '- [ ] Record the focused check result',
714
+ '',
715
+ '## Release',
716
+ '',
717
+ '- [ ] Record the atomic publish result',
718
+ '',
719
+ ].join('\n');
720
+ }
690
721
  return [
691
722
  '# Evidence',
692
723
  '',
@@ -708,6 +739,22 @@ function createEvidenceContent() {
708
739
  }
709
740
 
710
741
  function createDeltaSpecContent(meta, domain) {
742
+ if (meta.changeMode === 'quick') {
743
+ return [
744
+ `# ${domain} Delta Spec`,
745
+ '',
746
+ '## ADDED Requirements',
747
+ '',
748
+ `### Requirement: ${meta.title}`,
749
+ 'The application SHALL preserve existing behavior except for the exact approved L1 scope.',
750
+ '',
751
+ '#### Scenario: Focused change',
752
+ '- **Given** the existing application behavior',
753
+ `- **When** ${meta.title} is applied`,
754
+ '- **Then** only the covered target changes and unrelated behavior remains unchanged',
755
+ '',
756
+ ].join('\n');
757
+ }
711
758
  return `# ${domain} Delta Spec
712
759
 
713
760
  ## ADDED Requirements
@@ -807,13 +854,16 @@ function proposeSddChange(options = {}) {
807
854
  const meta = createChangeMetadata(changeId, options);
808
855
  const coverage = createCoverageManifest(meta, domain);
809
856
  const release = createReleaseManifest(meta);
810
- writeJsonFile(path.join(changeDir, CHANGE_META_FILE), meta);
811
- writeJsonFile(path.join(changeDir, COVERAGE_FILE), coverage);
812
- writeJsonFile(path.join(changeDir, RELEASE_FILE), release);
857
+ const jsonWriteOptions = {
858
+ compact: meta.changeMode === 'quick',
859
+ };
860
+ writeJsonFile(path.join(changeDir, CHANGE_META_FILE), meta, jsonWriteOptions);
861
+ writeJsonFile(path.join(changeDir, COVERAGE_FILE), coverage, jsonWriteOptions);
862
+ writeJsonFile(path.join(changeDir, RELEASE_FILE), release, jsonWriteOptions);
813
863
  fs.writeFileSync(path.join(changeDir, 'proposal.md'), createProposalContent(meta));
814
864
  fs.writeFileSync(path.join(changeDir, 'design.md'), createDesignContent(meta));
815
865
  fs.writeFileSync(path.join(changeDir, 'tasks.md'), createTasksContent(meta));
816
- fs.writeFileSync(path.join(changeDir, 'evidence.md'), createEvidenceContent());
866
+ fs.writeFileSync(path.join(changeDir, 'evidence.md'), createEvidenceContent(meta));
817
867
  fs.writeFileSync(
818
868
  path.join(changeDir, 'specs', domain, 'spec.md'),
819
869
  createDeltaSpecContent(meta, domain)
@@ -1024,7 +1074,9 @@ function approveSddChange(options = {}) {
1024
1074
  ...(options.approvalBasis ? { approvalBasis: options.approvalBasis } : {}),
1025
1075
  updatedAt: nowIso(),
1026
1076
  };
1027
- writeJsonFile(loaded.metaFile, meta);
1077
+ writeJsonFile(loaded.metaFile, meta, {
1078
+ compact: meta.changeMode === 'quick',
1079
+ });
1028
1080
  return {
1029
1081
  schemaVersion: meta.schemaVersion,
1030
1082
  change: meta,
@@ -2052,6 +2104,7 @@ function validateReleaseManifest(
2052
2104
  }
2053
2105
  if (
2054
2106
  !legacyMode &&
2107
+ !options.actualExecution &&
2055
2108
  Array.isArray(release.commands) &&
2056
2109
  release.commands.length > 0
2057
2110
  ) {
@@ -60,6 +60,8 @@ openxiangda sdd quick <change> --kind copy --pages <page> --files <file> --summa
60
60
 
61
61
  Function quick changes require the complete `--risk-json` assessment; Automation, Workflow, schema, permissions, auth/public access, migrations, destructive and cross-resource changes cannot use quick mode. Before release run `--stage prepublish`; use `postpublish` only after promotion and `archive` only when all evidence exists.
62
62
 
63
+ Quick changes use compact generated JSON/prose. Do not expand them into proposal/design essays; implement the exact covered code, run the focused check, and let the mainline bundle own final atomic promotion.
64
+
63
65
  Keep development lightweight: structured approval plus exact resource/file scope are authoritative. By default, unfinished tasks/evidence/spec prose produces warnings at every stage and does not block release; set `strictDocumentation: true` only for a workspace that intentionally wants prose as a gate. Actual publish argv, mainline identity, CAS, lease, destructive scope, and atomic activation remain hard gates.
64
66
 
65
67
  The CLI regenerates one canonical command set from structured coverage instead of asking agents to maintain two command copies. A single Function uses `resource publish function --only <code>`; qualified selectors are used only when Function and Automation are mixed. Generated React SPA plans use exact Form bundles, one Backend `--stage-only` command, Runtime `--no-activate`, and Root `app-finalize`. Actual argv is validated again at write time.
@@ -89,6 +91,8 @@ openxiangda release publish --change <release-change> --profile <name>
89
91
 
90
92
  `release publish` is the default promotion entrypoint. 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. `release begin` and child commands remain recovery/diagnostic primitives.
91
93
 
94
+ 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.
95
+
92
96
  `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.
93
97
 
94
98
  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.
@@ -94,8 +94,12 @@ openxiangda release publish --change <release-change> --profile <name>
94
94
 
95
95
  `release publish` is the normal whole-app entrypoint: 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. Individual release commands are recovery/diagnostic primitives.
96
96
 
97
+ 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.
98
+
97
99
  `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.
98
100
 
101
+ `release app-head` and `runtime releases` return compact summaries by default. Use `--full` only when the complete manifest is required.
102
+
99
103
  `resource plan`, `resource publish`, and `runtime deploy` report coarse phases on stderr and emit a heartbeat after 15 seconds without completion. JSON stdout stays machine-readable and includes `timings`; use it to identify whether time is spent in plan, preflight, build, upload, write, or activate instead of rerunning an opaque command.
100
104
 
101
105
  Use `--upload-mode legacy-json` only for an old platform that lacks staged uploads. Keep timeout/progress on stderr so `--json` stdout remains machine-readable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.173",
3
+ "version": "1.0.175",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {