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/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(file, `${JSON.stringify(value, null, 2)}\n`);
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
- '- [x] Record exact L1 scope and explicit approval basis',
639
- '- [ ] Implement the focused change',
663
+ '- [ ] Implement the exact covered change',
640
664
  '',
641
665
  '## Prepublish',
642
666
  '',
643
- '- [ ] Record implementation checks and dry-run evidence',
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/deploy result',
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
- '- [ ] Confirm the released change is ready to archive',
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
- writeJsonFile(path.join(changeDir, CHANGE_META_FILE), meta);
806
- writeJsonFile(path.join(changeDir, COVERAGE_FILE), coverage);
807
- writeJsonFile(path.join(changeDir, RELEASE_FILE), release);
857
+ const jsonWriteOptions = {
858
+ compact: meta.changeMode === 'quick',
859
+ };
860
+ writeJsonFile(path.join(changeDir, CHANGE_META_FILE), meta, jsonWriteOptions);
861
+ writeJsonFile(path.join(changeDir, COVERAGE_FILE), coverage, jsonWriteOptions);
862
+ writeJsonFile(path.join(changeDir, RELEASE_FILE), release, jsonWriteOptions);
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
- forms: affected.forms,
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: releasePlan?.targets || loaded.release?.targets || {},
1855
- changedFiles: releasePlan?.changedFiles || loaded.release?.changedFiles || [],
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 = loaded.release?.releaseSourceRevision;
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
- const release = {
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(release, releasePlan, legacyMode = false) {
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 targets = releasePlan?.targets || release.targets || {};
1946
- if (releasePlan?.targets) {
1947
- const releaseTargetCoverage = affectedCoversTargets(release.targets || {}, releasePlan.targets);
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 (releasePlan?.commands?.length && (!Array.isArray(release.commands) || release.commands.length === 0)) {
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 (!legacyMode && Array.isArray(release.commands)) {
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 && loaded.release && !loaded.release.__invalid
2293
- ? buildUpdatedReleaseManifest(loaded, options.releasePlan, verificationStage)
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(release, options.releasePlan, legacyMode);
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
- if (options.releasePlan && loaded.release && !loaded.release.__invalid) {
2352
- updateReleaseManifest(loaded, options.releasePlan, verificationStage);
2353
- }
2354
- writeJsonFile(loaded.metaFile, verifiedMeta);
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(releaseLoaded.release?.releaseSourceRevision);
2788
+ Boolean(releaseSourceRevision);
2599
2789
  if (requiresMainlineClosure) {
2600
2790
  if (
2601
2791
  !releaseLoaded.release ||
2602
2792
  releaseLoaded.release.__invalid ||
2603
- !releaseLoaded.release.releaseSourceRevision
2793
+ !releaseSourceRevision
2604
2794
  ) {
2605
2795
  const error = new Error(
2606
- 'SDD_RELEASE_SOURCE_LINEAGE_REQUIRED: 高风险 change 归档必须保留 release.json 的冻结发布提交;--force 不能绕过主分支回合证据'
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(releaseLoaded.release.releaseSourceRevision, {
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 sdd verify <release-change> --changed --stage prepublish --profile <name>
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 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.
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 and require `app:organization:manage` on the target app. 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.
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 whether the current actor can manage organization data for the app, the permission code, and supported operations.
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 admin-style app pages that intentionally manage platform departments/accounts. The current user needs `app:organization:manage` on the app. Do not call legacy `/user` or `/department` write endpoints from pages.
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 must have
37
- `app:organization:manage`; do not bypass OpenXiangda with legacy user or
38
- department write APIs.
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 sdd verify <release-change> --changed --stage prepublish --profile <name>
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
- `runtime deploy --no-activate` uploads an immutable preview release from a clean committed mainline `HEAD`. 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. The protected upload plan still runs before the actual build because its provider-specific `assetBaseUrl` is compiled into the bundle. `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.
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.*` and require the current operator to have `app:organization:manage`.
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 writes must go through
89
- `sdk.organization` or `ctx.organization`; the current operator must have
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