openxiangda 1.0.165 → 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.
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: {
@@ -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>
@@ -96,7 +101,7 @@ After promotion, immediately merge or fast-forward the exact frozen publish comm
96
101
 
97
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.
98
103
 
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 "..."`.
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.
100
105
 
101
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.
102
107
 
@@ -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,
@@ -26,6 +26,8 @@ Run `openxiangda update check --json` once per substantial task, on a suspected
26
26
 
27
27
  Every write/release command must include `--profile <name>`. In a shared checkout, create an isolated Git worktree and named feature branch before editing. `sdd propose|quick|context` claims `.openxiangda/worktree-owner.json` for the current `CODEX_THREAD_ID`; a foreign owner is a hard stop. `.openxiangda/state.json` maps profile-local IDs; it is not a collaboration lock. The task is not complete until the exact frozen publish SHA is merged without squash/rebase and pushed to the authoritative default branch.
28
28
 
29
+ SDD is permissive during coding and strict at release: `sdd verify --stage implementation` does not require unfinished checklist/evidence prose, and an omitted stage means prepublish rather than postpublish/archive. Exact command, child scope, and mainline gates apply only when staging or activating writes.
30
+
29
31
  First-class direct configuration writes (form/page/menu/role/permission/workflow/connector/notification/auth/route/public-access/scope/data-view/storage/settings) also require `--change <id>` or a complete stored session created by `openxiangda release begin --change <id>`. Missing context fails locally with `PUBLISH_CONTEXT_REQUIRED` before any HTTP write. Dry-run, schema planning, validation, query/invoke/export/submit/execution/upload, and other data-plane actions do not acquire a lease. Direct Workflow publish/unpublish/delete additionally requires the frozen `--expected-revision`; never fetch the latest revision merely to replay an older payload.
30
32
 
31
33
  ## Exact planning
@@ -55,11 +57,13 @@ openxiangda resource publish function --only function_a,function_b --profile <na
55
57
 
56
58
  Selectors apply before manifest parsing, source dependency analysis, and JS_CODE typecheck/build. A scoped Function/Automation plan must touch only the selected targets plus their transitive/shared/ambient dependencies; an unscoped plan intentionally retains full-workspace behavior. Resource commands use the canonical scoped builder packaged with the installed CLI for standard workspaces, so old checked-in builders still receive the optimization; refresh/bootstrap the workspace script only for equivalent manual `pnpm build-js-code` behavior. Nonstandard custom builders remain an explicit compatibility fallback.
57
59
 
58
- Source-triggered Function/Automation targets use one immutable Backend Release by default. The CLI uploads every eligible existing source-only target, freezes the current Backend Release parent plus Git/change baseline, then runs `prepare -> verify -> activate`; activation first CAS-checks the entire set and applies all updates in one transaction. Never fall back to sequential PATCH after prepare/verify/activate has started. Compatibility fallback is allowed only when the Backend Release `head` feature probe is an explicit HTTP 404, and must warn. Existing online bindings, contracts, metadata, trigger/view configuration, and enabled/published state remain unchanged; noops do not advance resource versions/timestamps. A deliberate whole-definition replacement requires exact `--only/--code` and `--replace-manifest --reason "<why>"`; SDD bypass does not imply replacement authority.
60
+ Source-triggered Function/Automation targets use Backend Release v2 when the platform exposes that capability. One child may mix create, source-only update, and manifest replacement update through explicit per-resource `operation/mode`; the CLI freezes the current Backend Release parent plus Git/change baseline, then runs `prepare -> verify -> activate` or stops verified for `--stage-only`. Activation CAS-checks the entire set and applies all updates in one transaction. `--stage-only` and Secret-bound Function publishing fail closed when Backend Release v2 is unavailable; compatibility fallback is limited to non-staged, non-Secret publishing after an explicit Backend head 404. Existing online bindings, contracts, metadata, trigger/view configuration, and enabled/published state remain unchanged; noops do not advance versions/timestamps. A deliberate whole-definition replacement requires exact `--only/--code` and `--replace-manifest --reason "<why>"`; SDD bypass does not imply replacement authority.
59
61
 
60
62
  Functions with a top-level `secretRefs` field use `backend_release_v2`, including an explicit empty list that removes bindings. This path never falls back to source PATCH. It requires the per-app Secret capability probe to grant `app_function_secrets_v1`, `trusted_node_v2`, `backend_release_v2`, and `atomic_staged_children_v2`; otherwise plan/publish fails closed.
61
63
 
62
- For whole-app atomic activation, publish the exact Function/Automation scope with `resource publish <type> --only <code> --stage-only`. This stops after Backend Release verify and returns a real `stagedResource`; combine it with every other changed staged child in `release app-finalize --staged-resources-json`. A Backend Release that was already activated returns `activeResource` and must not be presented as staged.
64
+ For whole-app atomic activation, publish the exact mixed Function/Automation scope with `resource publish function,automation --only function:<code>,automation:<code> --stage-only --change <change>`. This stops after Backend Release verify and returns `stagedResource` plus every handled selector; the CLI automatically merges it with other changed staged children in `.openxiangda/releases/<change>/staged-resources.json` for `release app-finalize --staged-resources-json`. A Backend Release that was already activated returns `activeResource` and must not be presented as staged.
65
+
66
+ Standard workspaces use the CLI-bundled scoped JS_CODE builder directly, including isolated Git-base checks; do not invoke `pnpm` merely to rebuild a selected Function/Automation. New snapshots carry `source_lineage_v1.sourceHash`, which is derived from authored source/dependencies rather than generated bundle bytes, so a builder upgrade does not falsely report source divergence.
63
67
 
64
68
  Use `openxiangda release backend-head|backend-list|backend-detail|backend-diff` to inspect immutable history. `backend-rollback <releaseId> --change <change> --reason "..."` prepares, verifies, and activates a new release from the historical snapshot; it never mutates or directly reactivates the old row. `backend-abort` stops prepared/verified work and `backend-retry` retries only recorded post-commit side effects. Do not fetch newer revisions to replay an older payload.
65
69
 
@@ -80,16 +84,16 @@ Do not call `lowcode-workspace publish-all`, `pnpm publish:all`, `pnpm publish:o
80
84
 
81
85
  ## React SPA
82
86
 
83
- Forms still publish schema shells explicitly. Resources and frontend runtime are separate releases:
87
+ Forms, Backend resources, and frontend Runtime are staged children of one Root App release:
84
88
 
85
89
  ```bash
86
- openxiangda workspace publish --profile <name> --form <formCode>
90
+ openxiangda resource publish form-setting --only <formCode> --change <change> --profile <name>
87
91
  openxiangda resource plan <type> --only <codes> --profile <name>
88
92
  openxiangda runtime deploy --change <change> --profile <name> --no-activate --json
89
- openxiangda sdd verify <change> --changed --stage prepublish
93
+ openxiangda sdd verify <change> --changed --stage prepublish --profile <name>
90
94
  openxiangda release begin --change <change> --profile <name>
91
- openxiangda resource publish <type> --only <codes> --change <change> --profile <name>
92
- openxiangda runtime activate <reviewedReleaseId> --change <change> --profile <name>
95
+ openxiangda resource publish function,automation --only function:<code>,automation:<code> --stage-only --change <change> --profile <name>
96
+ openxiangda release app-finalize --staged-resources-json .openxiangda/releases/<change>/staged-resources.json --change <change> --profile <name>
93
97
  # Merge/fast-forward the frozen release SHA to the remote default branch.
94
98
  openxiangda release integration-status --profile <name>
95
99
  openxiangda release end --profile <name>
@@ -23,9 +23,10 @@ openxiangda form list --profile <name>
23
23
  # src/forms/<formCode>/schema.ts — fields, options, rules, top-level FormEffect[]
24
24
  # src/forms/<formCode>/page.tsx — presentation only
25
25
 
26
- # 3. preview + publish single form
27
- openxiangda workspace publish --profile <name> --form <formCode> --dry-run
28
- openxiangda workspace publish --profile <name> --form <formCode>
26
+ # 3. normal release: stage the exact Form bundle, then Root App finalize
27
+ openxiangda resource plan form-setting --only <formCode> --profile <name>
28
+ openxiangda resource publish form-setting --only <formCode> --change <change> --profile <name>
29
+ openxiangda release app-finalize --staged-resources-json .openxiangda/releases/<change>/staged-resources.json --change <change> --profile <name>
29
30
  ```
30
31
 
31
32
  ## DO / DO NOT
@@ -71,19 +72,19 @@ src/forms/<formCode>/
71
72
  └── page.tsx
72
73
  ```
73
74
 
74
- Then publish:
75
+ Then stage the exact Form bundle:
75
76
 
76
77
  ```bash
77
- openxiangda workspace publish --profile <name>
78
+ openxiangda resource publish form-setting --only <formCodes> --change <change> --profile <name>
78
79
  ```
79
80
 
80
- For a single form edit, publish that form only:
81
+ For a single form edit, still keep the child staged until Root App finalize:
81
82
 
82
83
  ```bash
83
- openxiangda workspace publish --profile <name> --form <formCode>
84
+ openxiangda resource publish form-setting --only <formCode> --change <change> --profile <name>
84
85
  ```
85
86
 
86
- The workspace publish script receives `OPENXIANGDA_BASE_URL`, `OPENXIANGDA_ACCESS_TOKEN`, and `OPENXIANGDA_APP_TYPE` from the CLI. It creates/binds the platform form shell only when needed, syncs form metadata, builds the React form page bundle, uploads assets, and registers the bundle.
87
+ The Form resource bundle carries schema, settings, indexes, data-management, runtime-write, and public-access configuration under one CAS parent. It returns a canonical staged FormRelease and the CLI records it in the change-scoped staged-resources file. `workspace publish --form` is limited to an intentional first-time bootstrap or isolated repair outside a governed multi-resource release; it is not the normal release path.
87
88
 
88
89
  For Phase 6 React SPA workspaces, `app-workspace.config.ts` should declare
89
90
  `runtimeMode: "react-spa"`. In that mode, `workspace publish --form <code>` is
@@ -109,7 +110,7 @@ Do not use `openxiangda form create` as the normal way to generate a user-facing
109
110
  ## Form Release / CAS Safety
110
111
 
111
112
  - Treat all Form configuration as one immutable release stream. Freeze `revision`, `etag`, and `activeFormReleaseHead` from one snapshot before writing.
112
- - Publish declared schema, packages, settings, indexes, data-management, runtime-write, and public-access configuration through one Form `bundle` mutation. Resource Form bundles are stage-only by default. For normal multi-resource work, add each changed `FormRelease` entry (stable identity is `formUuid`) to `--staged-resources-json` and let root `app-finalize` atomically switch all child heads plus the App head. Use direct `--activate` only for an intentional form-only repair.
113
+ - Publish declared schema, packages, settings, indexes, data-management, runtime-write, and public-access configuration through one `resource publish form-setting --only <codes> --change <change>` Form `bundle` mutation. Resource Form bundles are stage-only by default and return immutable `contentHash` plus a canonical `stagedResource` (stable identity is `formUuid`); the CLI merges each entry into `.openxiangda/releases/<change>/staged-resources.json`. Root `app-finalize` atomically switches all child heads plus the App head. Use direct `--activate` only for an intentional form-only repair outside a governed multi-resource release; never use `workspace publish --form` inside an atomic React SPA release.
113
114
  - Every mutation must carry `If-Match`, `expectedRevision`, `expectedParent`, and a stable artifact ID. Creation starts at revision `0`.
114
115
  - A revision/head conflict is not retryable. Stop with zero later writes, re-plan, and explicitly merge the competing change; never silently adopt the newer live head.
115
116
  - Use `openxiangda form release-head|release-list|release-detail|release-diff` for inspection and `release-activate|release-rollback|release-abort` for controlled recovery. See `docs/openxiangda-form-releases.md` in the OpenXiangda tool repository.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.165",
3
+ "version": "1.0.166",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {