openxiangda 1.0.172 → 1.0.174
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -7
- package/lib/change-baseline.js +117 -18
- package/lib/cli.js +984 -125
- package/lib/form-schema-loader.js +166 -0
- package/lib/git-remote-identity.js +89 -0
- package/lib/release-mainline.js +59 -24
- package/lib/release-plan.js +294 -0
- package/lib/runtime-lineage.js +23 -3
- package/lib/sdd.js +246 -56
- package/openxiangda-skills/SKILL.md +8 -6
- package/openxiangda-skills/references/openxiangda-api.md +6 -6
- package/openxiangda-skills/references/pages/page-sdk.md +1 -1
- package/openxiangda-skills/references/permissions-settings.md +4 -3
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +8 -6
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +5 -4
- package/package.json +2 -1
- package/packages/sdk/dist/runtime/react.d.mts +3 -0
- package/packages/sdk/dist/runtime/react.d.ts +3 -0
- package/templates/openxiangda-react-spa/AGENTS.md +5 -11
- package/templates/openxiangda-react-spa/scripts/deploy.mjs +2 -8
- package/templates/sy-lowcode-app-workspace/AGENTS.md +2 -2
package/lib/sdd.js
CHANGED
|
@@ -3,6 +3,11 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { readGitSourceBase } = require('./change-baseline');
|
|
5
5
|
const { assertReleaseSourceIntegrated } = require('./release-mainline');
|
|
6
|
+
const {
|
|
7
|
+
buildWorkspaceReleaseCommands,
|
|
8
|
+
normalizeReleaseTargets,
|
|
9
|
+
releasePlanHash,
|
|
10
|
+
} = require('./release-plan');
|
|
6
11
|
|
|
7
12
|
const DEFAULT_SDD_DIR = 'openspec';
|
|
8
13
|
const SDD_CONFIG_FILE = 'config.yaml';
|
|
@@ -110,9 +115,12 @@ function readJsonFile(file, fallback = null) {
|
|
|
110
115
|
}
|
|
111
116
|
}
|
|
112
117
|
|
|
113
|
-
function writeJsonFile(file, value) {
|
|
118
|
+
function writeJsonFile(file, value, options = {}) {
|
|
114
119
|
ensureDir(path.dirname(file));
|
|
115
|
-
fs.writeFileSync(
|
|
120
|
+
fs.writeFileSync(
|
|
121
|
+
file,
|
|
122
|
+
`${JSON.stringify(value, null, options.compact ? 0 : 2)}\n`
|
|
123
|
+
);
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
function extractSddBlock(configText) {
|
|
@@ -573,6 +581,15 @@ function formatAffectedList(items) {
|
|
|
573
581
|
}
|
|
574
582
|
|
|
575
583
|
function createProposalContent(meta) {
|
|
584
|
+
if (meta.changeMode === 'quick') {
|
|
585
|
+
return [
|
|
586
|
+
`# Quick Change: ${meta.title}`,
|
|
587
|
+
'',
|
|
588
|
+
`User intent and approval are recorded in \`${CHANGE_META_FILE}\`.`,
|
|
589
|
+
`Exact resource/file scope is recorded in \`${COVERAGE_FILE}\`; no additional proposal prose is required.`,
|
|
590
|
+
'',
|
|
591
|
+
].join('\n');
|
|
592
|
+
}
|
|
576
593
|
return `# Proposal: ${meta.title}
|
|
577
594
|
|
|
578
595
|
## Intent
|
|
@@ -601,6 +618,14 @@ Deliver "${meta.title}" as one focused OpenXiangda application change.
|
|
|
601
618
|
}
|
|
602
619
|
|
|
603
620
|
function createDesignContent(meta) {
|
|
621
|
+
if (meta.changeMode === 'quick') {
|
|
622
|
+
return [
|
|
623
|
+
'# Design',
|
|
624
|
+
'',
|
|
625
|
+
'L1 exact-scope change; implementation stays inside coverage.json and uses the generated atomic release plan.',
|
|
626
|
+
'',
|
|
627
|
+
].join('\n');
|
|
628
|
+
}
|
|
604
629
|
return `# Design: ${meta.title}
|
|
605
630
|
|
|
606
631
|
## OpenXiangda Resource Impact
|
|
@@ -635,22 +660,19 @@ function createTasksContent(meta = {}) {
|
|
|
635
660
|
'',
|
|
636
661
|
'## Implementation',
|
|
637
662
|
'',
|
|
638
|
-
'- [
|
|
639
|
-
'- [ ] Implement the focused change',
|
|
663
|
+
'- [ ] Implement the exact covered change',
|
|
640
664
|
'',
|
|
641
665
|
'## Prepublish',
|
|
642
666
|
'',
|
|
643
|
-
'- [ ]
|
|
644
|
-
'- [ ] Confirm the exact release plan',
|
|
667
|
+
'- [ ] Run the focused check and confirm the generated release plan',
|
|
645
668
|
'',
|
|
646
669
|
'## Postpublish',
|
|
647
670
|
'',
|
|
648
|
-
'- [ ] Record publish
|
|
649
|
-
'- [ ] Merge/fast-forward the frozen release SHA and push the authoritative default branch',
|
|
671
|
+
'- [ ] Record atomic publish and mainline integration status',
|
|
650
672
|
'',
|
|
651
673
|
'## Archive',
|
|
652
674
|
'',
|
|
653
|
-
'- [ ]
|
|
675
|
+
'- [ ] Archive the released change',
|
|
654
676
|
'',
|
|
655
677
|
].join('\n');
|
|
656
678
|
}
|
|
@@ -681,7 +703,21 @@ function createTasksContent(meta = {}) {
|
|
|
681
703
|
].join('\n');
|
|
682
704
|
}
|
|
683
705
|
|
|
684
|
-
function createEvidenceContent() {
|
|
706
|
+
function createEvidenceContent(meta = {}) {
|
|
707
|
+
if (meta.changeMode === 'quick') {
|
|
708
|
+
return [
|
|
709
|
+
'# Evidence',
|
|
710
|
+
'',
|
|
711
|
+
'## Verification',
|
|
712
|
+
'',
|
|
713
|
+
'- [ ] Record the focused check result',
|
|
714
|
+
'',
|
|
715
|
+
'## Release',
|
|
716
|
+
'',
|
|
717
|
+
'- [ ] Record the atomic publish result',
|
|
718
|
+
'',
|
|
719
|
+
].join('\n');
|
|
720
|
+
}
|
|
685
721
|
return [
|
|
686
722
|
'# Evidence',
|
|
687
723
|
'',
|
|
@@ -703,6 +739,22 @@ function createEvidenceContent() {
|
|
|
703
739
|
}
|
|
704
740
|
|
|
705
741
|
function createDeltaSpecContent(meta, domain) {
|
|
742
|
+
if (meta.changeMode === 'quick') {
|
|
743
|
+
return [
|
|
744
|
+
`# ${domain} Delta Spec`,
|
|
745
|
+
'',
|
|
746
|
+
'## ADDED Requirements',
|
|
747
|
+
'',
|
|
748
|
+
`### Requirement: ${meta.title}`,
|
|
749
|
+
'The application SHALL preserve existing behavior except for the exact approved L1 scope.',
|
|
750
|
+
'',
|
|
751
|
+
'#### Scenario: Focused change',
|
|
752
|
+
'- **Given** the existing application behavior',
|
|
753
|
+
`- **When** ${meta.title} is applied`,
|
|
754
|
+
'- **Then** only the covered target changes and unrelated behavior remains unchanged',
|
|
755
|
+
'',
|
|
756
|
+
].join('\n');
|
|
757
|
+
}
|
|
706
758
|
return `# ${domain} Delta Spec
|
|
707
759
|
|
|
708
760
|
## ADDED Requirements
|
|
@@ -802,13 +854,16 @@ function proposeSddChange(options = {}) {
|
|
|
802
854
|
const meta = createChangeMetadata(changeId, options);
|
|
803
855
|
const coverage = createCoverageManifest(meta, domain);
|
|
804
856
|
const release = createReleaseManifest(meta);
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
857
|
+
const jsonWriteOptions = {
|
|
858
|
+
compact: meta.changeMode === 'quick',
|
|
859
|
+
};
|
|
860
|
+
writeJsonFile(path.join(changeDir, CHANGE_META_FILE), meta, jsonWriteOptions);
|
|
861
|
+
writeJsonFile(path.join(changeDir, COVERAGE_FILE), coverage, jsonWriteOptions);
|
|
862
|
+
writeJsonFile(path.join(changeDir, RELEASE_FILE), release, jsonWriteOptions);
|
|
808
863
|
fs.writeFileSync(path.join(changeDir, 'proposal.md'), createProposalContent(meta));
|
|
809
864
|
fs.writeFileSync(path.join(changeDir, 'design.md'), createDesignContent(meta));
|
|
810
865
|
fs.writeFileSync(path.join(changeDir, 'tasks.md'), createTasksContent(meta));
|
|
811
|
-
fs.writeFileSync(path.join(changeDir, 'evidence.md'), createEvidenceContent());
|
|
866
|
+
fs.writeFileSync(path.join(changeDir, 'evidence.md'), createEvidenceContent(meta));
|
|
812
867
|
fs.writeFileSync(
|
|
813
868
|
path.join(changeDir, 'specs', domain, 'spec.md'),
|
|
814
869
|
createDeltaSpecContent(meta, domain)
|
|
@@ -868,6 +923,19 @@ function createMainlineSddBundle(options = {}) {
|
|
|
868
923
|
if (affected.functions.length > 0 || affected.automations.length > 0) {
|
|
869
924
|
affected.backend = true;
|
|
870
925
|
}
|
|
926
|
+
const runtimeMode = /runtimeMode\s*:\s*['"]react-spa['"]/.test(
|
|
927
|
+
options.configText || ''
|
|
928
|
+
)
|
|
929
|
+
? 'react-spa'
|
|
930
|
+
: 'legacy';
|
|
931
|
+
const normalizedTargets = normalizeReleaseTargets(affected, runtimeMode);
|
|
932
|
+
const profile = String(options.profile || '<profile>').trim() || '<profile>';
|
|
933
|
+
const commands = buildWorkspaceReleaseCommands(
|
|
934
|
+
normalizedTargets.logicalTargets,
|
|
935
|
+
runtimeMode,
|
|
936
|
+
profile,
|
|
937
|
+
changeId
|
|
938
|
+
);
|
|
871
939
|
const createdAt = nowIso();
|
|
872
940
|
const meta = {
|
|
873
941
|
...createChangeMetadata(changeId, {
|
|
@@ -913,19 +981,18 @@ function createMainlineSddBundle(options = {}) {
|
|
|
913
981
|
changeId,
|
|
914
982
|
mainlinePolicy: 'publish-from-main-v1',
|
|
915
983
|
bundledChanges: sourceIds,
|
|
916
|
-
targets:
|
|
917
|
-
|
|
918
|
-
pages: affected.pages,
|
|
919
|
-
functions: affected.functions,
|
|
920
|
-
automations: affected.automations,
|
|
921
|
-
workflows: affected.workflows,
|
|
922
|
-
jsCodeNodes: affected.jsCodeNodes,
|
|
923
|
-
resources: affected.resources,
|
|
924
|
-
runtime: affected.runtime,
|
|
925
|
-
other: [],
|
|
926
|
-
},
|
|
984
|
+
targets: normalizedTargets.activationTargets,
|
|
985
|
+
logicalTargets: normalizedTargets.logicalTargets,
|
|
927
986
|
changedFiles: affected.files,
|
|
928
|
-
commands
|
|
987
|
+
commands,
|
|
988
|
+
entryCommand:
|
|
989
|
+
`openxiangda release publish --change ${changeId} --profile ${profile}`,
|
|
990
|
+
planHash: releasePlanHash({
|
|
991
|
+
changeId,
|
|
992
|
+
runtimeMode,
|
|
993
|
+
targets: normalizedTargets.activationTargets,
|
|
994
|
+
commands,
|
|
995
|
+
}),
|
|
929
996
|
verifiedAt: null,
|
|
930
997
|
};
|
|
931
998
|
const changeDir = path.join(governance.rootDir, 'changes', changeId);
|
|
@@ -1007,7 +1074,9 @@ function approveSddChange(options = {}) {
|
|
|
1007
1074
|
...(options.approvalBasis ? { approvalBasis: options.approvalBasis } : {}),
|
|
1008
1075
|
updatedAt: nowIso(),
|
|
1009
1076
|
};
|
|
1010
|
-
writeJsonFile(loaded.metaFile, meta
|
|
1077
|
+
writeJsonFile(loaded.metaFile, meta, {
|
|
1078
|
+
compact: meta.changeMode === 'quick',
|
|
1079
|
+
});
|
|
1011
1080
|
return {
|
|
1012
1081
|
schemaVersion: meta.schemaVersion,
|
|
1013
1082
|
change: meta,
|
|
@@ -1847,14 +1916,24 @@ function buildUpdatedReleaseManifest(loaded, releasePlan, verificationStage = 'd
|
|
|
1847
1916
|
const canonicalCommands = Array.isArray(releasePlan?.commands)
|
|
1848
1917
|
? releasePlan.commands.filter(command => String(command || '').trim())
|
|
1849
1918
|
: [];
|
|
1919
|
+
const actualExecution = releasePlan?.actualCommands === true;
|
|
1850
1920
|
return {
|
|
1851
1921
|
schemaVersion: SDD_SCHEMA_VERSION,
|
|
1852
1922
|
changeId: loaded.changeId,
|
|
1853
1923
|
...(loaded.release && !loaded.release.__invalid ? loaded.release : {}),
|
|
1854
|
-
targets:
|
|
1855
|
-
|
|
1924
|
+
targets:
|
|
1925
|
+
actualExecution
|
|
1926
|
+
? loaded.release?.targets || {}
|
|
1927
|
+
: releasePlan?.activationTargets ||
|
|
1928
|
+
releasePlan?.targets ||
|
|
1929
|
+
loaded.release?.targets ||
|
|
1930
|
+
{},
|
|
1931
|
+
changedFiles:
|
|
1932
|
+
actualExecution
|
|
1933
|
+
? loaded.release?.changedFiles || []
|
|
1934
|
+
: releasePlan?.changedFiles || loaded.release?.changedFiles || [],
|
|
1856
1935
|
commands:
|
|
1857
|
-
canonicalCommands.length > 0
|
|
1936
|
+
!actualExecution && canonicalCommands.length > 0
|
|
1858
1937
|
? canonicalCommands
|
|
1859
1938
|
: reviewedCommands,
|
|
1860
1939
|
verifiedAt,
|
|
@@ -1872,6 +1951,37 @@ function updateReleaseManifest(loaded, releasePlan, verificationStage = 'default
|
|
|
1872
1951
|
return release;
|
|
1873
1952
|
}
|
|
1874
1953
|
|
|
1954
|
+
function writeReleaseAttestation(cwd, changeId, name, value) {
|
|
1955
|
+
const root = path.resolve(cwd || process.cwd());
|
|
1956
|
+
const dir = path.join(
|
|
1957
|
+
root,
|
|
1958
|
+
'.openxiangda',
|
|
1959
|
+
'releases',
|
|
1960
|
+
normalizeChangeId(changeId)
|
|
1961
|
+
);
|
|
1962
|
+
ensureDir(dir);
|
|
1963
|
+
const file = path.join(dir, name);
|
|
1964
|
+
const tempFile = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
1965
|
+
fs.writeFileSync(tempFile, `${JSON.stringify(value, null, 2)}\n`, {
|
|
1966
|
+
mode: 0o600,
|
|
1967
|
+
});
|
|
1968
|
+
fs.renameSync(tempFile, file);
|
|
1969
|
+
return path.relative(root, file).replace(/\\/g, '/');
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
function readReleaseAttestation(cwd, changeId, name) {
|
|
1973
|
+
const file = path.join(
|
|
1974
|
+
path.resolve(cwd || process.cwd()),
|
|
1975
|
+
'.openxiangda',
|
|
1976
|
+
'releases',
|
|
1977
|
+
normalizeChangeId(changeId),
|
|
1978
|
+
name
|
|
1979
|
+
);
|
|
1980
|
+
if (!fs.existsSync(file)) return null;
|
|
1981
|
+
const value = readJsonFile(file, null);
|
|
1982
|
+
return value && !value.__invalid ? value : null;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1875
1985
|
function recordSddReleaseSourceRevision(options = {}) {
|
|
1876
1986
|
const loaded = loadSddChange(options);
|
|
1877
1987
|
const input = options.releaseSourceRevision;
|
|
@@ -1885,6 +1995,16 @@ function recordSddReleaseSourceRevision(options = {}) {
|
|
|
1885
1995
|
branch: String(input.branch || '').trim() || null,
|
|
1886
1996
|
remoteName: String(input.remoteName || '').trim() || null,
|
|
1887
1997
|
remoteUrlHash: String(input.remoteUrlHash || '').trim().toLowerCase() || null,
|
|
1998
|
+
remoteUrlHashAliases: Array.isArray(input.remoteUrlHashAliases)
|
|
1999
|
+
? input.remoteUrlHashAliases
|
|
2000
|
+
.map(value => String(value || '').trim().toLowerCase())
|
|
2001
|
+
.filter(value => /^sha256:[0-9a-f]{64}$/.test(value))
|
|
2002
|
+
: [],
|
|
2003
|
+
repoAliases: Array.isArray(input.repoAliases)
|
|
2004
|
+
? input.repoAliases
|
|
2005
|
+
.map(value => String(value || '').trim().toLowerCase())
|
|
2006
|
+
.filter(value => /^sha256:[0-9a-f]{64}$/.test(value))
|
|
2007
|
+
: [],
|
|
1888
2008
|
mainBranch: String(input.mainBranch || '').trim() || null,
|
|
1889
2009
|
mainTipCommit: String(input.mainTipCommit || '').trim().toLowerCase() || null,
|
|
1890
2010
|
capturedAt: input.capturedAt || nowIso(),
|
|
@@ -1901,7 +2021,13 @@ function recordSddReleaseSourceRevision(options = {}) {
|
|
|
1901
2021
|
'releaseSourceRevision 缺少有效的 repo/baseCommit/treeHash/remoteName/remoteUrlHash/mainBranch'
|
|
1902
2022
|
);
|
|
1903
2023
|
}
|
|
1904
|
-
const existing =
|
|
2024
|
+
const existing =
|
|
2025
|
+
readReleaseAttestation(
|
|
2026
|
+
options.cwd || process.cwd(),
|
|
2027
|
+
loaded.changeId,
|
|
2028
|
+
'source-revision.json'
|
|
2029
|
+
)?.releaseSourceRevision ||
|
|
2030
|
+
loaded.release?.releaseSourceRevision;
|
|
1905
2031
|
if (
|
|
1906
2032
|
existing?.baseCommit &&
|
|
1907
2033
|
(existing.baseCommit !== releaseSourceRevision.baseCommit ||
|
|
@@ -1917,20 +2043,29 @@ function recordSddReleaseSourceRevision(options = {}) {
|
|
|
1917
2043
|
error.code = 'RELEASE_PUBLISH_REVISION_CHANGED';
|
|
1918
2044
|
throw error;
|
|
1919
2045
|
}
|
|
1920
|
-
|
|
2046
|
+
writeReleaseAttestation(
|
|
2047
|
+
options.cwd || process.cwd(),
|
|
2048
|
+
loaded.changeId,
|
|
2049
|
+
'source-revision.json',
|
|
2050
|
+
{
|
|
2051
|
+
schemaVersion: SDD_SCHEMA_VERSION,
|
|
2052
|
+
changeId: loaded.changeId,
|
|
2053
|
+
mainlinePolicy: 'publish-from-main-v1',
|
|
2054
|
+
releaseSourceRevision,
|
|
2055
|
+
}
|
|
2056
|
+
);
|
|
2057
|
+
return {
|
|
1921
2058
|
...(loaded.release && !loaded.release.__invalid ? loaded.release : {}),
|
|
1922
|
-
schemaVersion: SDD_SCHEMA_VERSION,
|
|
1923
|
-
changeId: loaded.changeId,
|
|
1924
|
-
mainlinePolicy: 'publish-from-main-v1',
|
|
1925
2059
|
releaseSourceRevision,
|
|
1926
2060
|
};
|
|
1927
|
-
if (JSON.stringify(release) !== JSON.stringify(loaded.release)) {
|
|
1928
|
-
writeJsonFile(loaded.releaseFile, release);
|
|
1929
|
-
}
|
|
1930
|
-
return release;
|
|
1931
2061
|
}
|
|
1932
2062
|
|
|
1933
|
-
function validateReleaseManifest(
|
|
2063
|
+
function validateReleaseManifest(
|
|
2064
|
+
release,
|
|
2065
|
+
releasePlan,
|
|
2066
|
+
legacyMode = false,
|
|
2067
|
+
options = {}
|
|
2068
|
+
) {
|
|
1934
2069
|
const errors = [];
|
|
1935
2070
|
const warnings = [];
|
|
1936
2071
|
if (!release || release.__invalid) {
|
|
@@ -1942,9 +2077,14 @@ function validateReleaseManifest(release, releasePlan, legacyMode = false) {
|
|
|
1942
2077
|
else errors.push(issue);
|
|
1943
2078
|
return { errors, warnings };
|
|
1944
2079
|
}
|
|
1945
|
-
const
|
|
1946
|
-
|
|
1947
|
-
|
|
2080
|
+
const plannedTargets =
|
|
2081
|
+
releasePlan?.activationTargets || releasePlan?.targets || null;
|
|
2082
|
+
const targets = plannedTargets || release.targets || {};
|
|
2083
|
+
if (plannedTargets) {
|
|
2084
|
+
const releaseTargetCoverage = affectedCoversTargets(
|
|
2085
|
+
release.targets || {},
|
|
2086
|
+
plannedTargets
|
|
2087
|
+
);
|
|
1948
2088
|
if (!releaseTargetCoverage.covered) {
|
|
1949
2089
|
errors.push({
|
|
1950
2090
|
name: 'sdd-release-targets-uncovered',
|
|
@@ -1955,10 +2095,19 @@ function validateReleaseManifest(release, releasePlan, legacyMode = false) {
|
|
|
1955
2095
|
if (hasHighRiskTargets(targets) && !Array.isArray(release.commands)) {
|
|
1956
2096
|
errors.push({ name: 'sdd-release-commands-missing', message: 'release.json commands 必须是数组' });
|
|
1957
2097
|
}
|
|
1958
|
-
if (
|
|
2098
|
+
if (
|
|
2099
|
+
!options.actualExecution &&
|
|
2100
|
+
releasePlan?.commands?.length &&
|
|
2101
|
+
(!Array.isArray(release.commands) || release.commands.length === 0)
|
|
2102
|
+
) {
|
|
1959
2103
|
errors.push({ name: 'sdd-release-commands-empty', message: 'release.json 未记录发布命令' });
|
|
1960
2104
|
}
|
|
1961
|
-
if (
|
|
2105
|
+
if (
|
|
2106
|
+
!legacyMode &&
|
|
2107
|
+
!options.actualExecution &&
|
|
2108
|
+
Array.isArray(release.commands) &&
|
|
2109
|
+
release.commands.length > 0
|
|
2110
|
+
) {
|
|
1962
2111
|
errors.push(
|
|
1963
2112
|
...validateAtomicReleaseCommands(
|
|
1964
2113
|
release.commands,
|
|
@@ -2288,12 +2437,25 @@ function verifySddChange(options = {}) {
|
|
|
2288
2437
|
if (strictDocumentation) errors.push(issue);
|
|
2289
2438
|
else warnings.push(issue);
|
|
2290
2439
|
}
|
|
2440
|
+
const actualExecution = options.releasePlan?.actualCommands === true;
|
|
2291
2441
|
const release =
|
|
2292
|
-
options.releasePlan &&
|
|
2293
|
-
|
|
2442
|
+
options.releasePlan &&
|
|
2443
|
+
loaded.release &&
|
|
2444
|
+
!loaded.release.__invalid &&
|
|
2445
|
+
!actualExecution
|
|
2446
|
+
? buildUpdatedReleaseManifest(
|
|
2447
|
+
loaded,
|
|
2448
|
+
options.releasePlan,
|
|
2449
|
+
verificationStage
|
|
2450
|
+
)
|
|
2294
2451
|
: loaded.release;
|
|
2295
2452
|
if (stagePolicy.requireRelease) {
|
|
2296
|
-
const releaseValidation = validateReleaseManifest(
|
|
2453
|
+
const releaseValidation = validateReleaseManifest(
|
|
2454
|
+
release,
|
|
2455
|
+
options.releasePlan,
|
|
2456
|
+
legacyMode,
|
|
2457
|
+
{ actualExecution }
|
|
2458
|
+
);
|
|
2297
2459
|
errors.push(...releaseValidation.errors);
|
|
2298
2460
|
warnings.push(...releaseValidation.warnings);
|
|
2299
2461
|
const reviewedPlannedCommands = loaded.meta?.release?.plannedCommands;
|
|
@@ -2347,11 +2509,28 @@ function verifySddChange(options = {}) {
|
|
|
2347
2509
|
updatedAt: verifiedAt,
|
|
2348
2510
|
};
|
|
2349
2511
|
let metadataWritten = false;
|
|
2512
|
+
let attestationFile = null;
|
|
2350
2513
|
if (options.writeMetadata && passed) {
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2514
|
+
attestationFile = writeReleaseAttestation(
|
|
2515
|
+
options.cwd || process.cwd(),
|
|
2516
|
+
loaded.changeId,
|
|
2517
|
+
`verification-${verificationStage}.json`,
|
|
2518
|
+
{
|
|
2519
|
+
schemaVersion: SDD_SCHEMA_VERSION,
|
|
2520
|
+
changeId: loaded.changeId,
|
|
2521
|
+
stage: verificationStage,
|
|
2522
|
+
passed,
|
|
2523
|
+
verifiedAt,
|
|
2524
|
+
planHash: releasePlanHash({
|
|
2525
|
+
targets,
|
|
2526
|
+
commands: release?.commands || [],
|
|
2527
|
+
changedFiles:
|
|
2528
|
+
options.releasePlan?.changedFiles ||
|
|
2529
|
+
loaded.release?.changedFiles ||
|
|
2530
|
+
[],
|
|
2531
|
+
}),
|
|
2532
|
+
}
|
|
2533
|
+
);
|
|
2355
2534
|
metadataWritten = true;
|
|
2356
2535
|
}
|
|
2357
2536
|
return {
|
|
@@ -2365,6 +2544,8 @@ function verifySddChange(options = {}) {
|
|
|
2365
2544
|
blockingErrors: errors,
|
|
2366
2545
|
passed,
|
|
2367
2546
|
metadataWritten,
|
|
2547
|
+
trackedMetadataWritten: false,
|
|
2548
|
+
attestationFile,
|
|
2368
2549
|
targets,
|
|
2369
2550
|
highRiskFiles,
|
|
2370
2551
|
stage: verificationStage,
|
|
@@ -2559,6 +2740,15 @@ function syncSddChange(options = {}) {
|
|
|
2559
2740
|
|
|
2560
2741
|
function archiveSddChange(options = {}) {
|
|
2561
2742
|
const releaseLoaded = loadSddChange(options);
|
|
2743
|
+
const sourceRevisionAttestation = readReleaseAttestation(
|
|
2744
|
+
options.cwd || process.cwd(),
|
|
2745
|
+
releaseLoaded.changeId,
|
|
2746
|
+
'source-revision.json'
|
|
2747
|
+
);
|
|
2748
|
+
const releaseSourceRevision =
|
|
2749
|
+
sourceRevisionAttestation?.releaseSourceRevision ||
|
|
2750
|
+
releaseLoaded.release?.releaseSourceRevision ||
|
|
2751
|
+
null;
|
|
2562
2752
|
const coverageTargets = normalizeCoverageResources(releaseLoaded.coverage, {});
|
|
2563
2753
|
const releaseTargets = mergeAffectedScopes(
|
|
2564
2754
|
options.targets,
|
|
@@ -2595,20 +2785,20 @@ function archiveSddChange(options = {}) {
|
|
|
2595
2785
|
classifyHighRiskFiles(releaseTargets.files).length > 0 ||
|
|
2596
2786
|
metadataDeclaresHighRisk ||
|
|
2597
2787
|
Boolean(releaseLoaded.release?.mainlinePolicy) ||
|
|
2598
|
-
Boolean(
|
|
2788
|
+
Boolean(releaseSourceRevision);
|
|
2599
2789
|
if (requiresMainlineClosure) {
|
|
2600
2790
|
if (
|
|
2601
2791
|
!releaseLoaded.release ||
|
|
2602
2792
|
releaseLoaded.release.__invalid ||
|
|
2603
|
-
!
|
|
2793
|
+
!releaseSourceRevision
|
|
2604
2794
|
) {
|
|
2605
2795
|
const error = new Error(
|
|
2606
|
-
'SDD_RELEASE_SOURCE_LINEAGE_REQUIRED: 高风险 change
|
|
2796
|
+
'SDD_RELEASE_SOURCE_LINEAGE_REQUIRED: 高风险 change 归档必须保留冻结发布提交的本地 attestation;--force 不能绕过主分支回合证据'
|
|
2607
2797
|
);
|
|
2608
2798
|
error.code = 'SDD_RELEASE_SOURCE_LINEAGE_REQUIRED';
|
|
2609
2799
|
throw error;
|
|
2610
2800
|
}
|
|
2611
|
-
assertReleaseSourceIntegrated(
|
|
2801
|
+
assertReleaseSourceIntegrated(releaseSourceRevision, {
|
|
2612
2802
|
cwd: options.cwd || process.cwd(),
|
|
2613
2803
|
});
|
|
2614
2804
|
}
|
|
@@ -60,6 +60,8 @@ openxiangda sdd quick <change> --kind copy --pages <page> --files <file> --summa
|
|
|
60
60
|
|
|
61
61
|
Function quick changes require the complete `--risk-json` assessment; Automation, Workflow, schema, permissions, auth/public access, migrations, destructive and cross-resource changes cannot use quick mode. Before release run `--stage prepublish`; use `postpublish` only after promotion and `archive` only when all evidence exists.
|
|
62
62
|
|
|
63
|
+
Quick changes use compact generated JSON/prose. Do not expand them into proposal/design essays; implement the exact covered code, run the focused check, and let the mainline bundle own final atomic promotion.
|
|
64
|
+
|
|
63
65
|
Keep development lightweight: structured approval plus exact resource/file scope are authoritative. By default, unfinished tasks/evidence/spec prose produces warnings at every stage and does not block release; set `strictDocumentation: true` only for a workspace that intentionally wants prose as a gate. Actual publish argv, mainline identity, CAS, lease, destructive scope, and atomic activation remain hard gates.
|
|
64
66
|
|
|
65
67
|
The CLI regenerates one canonical command set from structured coverage instead of asking agents to maintain two command copies. A single Function uses `resource publish function --only <code>`; qualified selectors are used only when Function and Automation are mixed. Generated React SPA plans use exact Form bundles, one Backend `--stage-only` command, Runtime `--no-activate`, and Root `app-finalize`. Actual argv is validated again at write time.
|
|
@@ -82,16 +84,16 @@ React SPA resources/runtime:
|
|
|
82
84
|
# In the clean main checkout:
|
|
83
85
|
openxiangda sdd bundle <release-change> --changes <change-a,change-b>
|
|
84
86
|
# Commit and push the generated bundle on main, then:
|
|
85
|
-
openxiangda
|
|
86
|
-
openxiangda release begin --change <release-change> --profile <name>
|
|
87
|
-
# Execute the canonical exact commands printed by workspace plan/verify.
|
|
88
|
-
openxiangda release integration-status --profile <name>
|
|
89
|
-
openxiangda release end --profile <name>
|
|
87
|
+
openxiangda release publish --change <release-change> --profile <name>
|
|
90
88
|
```
|
|
91
89
|
|
|
92
90
|
`resource plan` and publish dry-runs are strictly GET/HEAD-only. `READ_ONLY_AUTH_REQUIRED` means the access token expired; run `openxiangda auth refresh --profile <name>` or log in again before retrying. Never add an automatic refresh POST inside a plan.
|
|
93
91
|
|
|
94
|
-
`release
|
|
92
|
+
`release publish` is the default promotion entrypoint. It verifies without rewriting reviewed `change.json`/`release.json`, waits for the app lease, freezes the App capture after ownership is acquired, executes deterministic exact staged steps, resumes from `.openxiangda/releases/<change>/execution.json`, atomically finalizes, verifies mainline integration, and releases the lease. `release begin` and child commands remain recovery/diagnostic primitives.
|
|
93
|
+
|
|
94
|
+
Reviewed bundle commands may retain `<profile>` as a template. The explicit real `release publish --profile <name>` value is bound to actual child argv without rewriting tracked SDD. React SPA page codes are logical coverage targets and activate through one Runtime child; they do not require PageRelease. `release app-head` is compact by default; use `--full` only when the complete manifest is required.
|
|
95
|
+
|
|
96
|
+
`release begin --change` freezes a clean committed `HEAD` only when the current branch is the authoritative default `main`/`master` and its commit exactly equals the live remote tip. Before any live write, the CLI preflights the complete target set and rejects source changes during the release. A feature worktree or unpushed main receives `RELEASE_SOURCE_MAINLINE_REQUIRED` / `RELEASE_SOURCE_MAINLINE_NOT_PUSHED`; merge, test, push, and start the one mainline release instead of forcing it. Optional `.git` remote suffix differences are aliases of the same repository; genuinely different remotes still fail closed.
|
|
95
97
|
|
|
96
98
|
Because promotion begins from the already-pushed authoritative mainline, `release integration-status` should pass immediately after activation. Run it, then `release end`; no post-release branch merge is required.
|
|
97
99
|
|
|
@@ -442,13 +442,13 @@ Requires app admin permission. Upserts a binding with `templateCode` or `templat
|
|
|
442
442
|
|
|
443
443
|
## Organization
|
|
444
444
|
|
|
445
|
-
Organization APIs are app-scoped
|
|
445
|
+
Organization APIs are app-scoped. List/detail operations accept `app:organization:read` or `app:organization:manage`; create/update/password operations require `app:organization:manage`. Platform admins pass through the same permission service. Runtime service principals are not accepted by themselves: trusted runtime calls must carry a real audit actor, and permission is checked against that user.
|
|
446
446
|
|
|
447
447
|
Use these endpoints for OpenXiangda app-managed platform departments and accounts. Do not use legacy `/user` or `/department` write endpoints for new app organization management.
|
|
448
448
|
|
|
449
449
|
### GET `/apps/:appType/organization/capabilities`
|
|
450
450
|
|
|
451
|
-
Requires Bearer token. Returns
|
|
451
|
+
Requires Bearer token. Returns `canRead`, `canManage`, `readPermissionCode`, `managePermissionCode`, the compatibility `permissionCode`, and supported operations.
|
|
452
452
|
|
|
453
453
|
CLI:
|
|
454
454
|
|
|
@@ -458,11 +458,11 @@ openxiangda organization capabilities --json
|
|
|
458
458
|
|
|
459
459
|
### GET `/apps/:appType/organization/departments`
|
|
460
460
|
|
|
461
|
-
Requires `app:organization:manage`. Lists platform departments in the current tenant.
|
|
461
|
+
Requires `app:organization:read` or `app:organization:manage`. Lists platform departments in the current tenant.
|
|
462
462
|
|
|
463
463
|
### GET `/apps/:appType/organization/departments/:departmentId`
|
|
464
464
|
|
|
465
|
-
Requires `app:organization:manage`. Returns one platform department.
|
|
465
|
+
Requires `app:organization:read` or `app:organization:manage`. Returns one platform department.
|
|
466
466
|
|
|
467
467
|
### POST `/apps/:appType/organization/departments`
|
|
468
468
|
|
|
@@ -492,11 +492,11 @@ Requires `app:organization:manage`. Updates department metadata, visibility/memb
|
|
|
492
492
|
|
|
493
493
|
### GET `/apps/:appType/organization/accounts`
|
|
494
494
|
|
|
495
|
-
Requires `app:organization:manage`. Lists tenant platform accounts. Supports `ids`, `departmentIds`, `keyword`, `name`, `username`, `phone`, `email`, `jobNumber`, `page`, and `pageSize`.
|
|
495
|
+
Requires `app:organization:read` or `app:organization:manage`. Lists tenant platform accounts. Supports `ids`, `departmentIds`, `keyword`, `name`, `username`, `phone`, `email`, `jobNumber`, `page`, and `pageSize`.
|
|
496
496
|
|
|
497
497
|
### GET `/apps/:appType/organization/accounts/:userId`
|
|
498
498
|
|
|
499
|
-
Requires `app:organization:manage`. Returns one platform account. Password, login failure counters, and lock fields are removed from the response.
|
|
499
|
+
Requires `app:organization:read` or `app:organization:manage`. Returns one platform account. Password, login failure counters, and lock fields are removed from the response.
|
|
500
500
|
|
|
501
501
|
### POST `/apps/:appType/organization/accounts`
|
|
502
502
|
|
|
@@ -17,7 +17,7 @@ Guidelines:
|
|
|
17
17
|
- Use `sdk.connector.invoke`, `sdk.connector.call("connector.api")`, or `sdk.connector.download` for external services. The SDK calls the platform runtime connector endpoint; it must not call third-party domains directly.
|
|
18
18
|
- Use `sdk.notification.sendByType` and `batchSendByType` for reusable business messages. Custom notification types must be declared in `src/resources/notifications/` and published with `openxiangda resource publish`.
|
|
19
19
|
- Use `AttachmentPreviewList`, `ImagePreviewGrid`, or `useFilePreview` from `openxiangda/runtime/react` for in-page attachment previews in a custom React SPA page. They use the current PageSdk context and enforce the platform capability and ticket contracts. Use `sdk.createFileAccessTicket(bucketName, objectName, fileName, "preview", { appType })` only when the page needs a shareable or new-window preview link. `appType` defaults to the current page context. Open `response.result.previewPageUrl`; do not use `previewUrl` or `/service/file/preview-by-ticket/:ticket` as the page entry.
|
|
20
|
-
- Use `sdk.organization.departments.*` and `sdk.organization.accounts.*` only for
|
|
20
|
+
- Use `sdk.organization.departments.*` and `sdk.organization.accounts.*` only for intentional organization pages. Read-only list/detail pages need `app:organization:read` or `app:organization:manage`; writes and password operations need `app:organization:manage`. Do not call legacy `/user` or `/department` endpoints from pages.
|
|
21
21
|
- Use `sdk.workCenter.listItems({ boxType })` and `sdk.workCenter.getStats()` for current-user work center lists and counts. `boxType` is one of `todo`, `done`, `cc`, or `initiated`. This is the end-user task/handled/cc/initiated surface; do not build todo pages by reading workflow operation logs, automation logs, or raw process-task tables.
|
|
22
22
|
- `sdk.form.create` returns identifiers and generated serial number values only. Treat `formInstId` / `formInstanceId` as the required success contract; `serialNumber` / `serialNumbers` are present only when the form has `SerialNumberField`. Do not expect the save response to contain the full row, formula results, or other server-generated field values. Call `sdk.form.getDetail` explicitly after create when those values are needed.
|
|
23
23
|
- Missing `formInstId` / `formInstanceId` is a real error in strict mode. Do not create fake IDs, mock rows, or fallback display data to hide it.
|
|
@@ -33,9 +33,10 @@ in `src/resources/roles/<code>.json`; otherwise the user may have the business
|
|
|
33
33
|
role but still fail when calling role-setting APIs.
|
|
34
34
|
|
|
35
35
|
For app-managed platform accounts or departments, use `sdk.organization.*` or
|
|
36
|
-
`ctx.organization.*`. The real current operator
|
|
37
|
-
`app:organization:manage
|
|
38
|
-
|
|
36
|
+
`ctx.organization.*`. The real current operator needs `app:organization:read`
|
|
37
|
+
or `app:organization:manage` for list/detail queries and
|
|
38
|
+
`app:organization:manage` for writes/password operations; do not bypass
|
|
39
|
+
OpenXiangda with legacy user or department APIs.
|
|
39
40
|
|
|
40
41
|
## Role Codes
|
|
41
42
|
|
|
@@ -89,14 +89,16 @@ Forms, Backend resources, and frontend Runtime are staged children of one Root A
|
|
|
89
89
|
```bash
|
|
90
90
|
openxiangda sdd bundle <release-change> --changes <change-a,change-b>
|
|
91
91
|
# Commit and push the bundle on main/master.
|
|
92
|
-
openxiangda
|
|
93
|
-
openxiangda release begin --change <release-change> --profile <name>
|
|
94
|
-
# Run the canonical exact staged commands emitted by the release plan.
|
|
95
|
-
openxiangda release integration-status --profile <name>
|
|
96
|
-
openxiangda release end --profile <name>
|
|
92
|
+
openxiangda release publish --change <release-change> --profile <name>
|
|
97
93
|
```
|
|
98
94
|
|
|
99
|
-
`
|
|
95
|
+
`release publish` is the normal whole-app entrypoint: it verifies SDD without mutating reviewed files, waits for the promotion lease, freezes one App capture, stages the exact Form/Backend/Runtime children, resumes from a private execution journal, finalizes once, and releases the lease. Individual release commands are recovery/diagnostic primitives.
|
|
96
|
+
|
|
97
|
+
Reviewed bundle commands may retain `<profile>` as a template. The explicit real `release publish --profile <name>` value is bound to actual child argv without rewriting tracked SDD. React SPA page codes remain logical coverage targets and activate through the single Runtime child; they do not require PageRelease. If local lease state disappears, `release end --change <id>` reconciles a self-owned remote lease from the private execution journal and never reports inactive while a remote lease is active.
|
|
98
|
+
|
|
99
|
+
`runtime deploy --no-activate` uploads an immutable preview release from a clean committed mainline `HEAD`. It reads a narrow Runtime head instead of the full app snapshot. Before acquiring a lease, the CLI confirms `package.json#scripts.build` and existing dependencies; it uses `npm run build` to execute the declared script so pnpm worktree symlinks do not trigger a reinstall. Immutable Git-base artifact hashes are cached under the Git common directory. `release begin` requires local main/master and the live remote default tip to be identical, so `integration-status` is already satisfied after activation and `release end` does not wait for a later merge.
|
|
100
|
+
|
|
101
|
+
`release app-head` returns a compact head/resource-count summary by default. Use `--full` only when the complete manifest is required.
|
|
100
102
|
|
|
101
103
|
`resource plan`, `resource publish`, and `runtime deploy` report coarse phases on stderr and emit a heartbeat after 15 seconds without completion. JSON stdout stays machine-readable and includes `timings`; use it to identify whether time is spent in plan, preflight, build, upload, write, or activate instead of rerunning an opaque command.
|
|
102
104
|
|
|
@@ -22,7 +22,7 @@ description: Manage OpenXiangda app **roles** (角色), **page permission groups
|
|
|
22
22
|
- ✅ For dynamic multi-role apps, build a role maintenance form + automation/JS_CODE sync, not hardcoded role logic in page code.
|
|
23
23
|
- ✅ For permission scope keys, derive hidden scalars (`collegeScopeKey`, `classScopeKey`, `ownerDeptScopeKey`, `ownerUserScopeKey`, `roleCode`) via `valueSync` from visible select/person/department fields.
|
|
24
24
|
- ✅ Use page permission groups for entry visibility, form permission groups for real data isolation with condition-based data permissions.
|
|
25
|
-
- ✅ For app-managed platform accounts/departments, use `sdk.organization.*` / `ctx.organization
|
|
25
|
+
- ✅ For app-managed platform accounts/departments, use `sdk.organization.*` / `ctx.organization.*`. Read-only list/detail operations require `app:organization:read` or `app:organization:manage`; writes and password operations require `app:organization:manage`.
|
|
26
26
|
- ✅ For delegated administrators such as 校区管理员, put role-setting API permissions in the role manifest with `apiPermissionCodes`; use `app:role:manage` for app role creation/member assignment/API-permission assignment, and add page/form/organization management permission codes only when needed.
|
|
27
27
|
- ✅ If a delegated administrator creates another role that can also manage accounts, roles, or permission groups, publish that new role with its own `apiPermissionCodes`.
|
|
28
28
|
- ❌ Expose raw ID text fields to users for permission scopes.
|
|
@@ -85,9 +85,10 @@ For dynamic multi-role apps, do not hardcode role behavior only in page code. Cr
|
|
|
85
85
|
For managed platform account governance, use organization/account forms and sync
|
|
86
86
|
functions as a closed loop: organization units, system accounts, role
|
|
87
87
|
assignments, roles resources, page groups, form groups, sync App Functions, and
|
|
88
|
-
backend role/scope checks. Platform account
|
|
89
|
-
`sdk.organization` or `ctx.organization`;
|
|
90
|
-
`app:organization:manage
|
|
88
|
+
backend role/scope checks. Platform account access must go through
|
|
89
|
+
`sdk.organization` or `ctx.organization`; give read-only operators
|
|
90
|
+
`app:organization:read` and require `app:organization:manage` for writes and
|
|
91
|
+
password operations.
|
|
91
92
|
|
|
92
93
|
## Page Permission Groups
|
|
93
94
|
|