openxiangda 1.0.138 → 1.0.140

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 CHANGED
@@ -82,7 +82,7 @@ Domestic npm mirrors may lag and return an older OpenXiangda version. `openxiang
82
82
 
83
83
  Architecture-class requests are plan-gated by default. For new apps, complex pages, login/register, public/no-login access, role/data-scope design, workflow/automation, App Function, connector, notification, and external integration work, AI agents must plan first and implement only after the user confirms the design.
84
84
 
85
- High-risk implementation is also SDD-gated. New workspaces include `openspec/` by default. Before changing forms, pages, resources, functions, automations, workflows, JS_CODE nodes, runtime code, or `app-workspace.config.ts`, run `openxiangda sdd context --json`, create or continue a change with `openxiangda sdd propose <change>`, and wait for `openxiangda sdd approve <change>`. Release checks fail when high-risk targets are not covered by an approved active change; emergency releases can use `--sdd-bypass --reason "..."`, which writes `openspec/changes/bypass-log.md`.
85
+ High-risk implementation is also SDD-gated. New workspaces include OpenXiangda SDD v2 records under `openspec/` by default. Before changing forms, pages, resources, functions, automations, workflows, JS_CODE nodes, runtime code, or `app-workspace.config.ts`, run `openxiangda sdd context --json`, create or continue a change with `openxiangda sdd propose <change>`, and wait for `openxiangda sdd approve <change>`. `approve` only opens the implementation gate; release checks also require `coverage.json`, `release.json`, `evidence.md`, `tasks.md`, and delta specs to pass `openxiangda sdd verify <change> --changed`. Emergency releases can use `--sdd-bypass --reason "..."`, which writes `openspec/changes/bypass-log.md` and appends bypass evidence to the active approved change when one exists.
86
86
 
87
87
  Before confirmation, agents may read, inspect, snapshot, dry-run, ask questions, and output/write the architecture document. They must not edit source files, mutate platform resources, publish, deploy, send notifications, or call live write/delete endpoints.
88
88
 
package/lib/cli.js CHANGED
@@ -127,7 +127,7 @@ Usage:
127
127
  openxiangda doctor [--profile name] [--app-type APP_XXX] [--release] [--json]
128
128
  openxiangda contract list|explain <contract-name> [--json]
129
129
  openxiangda design gates|template|review [--topic code] [--json]
130
- openxiangda sdd init|propose|approve|status|context|verify|sync|archive [change] [--json]
130
+ openxiangda sdd init|migrate|propose|approve|status|context|verify|sync|archive [change] [--json]
131
131
  openxiangda env [--profile name]
132
132
  openxiangda workspace init [dir] [--name package-name] [--runtime legacy|react-spa] [--install] [--profile name --app-type APP_XXX]
133
133
  openxiangda workspace init [dir] --profile <name> --app-name <app-name> [--runtime legacy|react-spa] [--install]
@@ -531,26 +531,28 @@ async function sdd(args) {
531
531
  const { flags, positional } = parseArgs(rest);
532
532
  if (wantsSubcommandHelp(subcommand, flags)) {
533
533
  print([
534
- '用法: openxiangda sdd init|propose|approve|status|context|verify|sync|archive [change] [--json]',
534
+ '用法: openxiangda sdd init|migrate|propose|approve|status|context|verify|sync|archive [change] [--json]',
535
535
  '常用流程:',
536
- ' openxiangda sdd init',
537
- ' openxiangda sdd propose add-customer-list --title "客户列表" --forms customer --pages customer_list',
536
+ ' openxiangda sdd init --upgrade',
537
+ ' openxiangda sdd propose add-customer-list --title "客户列表" --forms customer --pages customer_list --resources',
538
538
  ' openxiangda sdd approve add-customer-list --summary "用户确认设计"',
539
539
  ' openxiangda sdd context --json',
540
540
  ' openxiangda sdd verify add-customer-list --changed --json',
541
541
  ' openxiangda sdd archive add-customer-list',
542
542
  '说明:',
543
543
  ' - SDD 记录保存在 openspec/,不依赖官方 OpenSpec CLI。',
544
- ' - 高风险发布需要 approved change 覆盖本次 forms/pages/resources/runtime 目标。',
544
+ ' - 新工作区使用 openxiangda-sdd-v2:proposal/specs/design/tasks/evidence 给人读,coverage.json/release.json 给 CLI 校验。',
545
+ ' - 高风险发布需要 approved change 的 coverage 覆盖本次 forms/pages/resources/runtime 目标和高风险文件。',
545
546
  ].join('\n'));
546
547
  return;
547
548
  }
548
549
 
549
- if (subcommand === 'init') {
550
+ if (subcommand === 'init' || subcommand === 'migrate' || subcommand === 'upgrade') {
550
551
  const result = initSddWorkspace({
551
552
  cwd: process.cwd(),
552
553
  path: flags.path,
553
554
  force: Boolean(flags.force),
555
+ upgrade: Boolean(flags.upgrade || subcommand === 'migrate' || subcommand === 'upgrade'),
554
556
  });
555
557
  if (flags.json) return writeJson(result);
556
558
  printSddInitReport(result);
@@ -586,13 +588,17 @@ async function sdd(args) {
586
588
  }
587
589
 
588
590
  if (subcommand === 'status' || subcommand === 'context') {
589
- const status = buildSddStatus({ cwd: process.cwd(), configText: readWorkspaceConfigText() });
590
591
  const releasePlan = buildWorkspaceReleasePlan({
591
592
  changed: Boolean(flags.changed || flags['changed-only']),
592
593
  since: flags.since,
593
594
  profile: flags.profile || '<profile>',
594
595
  skipSdd: true,
595
596
  });
597
+ const status = buildSddStatus({
598
+ cwd: process.cwd(),
599
+ configText: readWorkspaceConfigText(),
600
+ releasePlan,
601
+ });
596
602
  const result = {
597
603
  ...status,
598
604
  releasePlan,
@@ -667,7 +673,7 @@ async function sdd(args) {
667
673
  return;
668
674
  }
669
675
 
670
- fail('用法: openxiangda sdd init|propose|approve|status|context|verify|sync|archive [change] [--json]');
676
+ fail('用法: openxiangda sdd init|migrate|propose|approve|status|context|verify|sync|archive [change] [--json]');
671
677
  }
672
678
 
673
679
  function parseSddAffectedFlags(flags) {
@@ -681,6 +687,11 @@ function parseSddAffectedFlags(flags) {
681
687
  return {
682
688
  forms: split(flags.forms || flags.form),
683
689
  pages: split(flags.pages || flags.page),
690
+ functions: split(flags.functions || flags.function),
691
+ automations: split(flags.automations || flags.automation),
692
+ workflows: split(flags.workflows || flags.workflow),
693
+ jsCodeNodes: split(flags['js-code-nodes'] || flags.jsCodeNodes || flags['js-code']),
694
+ files: split(flags.files || flags.file),
684
695
  resources: Boolean(flags.resources),
685
696
  runtime: Boolean(flags.runtime),
686
697
  backend: Boolean(flags.backend),
@@ -689,7 +700,7 @@ function parseSddAffectedFlags(flags) {
689
700
  }
690
701
 
691
702
  function printSddInitReport(result) {
692
- const lines = [`OpenXiangda SDD initialized: ${result.path}`];
703
+ const lines = [`OpenXiangda SDD initialized: ${result.path}`, `schemaVersion: ${result.schemaVersion || 'unknown'}`];
693
704
  for (const op of result.operations) {
694
705
  lines.push(`- ${op.action}: ${op.path}`);
695
706
  }
@@ -700,12 +711,18 @@ function printSddStatusReport(result) {
700
711
  const lines = [
701
712
  `OpenXiangda SDD status: ${result.governance.initialized ? 'initialized' : 'not initialized'}`,
702
713
  `path: ${result.governance.path}`,
714
+ `schemaVersion: ${result.schemaVersion || result.governance.schemaVersion || 'unknown'}`,
703
715
  `strictHighRisk: ${result.governance.strictHighRisk ? 'true' : 'false'}`,
704
716
  `active changes: ${result.activeChanges.length}`,
705
717
  ];
706
718
  for (const change of result.activeChanges) {
707
719
  lines.push(`- ${change.id}: ${change.status}${change.approvedAt ? ` approvedAt=${change.approvedAt}` : ''}`);
708
720
  }
721
+ if (result.activeChange) lines.push(`activeChange: ${result.activeChange.id}`);
722
+ if (result.blockingErrors?.length) {
723
+ lines.push('blockingErrors:');
724
+ for (const error of result.blockingErrors) lines.push(`- ${error.name}: ${error.message}`);
725
+ }
709
726
  if (result.bypassLog) lines.push(`bypass log: ${result.bypassLog}`);
710
727
  print(lines.join('\n'));
711
728
  }
@@ -714,6 +731,7 @@ function printSddVerifyReport(result) {
714
731
  const lines = [
715
732
  `OpenXiangda SDD verify: ${result.passed ? 'passed' : 'failed'}`,
716
733
  `change: ${result.change.id}`,
734
+ `schemaVersion: ${result.schemaVersion || result.change.schemaVersion || 'unknown'}`,
717
735
  ];
718
736
  if (result.errors.length > 0) {
719
737
  lines.push('Errors:');
@@ -4629,7 +4647,10 @@ async function resource(args) {
4629
4647
  assertSddReleaseGate(
4630
4648
  buildExplicitSddReleasePlan(
4631
4649
  { forms: [], pages: [], resources: true, runtime: false, other: [] },
4632
- { files: getWorkspaceChangedFiles() }
4650
+ {
4651
+ files: getWorkspaceChangedFiles(),
4652
+ commands: [`openxiangda resource publish --profile ${profileName}`],
4653
+ }
4633
4654
  ),
4634
4655
  flags,
4635
4656
  'openxiangda resource publish'
@@ -4696,7 +4717,10 @@ async function runtime(args) {
4696
4717
  assertSddReleaseGate(
4697
4718
  buildExplicitSddReleasePlan(
4698
4719
  { forms: [], pages: [], resources: false, runtime: true, other: [] },
4699
- { files: getWorkspaceChangedFiles() }
4720
+ {
4721
+ files: getWorkspaceChangedFiles(),
4722
+ commands: [`openxiangda runtime deploy --profile ${target.profileName}`],
4723
+ }
4700
4724
  ),
4701
4725
  flags,
4702
4726
  'openxiangda runtime deploy'
@@ -5313,7 +5337,7 @@ async function commands(args) {
5313
5337
  'doctor [--profile name] [--app-type APP_XXX] [--release]',
5314
5338
  'contract list|explain <contract-name>',
5315
5339
  'design gates|template|review [--topic code]',
5316
- 'sdd init|propose|approve|status|context|verify|sync|archive',
5340
+ 'sdd init|migrate|propose|approve|status|context|verify|sync|archive',
5317
5341
  'env',
5318
5342
  'workspace init|bind|plan|check|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
5319
5343
  'app list|create|snapshot',
@@ -5347,7 +5371,7 @@ async function commands(args) {
5347
5371
  'public-access is the new React SPA public policy resource for /view/:appType/public/* routes; page code should use PublicAccessGate, and Page SDK hooks must stay inside OpenXiangdaProvider + OpenXiangdaPageProvider.',
5348
5372
  'settings public-access is legacy form public-access compatibility/repair and should not be used for new React SPA apps.',
5349
5373
  'Direct live mutation commands should use --dry-run first and --write-manifest when the repository should remain source of truth.',
5350
- 'OpenXiangda native SDD uses openspec/ records; high-risk publishes require an approved change unless --sdd-bypass --reason is used.',
5374
+ 'OpenXiangda native SDD uses openspec/ records; high-risk publishes require an approved change with coverage.json/release.json verification unless --sdd-bypass --reason is used.',
5351
5375
  ],
5352
5376
  };
5353
5377
  if (flags.json) return writeJson(manifest);
@@ -7079,6 +7103,12 @@ function validateResourceItem(kind, item, errors, warnings) {
7079
7103
  if (kind === 'scopeGrantSources') {
7080
7104
  if (!item.name) warnings.push(`${label}: 未声明 name,将使用 code`);
7081
7105
  if (!item.sourceFormUuid && !item.formUuid) errors.push(`${label}: 缺少 sourceFormUuid`);
7106
+ if (
7107
+ item.syncMode !== undefined &&
7108
+ !['on_write', 'manual'].includes(String(item.syncMode))
7109
+ ) {
7110
+ errors.push(`${label}: syncMode 只能是 on_write 或 manual`);
7111
+ }
7082
7112
  const subjectMappings = item.subjectMappings || item.subjects;
7083
7113
  const dimensionMappings = item.dimensionMappings || item.dimensions;
7084
7114
  if (!Array.isArray(subjectMappings) || subjectMappings.length === 0) {
@@ -7159,6 +7189,11 @@ function validateScopePermissionResources(manifest, errors, warnings) {
7159
7189
  .map(String)
7160
7190
  );
7161
7191
  const scopePolicyRefs = [];
7192
+ for (const source of manifest.scopeGrantSources || []) {
7193
+ if (String(source.syncMode || '').trim() === 'manual') {
7194
+ warnings.push(`${resourceLabel('scopeGrantSources', source)}: syncMode=manual,运行时来源表单变更不会自动物化;请确认有显式 scope sync 作业`);
7195
+ }
7196
+ }
7162
7197
  const inspectPermissionGroup = (kind, item) => {
7163
7198
  const groups = kind === 'dataViews'
7164
7199
  ? (Array.isArray(item.permissionGroups) ? item.permissionGroups : [])
@@ -10610,10 +10645,20 @@ function normalizeScopeGrantSourceManifest(source, bound) {
10610
10645
  effectiveFromField: source.effectiveFromField,
10611
10646
  effectiveToField: source.effectiveToField,
10612
10647
  filterJson: source.filterJson || source.filter,
10648
+ syncMode: normalizeScopeGrantSourceSyncMode(source.syncMode, source.sync),
10613
10649
  enabled: source.enabled !== false,
10614
10650
  });
10615
10651
  }
10616
10652
 
10653
+ function normalizeScopeGrantSourceSyncMode(value, legacySync) {
10654
+ const raw = String(value || '').trim().toLowerCase();
10655
+ if (['manual', 'off', 'none', 'disabled', 'false'].includes(raw)) {
10656
+ return 'manual';
10657
+ }
10658
+ if (legacySync === false) return 'manual';
10659
+ return 'on_write';
10660
+ }
10661
+
10617
10662
  function normalizeDataScopePolicyManifest(policy) {
10618
10663
  const code = policy.code || policy.resourceCode;
10619
10664
  return stripUndefinedValues({
@@ -12346,6 +12391,7 @@ function buildWorkspaceReleasePlan(options = {}) {
12346
12391
  ? getWorkspaceChangedFiles({ since: options.since })
12347
12392
  : getAllWorkspaceSourceFiles();
12348
12393
  const targets = classifyWorkspaceFiles(files, runtimeMode);
12394
+ if (options.includeResources) targets.resources = true;
12349
12395
  const issues = scanWorkspaceReliabilityIssues(files);
12350
12396
  const errors = issues
12351
12397
  .filter(issue => issue.severity === 'error')
@@ -12361,6 +12407,7 @@ function buildWorkspaceReleasePlan(options = {}) {
12361
12407
  configText,
12362
12408
  targets,
12363
12409
  files,
12410
+ commands,
12364
12411
  });
12365
12412
  const sddErrors = sdd ? sdd.errors.map(error => ({ name: error.name, message: error.message })) : [];
12366
12413
  const checks = [
@@ -12450,14 +12497,17 @@ function printWorkspaceReleasePlan(plan) {
12450
12497
 
12451
12498
  function buildExplicitSddReleasePlan(targets, options = {}) {
12452
12499
  const configText = readWorkspaceConfigText();
12500
+ const commands = options.commands || [];
12453
12501
  const sdd = buildSddGovernanceReport({
12454
12502
  cwd: process.cwd(),
12455
12503
  configText,
12456
12504
  targets,
12457
12505
  files: options.files || [],
12506
+ commands,
12458
12507
  });
12459
12508
  return {
12460
12509
  targets,
12510
+ commands,
12461
12511
  sdd,
12462
12512
  errors: sdd.errors.map(error => ({ name: error.name, message: error.message })),
12463
12513
  };
@@ -12482,15 +12532,12 @@ function assertSddReleaseGate(plan, flags, command) {
12482
12532
 
12483
12533
  function buildWorkspacePublishSddPlan(publishOptions, runtimeMode, profile) {
12484
12534
  if (publishOptions.changed || (!publishOptions.targetForm && !publishOptions.targetPage && !publishOptions.only)) {
12485
- const plan = buildWorkspaceReleasePlan({
12535
+ return buildWorkspaceReleasePlan({
12486
12536
  changed: publishOptions.changed,
12487
12537
  since: publishOptions.since,
12488
12538
  profile,
12539
+ includeResources: publishOptions.includeResources,
12489
12540
  });
12490
- if (publishOptions.includeResources) {
12491
- plan.targets.resources = true;
12492
- }
12493
- return plan;
12494
12541
  }
12495
12542
  const targets = {
12496
12543
  forms: [],
@@ -12515,7 +12562,7 @@ function buildWorkspacePublishSddPlan(publishOptions, runtimeMode, profile) {
12515
12562
  targets.forms = unique(targets.forms).sort();
12516
12563
  targets.pages = unique(targets.pages).sort();
12517
12564
  const commands = buildWorkspaceReleaseCommands(targets, runtimeMode, profile);
12518
- const sddPlan = buildExplicitSddReleasePlan(targets, { files: [] });
12565
+ const sddPlan = buildExplicitSddReleasePlan(targets, { files: [], commands });
12519
12566
  return {
12520
12567
  workspace: {
12521
12568
  cwd: process.cwd(),
@@ -135,7 +135,7 @@ const DESIGN_GATE_TOPICS = [
135
135
  '正式后台优先选择 managed-platform-account;如果平台账号已存在,选择 existing-platform-user-assignment;固定内部门户选择 static-role-permission',
136
136
  'query-param-context 仅用于低风险页面上下文、筛选条件或公开 ticket 输入,不作为敏感数据授权依据',
137
137
  '角色写 src/resources/roles,页面组写 permissions/page-groups,表单组写 permissions/form-groups,显式声明 actions/dataPermission/fieldAccessPolicy;平台内部仍兼容存储为 operations',
138
- '复杂业务范围优先写 permissions/scope-dimensions、permissions/scope-grant-sources、permissions/data-scope-policies,并在表单权限组或 data-view 权限组使用 dataPermission.type=scope_policy',
138
+ '复杂业务范围优先写 permissions/scope-dimensions、permissions/scope-grant-sources、permissions/data-scope-policies,并在表单权限组或 data-view 权限组使用 dataPermission.type=scope_policy;授权来源默认 syncMode=on_write,由平台在来源表单写入后自动物化',
139
139
  'scope_policy 只表达数据范围,actions 表达操作能力;按钮隐藏只是 UX,后端 action check 才是权威',
140
140
  'scope_policy 默认语义是个人授权 + 当前应用角色授权;多维 rules 显式 AND;空授权集合拒绝;管理员绕过但可审计',
141
141
  '能管理角色或给别人分配角色的应用角色必须在 roles manifest 声明 apiPermissionCodes;至少包含 app:role:manage,按需加入 app:page-permission-group:manage、app:form-permission-group:manage、app:organization:manage',