openxiangda 1.0.164 → 1.0.166

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.
Files changed (33) hide show
  1. package/README.md +9 -7
  2. package/lib/change-baseline.js +74 -6
  3. package/lib/cli.js +2508 -167
  4. package/lib/sdd.js +320 -6
  5. package/lib/utils.js +76 -2
  6. package/openxiangda-skills/SKILL.md +13 -6
  7. package/openxiangda-skills/references/pages/publish-flow.md +15 -16
  8. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +24 -2
  9. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +14 -6
  10. package/openxiangda-skills/skills/openxiangda-form/SKILL.md +10 -9
  11. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +2 -0
  12. package/package.json +2 -1
  13. package/packages/sdk/dist/runtime/index.cjs +164 -48
  14. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  15. package/packages/sdk/dist/runtime/index.d.mts +1 -1
  16. package/packages/sdk/dist/runtime/index.d.ts +1 -1
  17. package/packages/sdk/dist/runtime/index.mjs +164 -48
  18. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  19. package/packages/sdk/dist/runtime/react.cjs +164 -48
  20. package/packages/sdk/dist/runtime/react.cjs.map +1 -1
  21. package/packages/sdk/dist/runtime/react.d.mts +79 -1
  22. package/packages/sdk/dist/runtime/react.d.ts +79 -1
  23. package/packages/sdk/dist/runtime/react.mjs +164 -48
  24. package/packages/sdk/dist/runtime/react.mjs.map +1 -1
  25. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +1 -0
  26. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +1 -0
  27. package/templates/openxiangda-react-spa/AGENTS.md +12 -10
  28. package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +20 -2
  29. package/templates/openxiangda-react-spa/scripts/deploy.mjs +2 -2
  30. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +2 -0
  31. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +1 -0
  32. package/templates/sy-lowcode-app-workspace/AGENTS.md +5 -3
  33. package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +20 -2
package/lib/sdd.js CHANGED
@@ -1223,6 +1223,12 @@ function getSddChangeScope(options = {}) {
1223
1223
  ...recordedChangedFiles,
1224
1224
  ...ownedFiles,
1225
1225
  ])).sort();
1226
+ const releaseCommands = Array.isArray(loaded.release?.commands)
1227
+ ? loaded.release.commands.filter(command => String(command || '').trim())
1228
+ : [];
1229
+ const plannedCommands = Array.isArray(loaded.meta?.release?.plannedCommands)
1230
+ ? loaded.meta.release.plannedCommands.filter(command => String(command || '').trim())
1231
+ : [];
1226
1232
  return {
1227
1233
  changeId: loaded.changeId,
1228
1234
  change: loaded.meta,
@@ -1232,7 +1238,9 @@ function getSddChangeScope(options = {}) {
1232
1238
  changedFiles,
1233
1239
  recordedChangedFiles,
1234
1240
  ownedFiles,
1235
- commands: Array.isArray(loaded.release?.commands) ? loaded.release.commands : [],
1241
+ commands: releaseCommands.length > 0 ? releaseCommands : plannedCommands,
1242
+ releaseCommands,
1243
+ plannedCommands,
1236
1244
  };
1237
1245
  }
1238
1246
 
@@ -1382,6 +1390,7 @@ function buildSddGovernanceReport(options = {}) {
1382
1390
  targets,
1383
1391
  changedFiles: files,
1384
1392
  commands: options.commands || [],
1393
+ actualCommands: options.actualCommands === true,
1385
1394
  },
1386
1395
  });
