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.
- package/README.md +9 -7
- package/lib/change-baseline.js +74 -6
- package/lib/cli.js +2508 -167
- package/lib/sdd.js +320 -6
- package/lib/utils.js +76 -2
- package/openxiangda-skills/SKILL.md +13 -6
- package/openxiangda-skills/references/pages/publish-flow.md +15 -16
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +24 -2
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +14 -6
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +10 -9
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +2 -0
- package/package.json +2 -1
- package/packages/sdk/dist/runtime/index.cjs +164 -48
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +1 -1
- package/packages/sdk/dist/runtime/index.d.ts +1 -1
- package/packages/sdk/dist/runtime/index.mjs +164 -48
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +164 -48
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +79 -1
- package/packages/sdk/dist/runtime/react.d.ts +79 -1
- package/packages/sdk/dist/runtime/react.mjs +164 -48
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +1 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +1 -0
- package/templates/openxiangda-react-spa/AGENTS.md +12 -10
- package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +20 -2
- package/templates/openxiangda-react-spa/scripts/deploy.mjs +2 -2
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +2 -0
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +1 -0
- package/templates/sy-lowcode-app-workspace/AGENTS.md +5 -3
- 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:
|
|
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
|
-
|
|
1443
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
83
|
-
openxiangda
|
|
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
|
-
|
|
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
|
|
3
|
+
Preferred React SPA release entry:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
openxiangda workspace
|
|
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
|
|
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
|
-
#
|
|
27
|
-
openxiangda
|
|
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
|
-
#
|
|
30
|
-
openxiangda
|
|
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
|
|
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,
|