openxiangda 1.0.172 → 1.0.173

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.
@@ -2,6 +2,7 @@ const path = require('path');
2
2
  const { spawnSync } = require('child_process');
3
3
 
4
4
  const { readGitSourceBase } = require('./change-baseline');
5
+ const { identitiesIntersect } = require('./git-remote-identity');
5
6
 
6
7
  const MIN_ROLLBACK_REASON_LENGTH = 8;
7
8
  const PURE_RUNTIME_OUTPUT_PREFIXES = Object.freeze([
@@ -58,7 +59,13 @@ function prepareRuntimeDeployLineage(options = {}) {
58
59
 
59
60
  const legacyBootstrap = Boolean(parentReleaseId && !parentSourceRevision);
60
61
  if (parentSourceRevision) {
61
- const sameRepository = parentSourceRevision.repo === sourceRevision.repo;
62
+ const sameRepository = identitiesIntersect(
63
+ [
64
+ parentSourceRevision.repo,
65
+ ...(parentSourceRevision.repoAliases || []),
66
+ ],
67
+ [sourceRevision.repo, ...(sourceRevision.repoAliases || [])]
68
+ );
62
69
  const descendsFromActive =
63
70
  sameRepository &&
64
71
  isGitAncestor(cwd, parentSourceRevision.baseCommit, sourceRevision.baseCommit);
@@ -73,7 +80,13 @@ function prepareRuntimeDeployLineage(options = {}) {
73
80
  );
74
81
  }
75
82
  return stripUndefinedValues({
76
- sourceRevision,
83
+ sourceRevision: sameRepository
84
+ ? {
85
+ ...sourceRevision,
86
+ repo: parentSourceRevision.repo,
87
+ repositoryId: parentSourceRevision.repo,
88
+ }
89
+ : sourceRevision,
77
90
  parentReleaseId,
78
91
  parentSourceRevision,
79
92
  lineageBypassReason: rollback.enabled ? rollback.reason : undefined,
@@ -246,7 +259,14 @@ function normalizeRuntimeSourceRevision(value, options = {}) {
246
259
  '线上 Runtime source revision 缺少有效的 repo/baseCommit/treeHash'
247
260
  );
248
261
  }
249
- return { repo, baseCommit, treeHash };
262
+ const repoAliases = Array.from(
263
+ new Set(
264
+ [repo, ...(Array.isArray(input.repoAliases) ? input.repoAliases : [])]
265
+ .map(item => String(item || '').trim().toLowerCase())
266
+ .filter(item => /^sha256:[0-9a-f]{64}$/.test(item))
267
+ )
268
+ );
269
+ return { repo, repoAliases, baseCommit, treeHash };
250
270
  }
251
271
 
252
272
  function normalizeAppSnapshot(value) {
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';
@@ -868,6 +873,19 @@ function createMainlineSddBundle(options = {}) {
868
873
  if (affected.functions.length > 0 || affected.automations.length > 0) {
869
874
  affected.backend = true;
870
875
  }
876
+ const runtimeMode = /runtimeMode\s*:\s*['"]react-spa['"]/.test(
877
+ options.configText || ''
878
+ )
879
+ ? 'react-spa'
880
+ : 'legacy';
881
+ const normalizedTargets = normalizeReleaseTargets(affected, runtimeMode);
882
+ const profile = String(options.profile || '<profile>').trim() || '<profile>';
883
+ const commands = buildWorkspaceReleaseCommands(
884
+ normalizedTargets.logicalTargets,
885
+ runtimeMode,
886
+ profile,
887
+ changeId
888
+ );
871
889
  const createdAt = nowIso();
872
890
  const meta = {
873
891
  ...createChangeMetadata(changeId, {
@@ -913,19 +931,18 @@ function createMainlineSddBundle(options = {}) {
913
931
  changeId,
914
932
  mainlinePolicy: 'publish-from-main-v1',
915
933
  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
- },
934
+ targets: normalizedTargets.activationTargets,
935
+ logicalTargets: normalizedTargets.logicalTargets,
927
936
  changedFiles: affected.files,
928
- commands: [],
937
+ commands,
938
+ entryCommand:
939
+ `openxiangda release publish --change ${changeId} --profile ${profile}`,
940
+ planHash: releasePlanHash({
941
+ changeId,
942
+ runtimeMode,
943
+ targets: normalizedTargets.activationTargets,
944
+ commands,
945
+ }),
929
946
  verifiedAt: null,
930
947
  };
931
948
  const changeDir = path.join(governance.rootDir, 'changes', changeId);
@@ -1847,14 +1864,24 @@ function buildUpdatedReleaseManifest(loaded, releasePlan, verificationStage = 'd
1847
1864
  const canonicalCommands = Array.isArray(releasePlan?.commands)
1848
1865
  ? releasePlan.commands.filter(command => String(command || '').trim())
1849
1866
  : [];
1867
+ const actualExecution = releasePlan?.actualCommands === true;
1850
1868
  return {
1851
1869
  schemaVersion: SDD_SCHEMA_VERSION,
1852
1870
  changeId: loaded.changeId,
1853
1871
  ...(loaded.release && !loaded.release.__invalid ? loaded.release : {}),
1854
- targets: releasePlan?.targets || loaded.release?.targets || {},
1855
- changedFiles: releasePlan?.changedFiles || loaded.release?.changedFiles || [],
1872
+ targets:
1873
+ actualExecution
1874
+ ? loaded.release?.targets || {}
1875
+ : releasePlan?.activationTargets ||
1876
+ releasePlan?.targets ||
1877
+ loaded.release?.targets ||
1878
+ {},
1879
+ changedFiles:
1880
+ actualExecution
1881
+ ? loaded.release?.changedFiles || []
1882
+ : releasePlan?.changedFiles || loaded.release?.changedFiles || [],
1856
1883
  commands:
1857
- canonicalCommands.length > 0
1884
+ !actualExecution && canonicalCommands.length > 0
1858
1885
  ? canonicalCommands
1859
1886
  : reviewedCommands,
1860
1887
  verifiedAt,
@@ -1872,6 +1899,37 @@ function updateReleaseManifest(loaded, releasePlan, verificationStage = 'default
1872
1899
  return release;
1873
1900
  }
1874
1901
 
1902
+ function writeReleaseAttestation(cwd, changeId, name, value) {
1903
+ const root = path.resolve(cwd || process.cwd());
1904
+ const dir = path.join(
1905
+ root,
1906
+ '.openxiangda',
1907
+ 'releases',
1908
+ normalizeChangeId(changeId)
1909
+ );
1910
+ ensureDir(dir);
1911
+ const file = path.join(dir, name);
1912
+ const tempFile = `${file}.${process.pid}.${Date.now()}.tmp`;
1913
+ fs.writeFileSync(tempFile, `${JSON.stringify(value, null, 2)}\n`, {
1914
+ mode: 0o600,
1915
+ });
1916
+ fs.renameSync(tempFile, file);
1917
+ return path.relative(root, file).replace(/\\/g, '/');
1918
+ }
1919
+
1920
+ function readReleaseAttestation(cwd, changeId, name) {
1921
+ const file = path.join(
1922
+ path.resolve(cwd || process.cwd()),
1923
+ '.openxiangda',
1924
+ 'releases',
1925
+ normalizeChangeId(changeId),
1926
+ name
1927
+ );
1928
+ if (!fs.existsSync(file)) return null;
1929
+ const value = readJsonFile(file, null);
1930
+ return value && !value.__invalid ? value : null;
1931
+ }
1932
+
1875
1933
  function recordSddReleaseSourceRevision(options = {}) {
1876
1934
  const loaded = loadSddChange(options);
1877
1935
  const input = options.releaseSourceRevision;
@@ -1885,6 +1943,16 @@ function recordSddReleaseSourceRevision(options = {}) {
1885
1943
  branch: String(input.branch || '').trim() || null,
1886
1944
  remoteName: String(input.remoteName || '').trim() || null,
1887
1945
  remoteUrlHash: String(input.remoteUrlHash || '').trim().toLowerCase() || null,
1946
+ remoteUrlHashAliases: Array.isArray(input.remoteUrlHashAliases)
1947
+ ? input.remoteUrlHashAliases
1948
+ .map(value => String(value || '').trim().toLowerCase())
1949
+ .filter(value => /^sha256:[0-9a-f]{64}$/.test(value))
1950
+ : [],
1951
+ repoAliases: Array.isArray(input.repoAliases)
1952
+ ? input.repoAliases
1953
+ .map(value => String(value || '').trim().toLowerCase())
1954
+ .filter(value => /^sha256:[0-9a-f]{64}$/.test(value))
1955
+ : [],
1888
1956
  mainBranch: String(input.mainBranch || '').trim() || null,
1889
1957
  mainTipCommit: String(input.mainTipCommit || '').trim().toLowerCase() || null,
1890
1958
  capturedAt: input.capturedAt || nowIso(),
@@ -1901,7 +1969,13 @@ function recordSddReleaseSourceRevision(options = {}) {
1901
1969
  'releaseSourceRevision 缺少有效的 repo/baseCommit/treeHash/remoteName/remoteUrlHash/mainBranch'
1902
1970
  );
1903
1971
  }
1904
- const existing = loaded.release?.releaseSourceRevision;
1972
+ const existing =
1973
+ readReleaseAttestation(
1974
+ options.cwd || process.cwd(),
1975
+ loaded.changeId,
1976
+ 'source-revision.json'
1977
+ )?.releaseSourceRevision ||
1978
+ loaded.release?.releaseSourceRevision;
1905
1979
  if (
1906
1980
  existing?.baseCommit &&
1907
1981
  (existing.baseCommit !== releaseSourceRevision.baseCommit ||
@@ -1917,20 +1991,29 @@ function recordSddReleaseSourceRevision(options = {}) {
1917
1991
  error.code = 'RELEASE_PUBLISH_REVISION_CHANGED';
1918
1992
  throw error;
1919
1993
  }
1920
- const release = {
1994
+ writeReleaseAttestation(
1995
+ options.cwd || process.cwd(),
1996
+ loaded.changeId,
1997
+ 'source-revision.json',
1998
+ {
1999
+ schemaVersion: SDD_SCHEMA_VERSION,
2000
+ changeId: loaded.changeId,
2001
+ mainlinePolicy: 'publish-from-main-v1',
2002
+ releaseSourceRevision,
2003
+ }
2004
+ );
2005
+ return {
1921
2006
  ...(loaded.release && !loaded.release.__invalid ? loaded.release : {}),
1922
- schemaVersion: SDD_SCHEMA_VERSION,
1923
- changeId: loaded.changeId,
1924
- mainlinePolicy: 'publish-from-main-v1',
1925
2007
  releaseSourceRevision,
1926
2008
  };
1927
- if (JSON.stringify(release) !== JSON.stringify(loaded.release)) {
1928
- writeJsonFile(loaded.releaseFile, release);
1929
- }
1930
- return release;
1931
2009
  }
1932
2010
 
1933
- function validateReleaseManifest(release, releasePlan, legacyMode = false) {
2011
+ function validateReleaseManifest(
2012
+ release,
2013
+ releasePlan,
2014
+ legacyMode = false,
2015
+ options = {}
2016
+ ) {
1934
2017
  const errors = [];
1935
2018
  const warnings = [];
1936
2019
  if (!release || release.__invalid) {
@@ -1942,9 +2025,14 @@ function validateReleaseManifest(release, releasePlan, legacyMode = false) {
1942
2025
  else errors.push(issue);
1943
2026
  return { errors, warnings };
1944
2027
  }
1945
- const targets = releasePlan?.targets || release.targets || {};
1946
- if (releasePlan?.targets) {
1947
- const releaseTargetCoverage = affectedCoversTargets(release.targets || {}, releasePlan.targets);
2028
+ const plannedTargets =
2029
+ releasePlan?.activationTargets || releasePlan?.targets || null;
2030
+ const targets = plannedTargets || release.targets || {};
2031
+ if (plannedTargets) {
2032
+ const releaseTargetCoverage = affectedCoversTargets(
2033
+ release.targets || {},
2034
+ plannedTargets
2035
+ );
1948
2036
  if (!releaseTargetCoverage.covered) {
1949
2037
  errors.push({
1950
2038
  name: 'sdd-release-targets-uncovered',
@@ -1955,10 +2043,18 @@ function validateReleaseManifest(release, releasePlan, legacyMode = false) {
1955
2043
  if (hasHighRiskTargets(targets) && !Array.isArray(release.commands)) {
1956
2044
  errors.push({ name: 'sdd-release-commands-missing', message: 'release.json commands 必须是数组' });
1957
2045
  }
1958
- if (releasePlan?.commands?.length && (!Array.isArray(release.commands) || release.commands.length === 0)) {
2046
+ if (
2047
+ !options.actualExecution &&
2048
+ releasePlan?.commands?.length &&
2049
+ (!Array.isArray(release.commands) || release.commands.length === 0)
2050
+ ) {
1959
2051
  errors.push({ name: 'sdd-release-commands-empty', message: 'release.json 未记录发布命令' });
1960
2052
  }
1961
- if (!legacyMode && Array.isArray(release.commands)) {
2053
+ if (
2054
+ !legacyMode &&
2055
+ Array.isArray(release.commands) &&
2056
+ release.commands.length > 0
2057
+ ) {
1962
2058
  errors.push(
1963
2059
  ...validateAtomicReleaseCommands(
1964
2060
  release.commands,
@@ -2288,12 +2384,25 @@ function verifySddChange(options = {}) {
2288
2384
  if (strictDocumentation) errors.push(issue);
2289
2385
  else warnings.push(issue);
2290
2386
  }
2387
+ const actualExecution = options.releasePlan?.actualCommands === true;
2291
2388
  const release =
2292
- options.releasePlan && loaded.release && !loaded.release.__invalid
2293
- ? buildUpdatedReleaseManifest(loaded, options.releasePlan, verificationStage)
2389
+ options.releasePlan &&
2390
+ loaded.release &&
2391
+ !loaded.release.__invalid &&
2392
+ !actualExecution
2393
+ ? buildUpdatedReleaseManifest(
2394
+ loaded,
2395
+ options.releasePlan,
2396
+ verificationStage
2397
+ )
2294
2398
  : loaded.release;
2295
2399
  if (stagePolicy.requireRelease) {
2296
- const releaseValidation = validateReleaseManifest(release, options.releasePlan, legacyMode);
2400
+ const releaseValidation = validateReleaseManifest(
2401
+ release,
2402
+ options.releasePlan,
2403
+ legacyMode,
2404
+ { actualExecution }
2405
+ );
2297
2406
  errors.push(...releaseValidation.errors);
2298
2407
  warnings.push(...releaseValidation.warnings);
2299
2408
  const reviewedPlannedCommands = loaded.meta?.release?.plannedCommands;
@@ -2347,11 +2456,28 @@ function verifySddChange(options = {}) {
2347
2456
  updatedAt: verifiedAt,
2348
2457
  };
2349
2458
  let metadataWritten = false;
2459
+ let attestationFile = null;
2350
2460
  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);
2461
+ attestationFile = writeReleaseAttestation(
2462
+ options.cwd || process.cwd(),
2463
+ loaded.changeId,
2464
+ `verification-${verificationStage}.json`,
2465
+ {
2466
+ schemaVersion: SDD_SCHEMA_VERSION,
2467
+ changeId: loaded.changeId,
2468
+ stage: verificationStage,
2469
+ passed,
2470
+ verifiedAt,
2471
+ planHash: releasePlanHash({
2472
+ targets,
2473
+ commands: release?.commands || [],
2474
+ changedFiles:
2475
+ options.releasePlan?.changedFiles ||
2476
+ loaded.release?.changedFiles ||
2477
+ [],
2478
+ }),
2479
+ }
2480
+ );
2355
2481
  metadataWritten = true;
2356
2482
  }
2357
2483
  return {
@@ -2365,6 +2491,8 @@ function verifySddChange(options = {}) {
2365
2491
  blockingErrors: errors,
2366
2492
  passed,
2367
2493
  metadataWritten,
2494
+ trackedMetadataWritten: false,
2495
+ attestationFile,
2368
2496
  targets,
2369
2497
  highRiskFiles,
2370
2498
  stage: verificationStage,
@@ -2559,6 +2687,15 @@ function syncSddChange(options = {}) {
2559
2687
 
2560
2688
  function archiveSddChange(options = {}) {
2561
2689
  const releaseLoaded = loadSddChange(options);
2690
+ const sourceRevisionAttestation = readReleaseAttestation(
2691
+ options.cwd || process.cwd(),
2692
+ releaseLoaded.changeId,
2693
+ 'source-revision.json'
2694
+ );
2695
+ const releaseSourceRevision =
2696
+ sourceRevisionAttestation?.releaseSourceRevision ||
2697
+ releaseLoaded.release?.releaseSourceRevision ||
2698
+ null;
2562
2699
  const coverageTargets = normalizeCoverageResources(releaseLoaded.coverage, {});
2563
2700
  const releaseTargets = mergeAffectedScopes(
2564
2701
  options.targets,
@@ -2595,20 +2732,20 @@ function archiveSddChange(options = {}) {
2595
2732
  classifyHighRiskFiles(releaseTargets.files).length > 0 ||
2596
2733
  metadataDeclaresHighRisk ||
2597
2734
  Boolean(releaseLoaded.release?.mainlinePolicy) ||
2598
- Boolean(releaseLoaded.release?.releaseSourceRevision);
2735
+ Boolean(releaseSourceRevision);
2599
2736
  if (requiresMainlineClosure) {
2600
2737
  if (
2601
2738
  !releaseLoaded.release ||
2602
2739
  releaseLoaded.release.__invalid ||
2603
- !releaseLoaded.release.releaseSourceRevision
2740
+ !releaseSourceRevision
2604
2741
  ) {
2605
2742
  const error = new Error(
2606
- 'SDD_RELEASE_SOURCE_LINEAGE_REQUIRED: 高风险 change 归档必须保留 release.json 的冻结发布提交;--force 不能绕过主分支回合证据'
2743
+ 'SDD_RELEASE_SOURCE_LINEAGE_REQUIRED: 高风险 change 归档必须保留冻结发布提交的本地 attestation;--force 不能绕过主分支回合证据'
2607
2744
  );
2608
2745
  error.code = 'SDD_RELEASE_SOURCE_LINEAGE_REQUIRED';
2609
2746
  throw error;
2610
2747
  }
2611
- assertReleaseSourceIntegrated(releaseLoaded.release.releaseSourceRevision, {
2748
+ assertReleaseSourceIntegrated(releaseSourceRevision, {
2612
2749
  cwd: options.cwd || process.cwd(),
2613
2750
  });
2614
2751
  }
@@ -82,16 +82,14 @@ React SPA resources/runtime:
82
82
  # In the clean main checkout:
83
83
  openxiangda sdd bundle <release-change> --changes <change-a,change-b>
84
84
  # 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>
85
+ openxiangda release publish --change <release-change> --profile <name>
90
86
  ```
91
87
 
92
88
  `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
89
 
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.
90
+ `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.
91
+
92
+ `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
93
 
96
94
  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
95
 
@@ -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,12 @@ 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
+ `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
98
 
101
99
  `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
100
 
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.172",
3
+ "version": "1.0.173",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -86,6 +86,7 @@
86
86
  "test:publish-lease-state": "node scripts/publish-lease-state-smoke.mjs",
87
87
  "test:publish-lease-heartbeat": "node scripts/publish-lease-heartbeat-smoke.mjs",
88
88
  "test:release-telemetry": "node scripts/release-telemetry-smoke.mjs",
89
+ "test:release-plan": "node scripts/release-plan-smoke.mjs",
89
90
  "test:release-mainline": "node scripts/release-mainline-smoke.mjs",
90
91
  "test:publish-lease-cli": "node scripts/publish-lease-cli-smoke.mjs",
91
92
  "test:publish-context": "node scripts/publish-context-smoke.mjs",
@@ -264,6 +264,9 @@ interface AppFunctionNotificationApi {
264
264
  interface OrganizationCapabilities {
265
265
  appType: string;
266
266
  permissionCode: "app:organization:manage" | string;
267
+ readPermissionCode: "app:organization:read" | string;
268
+ managePermissionCode: "app:organization:manage" | string;
269
+ canRead: boolean;
267
270
  canManage: boolean;
268
271
  actor?: Record<string, unknown>;
269
272
  runtimeServicePrincipalRequiresAuditActor: boolean;
@@ -264,6 +264,9 @@ interface AppFunctionNotificationApi {
264
264
  interface OrganizationCapabilities {
265
265
  appType: string;
266
266
  permissionCode: "app:organization:manage" | string;
267
+ readPermissionCode: "app:organization:read" | string;
268
+ managePermissionCode: "app:organization:manage" | string;
269
+ canRead: boolean;
267
270
  canManage: boolean;
268
271
  actor?: Record<string, unknown>;
269
272
  runtimeServicePrincipalRequiresAuditActor: boolean;
@@ -9,7 +9,7 @@
9
9
  - 一个开发任务只使用一个显式 change,并从 `openxiangda sdd context --change <change> --changed --json` 开始;结构化 approval、coverage 资源和文件范围是硬约束,任务/证据/规格文案默认只告警,不阻断开发或发布。只有明确配置 `strictDocumentation: true` 才把文案完成度恢复为门禁。
10
10
  - 多会话开发使用独立 Git worktree/branch,但 feature worktree 不发布。把已批准提交合并并 push 到权威默认主分支后,用 `sdd bundle <release-change> --changes ...` 聚合范围;只从与远端 tip 完全一致的 clean `main`/`master` 一次发布。
11
11
  - 账号、角色、权限、数据范围、组织账号、RBAC、查询参数授权需求必须先运行 `openxiangda design gates --topic permissions --json`,选择 `managed-platform-account` / `existing-platform-user-assignment` / `static-role-permission` / `query-param-context`,输出权限矩阵后再实现。
12
- - 应用角色如果要创建角色、分配角色成员、给角色授接口权限、维护页面/表单权限组或管理组织账号,必须在角色资源声明 `apiPermissionCodes`,例如 `app:role:manage`、`app:page-permission-group:manage`、`app:form-permission-group:manage`、`app:organization:manage`。
12
+ - 应用角色只读查询组织账号时声明 `app:organization:read`;创建、修改账号/部门或重置密码时声明 `app:organization:manage`。创建角色、分配成员或维护权限组也必须在角色资源的 `apiPermissionCodes` 声明对应的 `app:role:manage`、`app:page-permission-group:manage`、`app:form-permission-group:manage`。
13
13
  - 默认用户界面保持克制:左侧应用导航、顶部账号信息、首页内容区域。
14
14
  - 不在默认可见页面展示 SDK、Runtime、Cookie、Proxy、Playwright、AI 验证、调试上下文、构建号等开发语言。
15
15
  - 使用 React Router 管理路由,路由定义在 `src/app/router.tsx`。
@@ -17,7 +17,7 @@
17
17
  - 菜单和首页文案优先改 `src/app/navigation.ts`、`src/app/starter-content.ts` 与 `src/pages/admin/AdminDashboardPage.tsx`。
18
18
  - 页面、表单、字段、数据范围、流程动作、文件、连接器权限以后端接口为准;前端只做展示保护和清晰状态页。
19
19
  - 查询参数只能作为上下文、筛选或 ticket 输入,不能作为敏感数据授权依据;敏感读写必须由 public-access grants、平台角色、页面/表单权限组或 App Function 后端校验保护。
20
- - 应用管理平台账号/部门时,必须走 `sdk.organization` / `ctx.organization`,并要求当前操作者具备 `app:organization:manage`。
20
+ - 应用查询平台账号/部门时必须走 `sdk.organization` / `ctx.organization`,当前操作者至少具备 `app:organization:read`;写入和密码操作必须具备 `app:organization:manage`。
21
21
  - 只给用户分配“管理员”业务角色不等于授权其设置角色;缺少 `app:role:manage` 时角色创建、成员分配和角色接口权限授予会被后端拒绝。
22
22
  - 本地开发通过 Vite `/service` 代理远端平台,保持 HttpOnly Cookie 同域访问。
23
23
  - 新公开访问页使用 `/view/:appType/public/*`、`src/resources/routes/`、`src/resources/public-access/` 和 `PublicAccessGate`。不要使用旧 `?publicAccess=guest`。
@@ -40,10 +40,7 @@ openxiangda sdd context --change <change> --changed --json
40
40
  openxiangda sdd verify <change> --changed --stage implementation
41
41
  openxiangda resource plan <type> --only <code1,code2> --profile <name>
42
42
  openxiangda sdd bundle <release-change> --changes <change-a,change-b>
43
- openxiangda sdd verify <release-change> --changed --stage prepublish --profile <name>
44
- openxiangda release begin --change <release-change> --profile <name>
45
- openxiangda release integration-status --profile <name>
46
- openxiangda release end --profile <name>
43
+ openxiangda release publish --change <release-change> --profile <name>
47
44
  openxiangda commands --json
48
45
  ```
49
46
 
@@ -56,11 +53,8 @@ openxiangda commands --json
56
53
  ```bash
57
54
  # 先合并所有 approved task commits 到 main/master 并 push
58
55
  openxiangda sdd bundle <release-change> --changes <change-a,change-b>
59
- # commit/push bundle 后执行 verify,随后按生成的唯一 canonical command set 暂存并原子激活
60
- openxiangda sdd verify <release-change> --changed --stage prepublish --profile <name>
61
- openxiangda release begin --change <release-change> --profile <name>
62
- openxiangda release integration-status --profile <name>
63
- openxiangda release end --profile <name>
56
+ # commit/push bundle 后由一个可恢复命令完成 verify、排队、暂存和原子激活
57
+ openxiangda release publish --change <release-change> --profile <name>
64
58
  ```
65
59
 
66
60
  `pnpm build-js-code` 会检查并打包 `src/js-code-nodes/<code>/index.ts`、`src/automations/<code>/index.ts`、`src/functions/<code>/index.ts`,供 JS_CODE V2、代码自动化和 App Function 资源发布使用。批量目标使用 `pnpm build-js-code --scripts functions:a,functions:b,automations:c`,也兼容重复的 `--script a --script b --source functions`。正式 `resource plan/publish` 由 CLI 内置 scoped builder 一次批量构建选中入口及其传递/shared/ambient 依赖,不再为每个资源启动工作区 `pnpm`。缓存写入 `.openxiangda/build-cache.cli-v4.json`,稳定 `source_lineage_v1` 只按 authored source/dependencies 比较,不因构建器升级制造假冲突;需要强制重建时追加 `--force`。