1387
1396
  if (!verify.passed) {
@@ -1439,15 +1448,20 @@ function getSddVerifyStagePolicy(stageValue) {
1439
1448
  if (stage === 'default') {
1440
1449
  return {
1441
1450
  stage,
1442
- taskStages: new Set(SDD_VERIFY_STAGES),
1443
- evidenceStages: new Set(SDD_VERIFY_STAGES),
1451
+ // An unspecified verify is a prepublish check. Postpublish/archive
1452
+ // evidence cannot exist yet and must never block ordinary development.
1453
+ taskStages: new Set(['implementation', 'prepublish']),
1454
+ evidenceStages: new Set(['prepublish']),
1444
1455
  requireRelease: true,
1445
1456
  };
1446
1457
  }
1447
1458
  const rank = SDD_VERIFY_STAGES.indexOf(stage);
1448
1459
  return {
1449
1460
  stage,
1450
- taskStages: new Set(SDD_VERIFY_STAGES.slice(0, rank + 1)),
1461
+ taskStages:
1462
+ stage === 'implementation'
1463
+ ? new Set()
1464
+ : new Set(SDD_VERIFY_STAGES.slice(0, rank + 1)),
1451
1465
  evidenceStages:
1452
1466
  stage === 'implementation'
1453
1467
  ? new Set()
@@ -1661,13 +1675,19 @@ function validateDeltaSpecs(changeDir) {
1661
1675
 
1662
1676
  function buildUpdatedReleaseManifest(loaded, releasePlan, verificationStage = 'default') {
1663
1677
  const verifiedAt = nowIso();
1678
+ const reviewedCommands = Array.isArray(loaded.release?.commands)
1679
+ ? loaded.release.commands.filter(command => String(command || '').trim())
1680
+ : [];
1664
1681
  return {
1665
1682
  schemaVersion: SDD_SCHEMA_VERSION,
1666
1683
  changeId: loaded.changeId,
1667
1684
  ...(loaded.release && !loaded.release.__invalid ? loaded.release : {}),
1668
1685
  targets: releasePlan?.targets || loaded.release?.targets || {},
1669
1686
  changedFiles: releasePlan?.changedFiles || loaded.release?.changedFiles || [],
1670
- commands: releasePlan?.commands || loaded.release?.commands || [],
1687
+ commands:
1688
+ reviewedCommands.length > 0
1689
+ ? reviewedCommands
1690
+ : releasePlan?.commands || [],
1671
1691
  verifiedAt,
1672
1692
  verificationStage,
1673
1693
  verificationStages: {
@@ -1769,9 +1789,270 @@ function validateReleaseManifest(release, releasePlan, legacyMode = false) {
1769
1789
  if (releasePlan?.commands?.length && (!Array.isArray(release.commands) || release.commands.length === 0)) {
1770
1790
  errors.push({ name: 'sdd-release-commands-empty', message: 'release.json 未记录发布命令' });
1771
1791
  }
1792
+ if (!legacyMode && Array.isArray(release.commands)) {
1793
+ errors.push(
1794
+ ...validateAtomicReleaseCommands(
1795
+ release.commands,
1796
+ targets,
1797
+ release.changeId
1798
+ )
1799
+ );
1800
+ }
1772
1801
  return { errors, warnings };
1773
1802
  }
1774
1803
 
1804
+ function commandFlagValue(command, name) {
1805
+ const match = String(command || '').match(
1806
+ new RegExp(`(?:^|\\s)--${escapeRegExp(name)}(?:=|\\s+)([^\\s]+)`)
1807
+ );
1808
+ return match ? match[1] : null;
1809
+ }
1810
+
1811
+ function normalizedCommandSelectors(command) {
1812
+ return Array.from(
1813
+ new Set(
1814
+ String(commandFlagValue(command, 'only') || commandFlagValue(command, 'code') || '')
1815
+ .split(',')
1816
+ .map(value => value.trim())
1817
+ .filter(Boolean)
1818
+ )
1819
+ ).sort();
1820
+ }
1821
+
1822
+ function normalizedResourcePublishTypes(command) {
1823
+ const match = String(command || '').match(
1824
+ /^openxiangda\s+resource\s+publish(?:\s+([^\s-][^\s]*))?/
1825
+ );
1826
+ if (!match?.[1]) return [];
1827
+ const aliases = {
1828
+ functions: 'function',
1829
+ automations: 'automation',
1830
+ workflows: 'workflow',
1831
+ 'form-settings': 'form-setting',
1832
+ };
1833
+ return Array.from(
1834
+ new Set(
1835
+ match[1]
1836
+ .split(',')
1837
+ .map(value => value.trim().toLowerCase())
1838
+ .filter(Boolean)
1839
+ .map(value => aliases[value] || value)
1840
+ )
1841
+ ).sort();
1842
+ }
1843
+
1844
+ function sameStringSet(left, right) {
1845
+ const normalizedLeft = Array.from(new Set(left || [])).sort();
1846
+ const normalizedRight = Array.from(new Set(right || [])).sort();
1847
+ return (
1848
+ normalizedLeft.length === normalizedRight.length &&
1849
+ normalizedLeft.every((value, index) => value === normalizedRight[index])
1850
+ );
1851
+ }
1852
+
1853
+ function validateAtomicReleaseCommands(
1854
+ commands,
1855
+ targets,
1856
+ changeId,
1857
+ options = {}
1858
+ ) {
1859
+ const errors = [];
1860
+ const requireCompletePlan = options.requireCompletePlan !== false;
1861
+ const normalized = commands.map(command => String(command || '').trim());
1862
+ const publishCommands = normalized.filter(command =>
1863
+ /^openxiangda\s+(?:workspace\s+publish|resource\s+publish|runtime\s+deploy|release\s+app-finalize)\b/.test(
1864
+ command
1865
+ )
1866
+ );
1867
+ for (const command of publishCommands) {
1868
+ if (!/\s--profile(?:=|\s+)\S+/.test(command) || /<profile>/.test(command)) {
1869
+ errors.push({
1870
+ name: 'sdd-release-profile-missing',
1871
+ message: `发布命令必须显式指定真实 --profile: ${command}`,
1872
+ });
1873
+ }
1874
+ if (
1875
+ changeId &&
1876
+ !new RegExp(`\\s--change(?:=|\\s+)${escapeRegExp(changeId)}(?:\\s|$)`).test(
1877
+ command
1878
+ )
1879
+ ) {
1880
+ errors.push({
1881
+ name: 'sdd-release-change-missing',
1882
+ message: `发布命令必须绑定 --change ${changeId}: ${command}`,
1883
+ });
1884
+ }
1885
+ if (/^openxiangda\s+workspace\s+publish\b/.test(command) && /\s--form(?:=|\s+)/.test(command)) {
1886
+ errors.push({
1887
+ name: 'sdd-release-direct-form-schema-forbidden',
1888
+ message: `React SPA Form 必须走 staged form-setting bundle,禁止 workspace publish --form: ${command}`,
1889
+ });
1890
+ }
1891
+ if (
1892
+ /^openxiangda\s+resource\s+publish\s+form-setting\b/.test(command) &&
1893
+ /\s--activate(?:\s|$)/.test(command)
1894
+ ) {
1895
+ errors.push({
1896
+ name: 'sdd-release-direct-form-activation-forbidden',
1897
+ message: `Form bundle 必须保持 staged,禁止在 Root App finalize 前 --activate: ${command}`,
1898
+ });
1899
+ }
1900
+ if (
1901
+ /^openxiangda\s+resource\s+publish\b/.test(command) &&
1902
+ !/\s--(?:only|code)(?:=|\s+)/.test(command)
1903
+ ) {
1904
+ errors.push({
1905
+ name: 'sdd-release-resource-scope-missing',
1906
+ message: `resource publish 必须使用 --only/--code 精确范围: ${command}`,
1907
+ });
1908
+ }
1909
+ if (
1910
+ /^openxiangda\s+runtime\s+deploy\b/.test(command) &&
1911
+ !/\s--no-activate(?:\s|$)/.test(command)
1912
+ ) {
1913
+ errors.push({
1914
+ name: 'sdd-release-runtime-direct-activation-forbidden',
1915
+ message: `Runtime 必须先 --no-activate 暂存,再由 App Release 原子激活: ${command}`,
1916
+ });
1917
+ }
1918
+ const commandTypes = normalizedResourcePublishTypes(command);
1919
+ const expectedBackendTypes = [
1920
+ ...((targets.functions || []).length > 0 ? ['function'] : []),
1921
+ ...((targets.automations || []).length > 0 ? ['automation'] : []),
1922
+ ].sort();
1923
+ if (
1924
+ commandTypes.some(type => ['function', 'automation'].includes(type)) &&
1925
+ expectedBackendTypes.length > 0
1926
+ ) {
1927
+ const qualified = expectedBackendTypes.length > 1;
1928
+ const expectedSelectors = [
1929
+ ...(targets.functions || []).map(code =>
1930
+ qualified ? `function:${code}` : String(code)
1931
+ ),
1932
+ ...(targets.automations || []).map(code =>
1933
+ qualified ? `automation:${code}` : String(code)
1934
+ ),
1935
+ ].sort();
1936
+ if (!/\s--stage-only(?:\s|$)/.test(command)) {
1937
+ errors.push({
1938
+ name: 'sdd-release-backend-direct-activation-forbidden',
1939
+ message: `Function/Automation 必须使用 --stage-only: ${command}`,
1940
+ });
1941
+ }
1942
+ if (
1943
+ !sameStringSet(commandTypes, expectedBackendTypes) ||
1944
+ !sameStringSet(normalizedCommandSelectors(command), expectedSelectors)
1945
+ ) {
1946
+ errors.push({
1947
+ name: 'sdd-release-backend-scope-inexact',
1948
+ message: `Function/Automation 命令必须精确覆盖审核 selector,不能缺失或夹带资源: ${command}`,
1949
+ });
1950
+ }
1951
+ }
1952
+ if (
1953
+ commandTypes.includes('form-setting') &&
1954
+ (targets.forms || []).length > 0 &&
1955
+ !sameStringSet(
1956
+ normalizedCommandSelectors(command),
1957
+ (targets.forms || []).map(String)
1958
+ )
1959
+ ) {
1960
+ errors.push({
1961
+ name: 'sdd-release-form-scope-inexact',
1962
+ message: `Form bundle 命令必须精确覆盖审核 selector,不能缺失或夹带表单: ${command}`,
1963
+ });
1964
+ }
1965
+ }
1966
+
1967
+ if (!requireCompletePlan) return errors;
1968
+
1969
+ const backendCodes = [
1970
+ ...(targets.functions || []),
1971
+ ...(targets.automations || []),
1972
+ ];
1973
+ if (backendCodes.length > 0) {
1974
+ const backendStage = normalized.find(
1975
+ command =>
1976
+ /^openxiangda\s+resource\s+publish\b/.test(command) &&
1977
+ /\s--stage-only(?:\s|$)/.test(command) &&
1978
+ sameStringSet(normalizedResourcePublishTypes(command), [
1979
+ ...((targets.functions || []).length > 0 ? ['function'] : []),
1980
+ ...((targets.automations || []).length > 0 ? ['automation'] : []),
1981
+ ]) &&
1982
+ sameStringSet(normalizedCommandSelectors(command), [
1983
+ ...(targets.functions || []).map(code =>
1984
+ (targets.automations || []).length > 0
1985
+ ? `function:${code}`
1986
+ : String(code)
1987
+ ),
1988
+ ...(targets.automations || []).map(code =>
1989
+ (targets.functions || []).length > 0
1990
+ ? `automation:${code}`
1991
+ : String(code)
1992
+ ),
1993
+ ])
1994
+ );
1995
+ if (!backendStage) {
1996
+ errors.push({
1997
+ name: 'sdd-release-backend-stage-missing',
1998
+ message: 'Function/Automation 必须由一条精确 --only 的 --stage-only Backend Release 命令覆盖。',
1999
+ });
2000
+ }
2001
+ }
2002
+ if ((targets.forms || []).length > 0) {
2003
+ const formStage = normalized.find(
2004
+ command =>
2005
+ /^openxiangda\s+resource\s+publish\s+form-setting\b/.test(command) &&
2006
+ !/\s--activate(?:\s|$)/.test(command) &&
2007
+ sameStringSet(
2008
+ normalizedCommandSelectors(command),
2009
+ (targets.forms || []).map(String)
2010
+ )
2011
+ );
2012
+ if (!formStage) {
2013
+ errors.push({
2014
+ name: 'sdd-release-form-stage-missing',
2015
+ message: 'Form 必须由精确 form-setting bundle 命令暂存为 FormRelease。',
2016
+ });
2017
+ }
2018
+ }
2019
+ if (targets.runtime) {
2020
+ const runtimeStage = normalized.find(
2021
+ command =>
2022
+ /^openxiangda\s+runtime\s+deploy\b/.test(command) &&
2023
+ /\s--no-activate(?:\s|$)/.test(command)
2024
+ );
2025
+ if (!runtimeStage) {
2026
+ errors.push({
2027
+ name: 'sdd-release-runtime-stage-missing',
2028
+ message: 'Runtime 目标缺少 runtime deploy --no-activate 暂存命令。',
2029
+ });
2030
+ }
2031
+ }
2032
+ if (
2033
+ backendCodes.length > 0 ||
2034
+ (targets.forms || []).length > 0 ||
2035
+ targets.runtime
2036
+ ) {
2037
+ const finalize = normalized.find(
2038
+ command =>
2039
+ /^openxiangda\s+release\s+app-finalize\b/.test(command) &&
2040
+ /\s--staged-resources-json(?:=|\s+)\S+/.test(command)
2041
+ );
2042
+ if (!finalize) {
2043
+ errors.push({
2044
+ name: 'sdd-release-app-finalize-missing',
2045
+ message: '暂存的 Form/Backend/Runtime 必须由 app-finalize 一次原子激活。',
2046
+ });
2047
+ }
2048
+ }
2049
+ return errors;
2050
+ }
2051
+
2052
+ function escapeRegExp(value) {
2053
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2054
+ }
2055
+
1775
2056
  function verifySddChange(options = {}) {
1776
2057
  const loaded = loadSddChange(options);
1777
2058
  const stagePolicy = getSddVerifyStagePolicy(options.stage || options.phase);
@@ -1833,6 +2114,35 @@ function verifySddChange(options = {}) {
1833
2114
  const releaseValidation = validateReleaseManifest(release, options.releasePlan, legacyMode);
1834
2115
  errors.push(...releaseValidation.errors);
1835
2116
  warnings.push(...releaseValidation.warnings);
2117
+ const reviewedPlannedCommands = loaded.meta?.release?.plannedCommands;
2118
+ if (
2119
+ !legacyMode &&
2120
+ Array.isArray(reviewedPlannedCommands) &&
2121
+ reviewedPlannedCommands.length > 0
2122
+ ) {
2123
+ errors.push(
2124
+ ...validateAtomicReleaseCommands(
2125
+ reviewedPlannedCommands,
2126
+ targets,
2127
+ loaded.changeId
2128
+ )
2129
+ );
2130
+ }
2131
+ }
2132
+ if (
2133
+ !legacyMode &&
2134
+ options.releasePlan?.actualCommands === true &&
2135
+ Array.isArray(options.releasePlan.commands) &&
2136
+ options.releasePlan.commands.length > 0
2137
+ ) {
2138
+ errors.push(
2139
+ ...validateAtomicReleaseCommands(
2140
+ options.releasePlan.commands,
2141
+ targets,
2142
+ loaded.changeId,
2143
+ { requireCompletePlan: false }
2144
+ )
2145
+ );
1836
2146
  }
1837
2147
  const passed = errors.length === 0;
1838
2148
  const verifiedAt = nowIso();
@@ -1840,7 +2150,11 @@ function verifySddChange(options = {}) {
1840
2150
  ...loaded.meta,
1841
2151
  release: {
1842
2152
  ...(loaded.meta.release || {}),
1843
- plannedCommands: options.releasePlan?.commands || loaded.meta.release?.plannedCommands || release?.commands || [],
2153
+ plannedCommands:
2154
+ Array.isArray(loaded.meta.release?.plannedCommands) &&
2155
+ loaded.meta.release.plannedCommands.length > 0
2156
+ ? loaded.meta.release.plannedCommands
2157
+ : release?.commands || options.releasePlan?.commands || [],
1844
2158
  lastVerifiedAt: verifiedAt,
1845
2159
  },
1846
2160
  verification: {
package/lib/utils.js CHANGED
@@ -1,14 +1,86 @@
1
1
  const { spawn } = require('child_process');
2
2
  const os = require('os');
3
3
 
4
+ const sensitiveTextValues = new Set();
5
+
6
+ function buildSensitiveTextVariants(value) {
7
+ const normalized = String(value);
8
+ const bytes = Buffer.from(normalized, 'utf8');
9
+ const base64 = bytes.toString('base64');
10
+ const variants = [
11
+ normalized,
12
+ JSON.stringify(normalized).slice(1, -1),
13
+ encodeURIComponent(normalized),
14
+ base64,
15
+ base64.replace(/=+$/g, ''),
16
+ bytes.toString('base64url'),
17
+ bytes.toString('hex'),
18
+ ].filter(Boolean);
19
+ return Array.from(
20
+ new Set([
21
+ ...variants,
22
+ ...variants.map(variant => JSON.stringify(variant).slice(1, -1)),
23
+ ])
24
+ );
25
+ }
26
+
27
+ function registerSensitiveText(value) {
28
+ if (value === undefined || value === null) return;
29
+ const normalized = String(value);
30
+ if (!normalized) return;
31
+ for (const variant of buildSensitiveTextVariants(normalized)) {
32
+ sensitiveTextValues.add(variant);
33
+ }
34
+ }
35
+
36
+ function clearSensitiveText() {
37
+ sensitiveTextValues.clear();
38
+ }
39
+
4
40
  function maskText(value) {
5
- return String(value || '')
41
+ let masked = String(value || '')
6
42
  .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, '***token***')
7
43
  .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, match => {
8
44
  const [name, domain] = match.split('@');
9
45
  return `${name.slice(0, 2)}***@${domain}`;
10
46
  })
11
47
  .replace(/\b1[3-9]\d{9}\b/g, match => `${match.slice(0, 3)}****${match.slice(-4)}`);
48
+ for (const sensitive of Array.from(sensitiveTextValues).sort(
49
+ (left, right) => right.length - left.length
50
+ )) {
51
+ masked = masked.split(sensitive).join('***secret***');
52
+ }
53
+ return masked;
54
+ }
55
+
56
+ function maskSensitiveTextOnly(value) {
57
+ let masked = String(value || '');
58
+ for (const sensitive of Array.from(sensitiveTextValues).sort(
59
+ (left, right) => right.length - left.length
60
+ )) {
61
+ masked = masked.split(sensitive).join('***secret***');
62
+ }
63
+ return masked;
64
+ }
65
+
66
+ function maskSensitiveJsonValue(value, seen = new WeakSet()) {
67
+ if (typeof value === 'string') return maskSensitiveTextOnly(value);
68
+ if (!value || typeof value !== 'object') return value;
69
+ if (typeof value.toJSON === 'function') {
70
+ return maskSensitiveJsonValue(value.toJSON(), seen);
71
+ }
72
+ if (seen.has(value)) return '[Circular]';
73
+ seen.add(value);
74
+ const masked = Array.isArray(value)
75
+ ? value.map(item => maskSensitiveJsonValue(item, seen))
76
+ : Object.fromEntries(
77
+ Object.entries(value).map(([key, child]) => [
78
+ maskSensitiveTextOnly(key),
79
+ maskSensitiveJsonValue(child, seen),
80
+ ])
81
+ );
82
+ seen.delete(value);
83
+ return masked;
12
84
  }
13
85
 
14
86
  function formatFetchError(error, context) {
@@ -88,16 +160,18 @@ function sleep(ms) {
88
160
  }
89
161
 
90
162
  function writeJson(value) {
91
- console.log(JSON.stringify(value, null, 2));
163
+ console.log(JSON.stringify(maskSensitiveJsonValue(value), null, 2));
92
164
  }
93
165
 
94
166
  module.exports = {
95
167
  fail,
168
+ clearSensitiveText,
96
169
  formatFetchError,
97
170
  maskText,
98
171
  openBrowser,
99
172
  parseArgs,
100
173
  print,
174
+ registerSensitiveText,
101
175
  sleep,
102
176
  warn,
103
177
  writeJson,
@@ -49,7 +49,7 @@ openxiangda sdd context --changed --change <change> --json
49
49
  openxiangda sdd propose <change>
50
50
  # record the user's approval, then:
51
51
  openxiangda sdd approve <change>
52
- openxiangda sdd verify <change> --changed --stage implementation
52
+ openxiangda sdd verify <change> --changed --stage implementation --profile <name>
53
53
  ```
54
54
 
55
55
  For an eligible L1 edit, create the exact approved record directly; the user's concrete request is the approval, so do not add a redundant proposal/approval loop:
@@ -60,6 +60,10 @@ 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
+ Keep development lightweight: `sdd verify --stage implementation` checks scope/contracts but does not block on unfinished task/evidence prose. Plain `sdd verify` means prepublish, not all future stages. Do not manufacture postpublish/archive evidence during coding; the strict gates belong at stage/finalize.
64
+
65
+ `sdd verify/sync` preserves non-empty, reviewed `release.json.commands` and `change.json.release.plannedCommands` independently; it must not regenerate either into broader or directly activating commands. The prepublish gate also validates the actual argv, so a safe recorded plan cannot authorize a direct Backend/Form/Runtime activation. Generated React SPA plans use exact Form bundles, one mixed Backend `--stage-only` command, Runtime `--no-activate`, and Root `app-finalize`, with explicit `--profile` and `--change` on every publish command. Each staged command automatically merges its canonical child entry into `.openxiangda/releases/<change>/staged-resources.json`.
66
+
63
67
  ## Safe release recipes
64
68
 
65
69
  Classic workspace, exact page/form:
@@ -77,10 +81,11 @@ React SPA resources/runtime:
77
81
  openxiangda resource validate <type> --only <codes> --profile <name>
78
82
  openxiangda resource plan <type> --only <codes> --profile <name> --json
79
83
  openxiangda runtime deploy --change <change> --profile <name> --no-activate --json
80
- openxiangda sdd verify <change> --changed --stage prepublish
84
+ openxiangda sdd verify <change> --changed --stage prepublish --profile <name>
81
85
  openxiangda release begin --change <change> --profile <name>
82
- openxiangda resource publish <type> --only <codes> --change <change> --profile <name>
83
- openxiangda runtime activate <reviewedReleaseId> --change <change> --profile <name>
86
+ openxiangda resource publish function,automation --only <qualified-codes> --stage-only --change <change> --profile <name>
87
+ openxiangda resource publish form-setting --only <form-codes> --change <change> --profile <name>
88
+ openxiangda release app-finalize --change <change> --staged-resources-json <JSON|file> --profile <name>
84
89
  # Merge/fast-forward the frozen release commit into the remote default branch.
85
90
  openxiangda release integration-status --profile <name>
86
91
  openxiangda release end --profile <name>
@@ -94,9 +99,11 @@ After promotion, immediately merge or fast-forward the exact frozen publish comm
94
99
 
95
100
  `runtime deploy` separately freezes the clean committed `HEAD` as `sourceRevision` together with the current active Runtime parent before any build or upload. Any deploy whose source does not descend from the online Runtime fails with `RUNTIME_SOURCE_BASE_DIVERGED`, including `--no-activate`; this prevents staging an old preview for later activation. `--no-build` does not bypass this guard. Only an intentional audited rollback may use `--allow-runtime-rollback --reason "<at least 8 characters>"`, and that flag never permits dirty or non-Git input. SDD evidence under `openspec/` and generated/state paths do not make Runtime source dirty.
96
101
 
97
- For a whole-app release, keep changed Runtime/Page/Backend/Form children staged and run `openxiangda release app-finalize --change <change> --staged-resources-json <JSON|file> --profile <name>`. The JSON contains only changed immutable child entries; the CLI overlays Runtime/Page/Backend by singleton kind and FormRelease by `formUuid` onto one authoritative read-only capture, preserving every unmodified active child. It then performs `prepare -> verify -> activateStagedChildren=true`, switching child heads and the App head atomically (`atomic_staged_children_v1`). Never refresh or retry after a conflict. The flow requires the owned stored lease/change baseline and carries the same client session and Git lineage on every write. Omitting the overlay is compatibility-only retrospective aggregation of already-active children.
102
+ For a whole-app release, keep changed Runtime/Page/Backend/Form children staged and run `openxiangda release app-finalize --change <change> --staged-resources-json <JSON|file> --profile <name>`. The JSON contains only changed immutable child entries; the CLI overlays Runtime/Page/Backend by singleton kind and FormRelease by `formUuid` onto one authoritative read-only capture, preserving every unmodified active child. It then performs `prepare -> verify -> activateStagedChildren=true`, switching child heads and the App head atomically (`atomic_staged_children_v1`, or `atomic_staged_children_v2` when a Backend Release v2 child is present). Never refresh or retry after a conflict. The flow requires the owned stored lease/change baseline and carries the same client session and Git lineage on every write. Omitting the overlay is compatibility-only retrospective aggregation of already-active children.
103
+
104
+ When a Function or Automation is selected because its TypeScript source changed, publishing is source-only by default. Backend Release v2 accepts per-resource create, source-only update, and manifest replacement update in one immutable child and one database transaction, so a stale member produces zero resource writes and noops do not advance versions/timestamps. `--stage-only` is fail-closed: every selected mutation must enter that child, and a missing/incompatible Backend Release API never falls back to direct writes. Online bindings, input/output contracts, metadata, trigger/view configuration, and enabled/published state remain unchanged unless exact `--replace-manifest --reason "..."` authority was provided.
98
105
 
99
- When a Function or Automation is selected because its TypeScript source changed, publishing is source-only by default. Eligible existing Function/Automation updates in the same command use one immutable Backend Release (`prepare -> verify -> activate`) and one database transaction, so a stale member produces zero resource writes and noops do not advance versions/timestamps. Online bindings, input/output contracts, metadata, trigger/view configuration, and enabled/published state remain unchanged. Only an explicit Backend Release `head` HTTP 404 permits the warned compatibility source PATCH. A complete manifest replacement is exceptional and requires exact `--only/--code` plus `--replace-manifest --reason "..."`.
106
+ An App Function may declare metadata-only top-level `secretRefs: [{ name, required }]` only with `function_v2` + `trusted_node_v2`; source resolves values with `await ctx.secrets.get(name)` and uses `ctx.utils.http` for controlled public HTTPS. Create/rotate values through hidden TTY or `openxiangda secret ... --value-stdin --change <change> --profile <name>`. Never put values in arguments, files, manifests, state, plans, logs, errors, or chat. Secret bindings require `backend_release_v2` and whole-app `atomic_staged_children_v2`; a missing capability is fail-closed and never uses the legacy source PATCH. For whole-app activation use exact-scope `resource publish <type> --only <code> --stage-only`, then pass the returned verified `stagedResource` to `release app-finalize`; an active Backend Release is never labeled staged.
100
107
 
101
108
  The lease is app-level promotion ownership, while worktree ownership prevents two Codex tasks from editing/publishing through the same source directory. Different worktrees may keep developing and validating. After promotion, merge/push the frozen SHA and run normal `release end`; an expired lease still retains reconciliation evidence. Use full resource/runtime publish only when the approved dependency closure is app-wide.
102
109
 
@@ -1,9 +1,9 @@
1
1
  # Code Page Publish Flow
2
2
 
3
- Preferred publish command:
3
+ Preferred React SPA release entry:
4
4
 
5
5
  ```bash
6
- openxiangda workspace publish --profile <name>
6
+ openxiangda workspace plan --change <change> --changed --profile <name>
7
7
  ```
8
8
 
9
9
  Do not run `lowcode-workspace publish-all`, `pnpm publish:all`, or legacy
@@ -11,7 +11,8 @@ project scripts such as `scripts/openxiangda-publish.mjs` directly for live
11
11
  publishing. Those commands are workspace internals. They may skip OpenXiangda's
12
12
  normal-user token/profile injection and fall back to old `APP_KEY` /
13
13
  `APP_SECRET` behavior, or publish with a buildId that the platform menu does not
14
- activate. Use `openxiangda workspace publish ...` as the external entry.
14
+ activate. Use the staged Form/Backend/Runtime commands emitted by the workspace
15
+ plan, then one Root App finalize.
15
16
 
16
17
  For day-to-day AI edits, prefer a targeted publish:
17
18
 
@@ -23,15 +24,12 @@ openxiangda workspace check --changed
23
24
  # Preview what git-touched forms/pages would publish.
24
25
  openxiangda workspace publish --profile <name> --changed --dry-run
25
26
 
26
- # Publish only git-touched src/forms/* and src/pages/* modules.
27
- openxiangda workspace publish --profile <name> --changed
27
+ # Stage one exact Form bundle; React page changes are staged in Runtime.
28
+ openxiangda resource publish form-setting --only customer --change <change> --profile <name>
29
+ openxiangda runtime deploy --no-activate --change <change> --profile <name>
28
30
 
29
- # Publish one code page or one form when the changed module is known.
30
- openxiangda workspace publish --profile <name> --page dashboard
31
- openxiangda workspace publish --profile <name> --form customer
32
-
33
- # Publish several explicit modules.
34
- openxiangda workspace publish --profile <name> --only pages/dashboard,forms/customer
31
+ # Atomically activate every staged child and the App head.
32
+ openxiangda release app-finalize --staged-resources-json .openxiangda/releases/<change>/staged-resources.json --change <change> --profile <name>
35
33
  ```
36
34
 
37
35
  Do not use full workspace publish as a reflex. Full publish is only appropriate
@@ -43,13 +41,14 @@ Targeted publish skips `src/resources` by default. Add `--resources` if resource
43
41
  manifests changed too, or run `openxiangda resource plan|publish` separately.
44
42
  Use `--skip-resources` when you explicitly want forms/pages only.
45
43
 
46
- React SPA workspaces use a fixed release order:
44
+ React SPA workspaces use a fixed staged release order:
47
45
 
48
46
  ```bash
49
- openxiangda workspace plan --changed --json
50
- openxiangda workspace publish --profile <name> --form <formCode>
51
- openxiangda resource publish --profile <name>
52
- openxiangda runtime deploy --profile <name>
47
+ openxiangda workspace plan --change <change> --changed --profile <name> --json
48
+ openxiangda resource publish form-setting --only <formCode> --change <change> --profile <name>
49
+ openxiangda resource publish function,automation --only function:<code>,automation:<code> --stage-only --change <change> --profile <name>
50
+ openxiangda runtime deploy --no-activate --change <change> --profile <name>
51
+ openxiangda release app-finalize --staged-resources-json .openxiangda/releases/<change>/staged-resources.json --change <change> --profile <name>
53
52
  ```
54
53
 
55
54
  If `workspace check --changed` reports risky AI fallback code, missing schema,