openxiangda 1.0.173 → 1.0.174

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` returns a compact summary 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,并在同一事务中原子激活。',
@@ -1599,34 +1805,22 @@ async function release(args) {
1599
1805
  return;
1600
1806
  }
1601
1807
  if (!leaseIdFromArgs) assertLocalPublishLeaseOwnership(stored);
1602
- assertOrClaimWorktreeOwner({
1603
- cwd: process.cwd(),
1604
- changeId: stored?.changeId || readStringFlag(flags, 'change') || 'unscoped-publish',
1605
- });
1606
1808
  const leaseId = leaseIdFromArgs || stored?.leaseId;
1607
1809
  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() });
1810
+ const result = await endReleaseWithoutLocalLease(
1811
+ config,
1812
+ target,
1813
+ flags,
1814
+ baseline
1815
+ );
1626
1816
  if (flags.json) return writeJson(result);
1627
1817
  print(`当前工作区没有活动发布租约: ${target.appType}`);
1628
1818
  return;
1629
1819
  }
1820
+ assertOrClaimWorktreeOwner({
1821
+ cwd: process.cwd(),
1822
+ changeId: stored?.changeId || readStringFlag(flags, 'change') || 'unscoped-publish',
1823
+ });
1630
1824
  const integration = assertReleaseSourceIntegrated(
1631
1825
  baseline?.releaseSourceRevision,
1632
1826
  { cwd: process.cwd() }
@@ -2427,8 +2621,15 @@ function changedAppReleaseResources(detailResources, captureResources) {
2427
2621
  }
2428
2622
 
2429
2623
  function assertAppReleaseActualGate(target, flags, scope, command) {
2624
+ const runtimeMode = getWorkspaceRuntimeModeFromConfig(
2625
+ readWorkspaceConfigText()
2626
+ );
2627
+ const normalized = normalizeReleaseTargets(
2628
+ scope.targets || {},
2629
+ runtimeMode
2630
+ );
2430
2631
  assertSddReleaseGate(
2431
- buildExplicitSddReleasePlan(scope.targets, {
2632
+ buildExplicitSddReleasePlan(normalized.activationTargets, {
2432
2633
  files: scope.changedFiles || [],
2433
2634
  commands: [command],
2434
2635
  changeId: scope.changeId,
@@ -2596,6 +2797,70 @@ async function activateAppReleaseDetail(
2596
2797
  );
2597
2798
  }
2598
2799
 
2800
+ function summarizeAppReleaseHead(data) {
2801
+ const value =
2802
+ data && typeof data === 'object' && !Array.isArray(data) ? data : {};
2803
+ const release =
2804
+ value.release &&
2805
+ typeof value.release === 'object' &&
2806
+ !Array.isArray(value.release)
2807
+ ? value.release
2808
+ : {};
2809
+ const manifest =
2810
+ release.manifestJson &&
2811
+ typeof release.manifestJson === 'object'
2812
+ ? release.manifestJson
2813
+ : value.manifestJson &&
2814
+ typeof value.manifestJson === 'object'
2815
+ ? value.manifestJson
2816
+ : {};
2817
+ const resources = Array.isArray(manifest.resources)
2818
+ ? manifest.resources
2819
+ : Array.isArray(value.resources)
2820
+ ? value.resources
2821
+ : [];
2822
+ const resourcesByKind = Object.fromEntries(
2823
+ Array.from(
2824
+ resources.reduce((counts, resource) => {
2825
+ const kind = String(resource?.kind || 'Unknown');
2826
+ counts.set(kind, (counts.get(kind) || 0) + 1);
2827
+ return counts;
2828
+ }, new Map())
2829
+ ).sort(([left], [right]) => left.localeCompare(right))
2830
+ );
2831
+ const compactRelease = {};
2832
+ for (const key of [
2833
+ 'id',
2834
+ 'status',
2835
+ 'parentReleaseId',
2836
+ 'manifestHash',
2837
+ 'verificationHash',
2838
+ 'protocolVersion',
2839
+ 'createdAt',
2840
+ 'updatedAt',
2841
+ 'activatedAt',
2842
+ ]) {
2843
+ if (release[key] !== undefined) compactRelease[key] = release[key];
2844
+ }
2845
+ return {
2846
+ appType: value.appType,
2847
+ activeAppReleaseId:
2848
+ value.activeAppReleaseId || release.id || null,
2849
+ ...(value.activeRuntimeReleaseId
2850
+ ? { activeRuntimeReleaseId: value.activeRuntimeReleaseId }
2851
+ : {}),
2852
+ ...(value.activePageReleaseId
2853
+ ? { activePageReleaseId: value.activePageReleaseId }
2854
+ : {}),
2855
+ ...(value.activeBackendReleaseId
2856
+ ? { activeBackendReleaseId: value.activeBackendReleaseId }
2857
+ : {}),
2858
+ release: compactRelease,
2859
+ resourceCount: resources.length,
2860
+ resourcesByKind,
2861
+ };
2862
+ }
2863
+
2599
2864
  async function runAppReleaseCommand(
2600
2865
  config,
2601
2866
  target,
@@ -2613,6 +2878,7 @@ async function runAppReleaseCommand(
2613
2878
  target.profileName,
2614
2879
  appReleaseApiPath(target, '/head')
2615
2880
  );
2881
+ if (!flags.full) data = summarizeAppReleaseHead(data);
2616
2882
  } else if (subcommand === 'app-list') {
2617
2883
  data = await requestWithAuth(
2618
2884
  config,
@@ -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` is 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` returns a compact head/resource-count summary 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.174",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {