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 +1 -1
- package/lib/cli.js +66 -19
- package/lib/design-gates.js +1 -1
- package/lib/sdd.js +783 -101
- package/openxiangda-skills/SKILL.md +1 -1
- package/openxiangda-skills/references/permission-design-patterns.md +5 -3
- package/openxiangda-skills/references/permissions-settings.md +6 -4
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +7 -3
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +2 -2
- package/package.json +1 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +1 -1
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +1 -1
- package/templates/openxiangda-react-spa/AGENTS.md +1 -1
- package/templates/openxiangda-react-spa/app-workspace.config.ts +1 -0
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +1 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +1 -1
- package/templates/sy-lowcode-app-workspace/AGENTS.md +3 -3
- package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +1 -0
- package/templates/sy-lowcode-app-workspace/src/types/app-workspace.types.ts +1 -0
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>`.
|
|
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
|
-
' -
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
|
|
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(),
|
package/lib/design-gates.js
CHANGED
|
@@ -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',
|