openxiangda 1.0.159 → 1.0.161

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.
Files changed (62) hide show
  1. package/README.md +39 -13
  2. package/lib/change-baseline.js +1077 -0
  3. package/lib/cli.js +12002 -6723
  4. package/lib/config.js +272 -7
  5. package/lib/http.js +2 -0
  6. package/lib/js-code-build.js +61 -0
  7. package/lib/publish-context.js +98 -0
  8. package/lib/publish-lease-heartbeat.js +182 -0
  9. package/lib/publish-lease.js +190 -0
  10. package/lib/release-telemetry.js +182 -0
  11. package/lib/runtime-lineage.js +297 -0
  12. package/lib/sdd.js +567 -35
  13. package/lib/source-dependencies.js +742 -0
  14. package/lib/workspace-init.js +6 -1
  15. package/lib/worktree-owner.js +219 -0
  16. package/openxiangda-skills/SKILL.md +103 -228
  17. package/openxiangda-skills/references/architecture-design.md +2 -1
  18. package/openxiangda-skills/references/openxiangda-api.md +27 -0
  19. package/openxiangda-skills/references/pages/page-sdk.md +11 -1
  20. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +12 -2
  21. package/openxiangda-skills/references/workspace-state.md +2 -0
  22. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +77 -243
  23. package/openxiangda-skills/skills/openxiangda-form/SKILL.md +8 -0
  24. package/openxiangda-skills/skills/openxiangda-page/SKILL.md +18 -0
  25. package/package.json +20 -1
  26. package/packages/sdk/dist/{ProcessPreview-DMkzccq4.d.mts → ProcessPreview-Dux73lY_.d.mts} +5 -0
  27. package/packages/sdk/dist/{ProcessPreview-DMkzccq4.d.ts → ProcessPreview-Dux73lY_.d.ts} +5 -0
  28. package/packages/sdk/dist/components/index.cjs +29 -1
  29. package/packages/sdk/dist/components/index.cjs.map +1 -1
  30. package/packages/sdk/dist/components/index.d.mts +4 -4
  31. package/packages/sdk/dist/components/index.d.ts +4 -4
  32. package/packages/sdk/dist/components/index.mjs +29 -1
  33. package/packages/sdk/dist/components/index.mjs.map +1 -1
  34. package/packages/sdk/dist/{dataManagementApi-C9O-Bb0j.d.ts → dataManagementApi-BNLH2o7f.d.ts} +2 -1
  35. package/packages/sdk/dist/{dataManagementApi-gpZkgRDM.d.mts → dataManagementApi-BOcIx4U-.d.mts} +2 -1
  36. package/packages/sdk/dist/runtime/index.cjs +150 -22
  37. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  38. package/packages/sdk/dist/runtime/index.d.mts +3 -3
  39. package/packages/sdk/dist/runtime/index.d.ts +3 -3
  40. package/packages/sdk/dist/runtime/index.mjs +150 -22
  41. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  42. package/packages/sdk/dist/runtime/react.cjs +121 -21
  43. package/packages/sdk/dist/runtime/react.cjs.map +1 -1
  44. package/packages/sdk/dist/runtime/react.d.mts +15 -2
  45. package/packages/sdk/dist/runtime/react.d.ts +15 -2
  46. package/packages/sdk/dist/runtime/react.mjs +121 -21
  47. package/packages/sdk/dist/runtime/react.mjs.map +1 -1
  48. package/packages/sdk/src/build-source/scripts/register.mjs +54 -23
  49. package/packages/sdk/src/build-source/scripts/sync-schema.mjs +59 -19
  50. package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +242 -2
  51. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +7 -3
  52. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +11 -4
  53. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +7 -3
  54. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +10 -4
  55. package/templates/openxiangda-react-spa/AGENTS.md +25 -12
  56. package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +344 -60
  57. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +5 -1
  58. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +8 -2
  59. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +8 -4
  60. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +8 -2
  61. package/templates/sy-lowcode-app-workspace/AGENTS.md +14 -4
  62. package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +344 -60
package/lib/sdd.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
+ const { readGitSourceBase } = require('./change-baseline');
4
5
 
5
6
  const DEFAULT_SDD_DIR = 'openspec';
6
7
  const SDD_CONFIG_FILE = 'config.yaml';
@@ -11,6 +12,20 @@ const BYPASS_LOG_FILE = path.join('changes', 'bypass-log.md');
11
12
  const SDD_SCHEMA_VERSION = 'openxiangda-sdd-v2';
12
13
  const LEGACY_SDD_SCHEMA_VERSION = 'openxiangda-sdd-v1';
13
14
  const CODE_RESOURCE_TARGETS = ['functions', 'automations', 'workflows', 'jsCodeNodes'];
15
+ const SDD_VERIFY_STAGES = ['implementation', 'prepublish', 'postpublish', 'archive'];
16
+ const QUICK_SDD_RISK_TIER = 'L1';
17
+ const QUICK_SDD_ALLOWED_KINDS = ['copy', 'style', 'binding', 'metadata', 'narrow-fix'];
18
+ const QUICK_FUNCTION_SAFETY_FIELDS = [
19
+ 'schema',
20
+ 'permission',
21
+ 'auth',
22
+ 'publicAccess',
23
+ 'dataMigration',
24
+ 'destructive',
25
+ 'crossResource',
26
+ ];
27
+ const QUICK_SDD_MAX_TARGETS = 3;
28
+ const QUICK_SDD_MAX_FILES = 12;
14
29
 
15
30
  const DEFAULT_SPEC_CONTENT = `# Application Specification
16
31
 
@@ -311,6 +326,165 @@ function normalizeAffected(input = {}) {
311
326
  };
312
327
  }
313
328
 
329
+ function normalizeQuickScopeFile(value) {
330
+ const file = String(value || '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
331
+ if (
332
+ !file ||
333
+ path.isAbsolute(file) ||
334
+ file.split('/').includes('..') ||
335
+ file.endsWith('/') ||
336
+ /[*?\[\]{}]/.test(file)
337
+ ) {
338
+ return null;
339
+ }
340
+ return file;
341
+ }
342
+
343
+ function normalizeQuickChangeKind(value) {
344
+ return String(value || '')
345
+ .trim()
346
+ .toLowerCase()
347
+ .replace(/[\s_]+/g, '-');
348
+ }
349
+
350
+ function normalizeQuickRiskAssessment(value) {
351
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
352
+ return {
353
+ confirmed: value.confirmed === true,
354
+ schema: value.schema,
355
+ permission: value.permission ?? value.permissions,
356
+ auth: value.auth ?? value.authentication,
357
+ publicAccess: value.publicAccess ?? value.public,
358
+ dataMigration: value.dataMigration ?? value.migration,
359
+ destructive: value.destructive,
360
+ crossResource: value.crossResource ?? value.crossResources,
361
+ };
362
+ }
363
+
364
+ function validateQuickSddScope(options = {}) {
365
+ const affected = normalizeAffected(options);
366
+ affected.pages = Array.from(new Set(affected.pages));
367
+ affected.functions = Array.from(new Set(affected.functions));
368
+ affected.files = Array.from(new Set(affected.files.map(normalizeQuickScopeFile).filter(Boolean))).sort();
369
+ if (affected.pages.length > 0) affected.runtime = true;
370
+ const errors = [];
371
+ const requestedRisk = String(
372
+ options.riskTier || options.riskLevel || options.risk || QUICK_SDD_RISK_TIER
373
+ ).trim().toUpperCase();
374
+ if (!['L1', 'LOW'].includes(requestedRisk)) {
375
+ errors.push(`quick change 仅允许 ${QUICK_SDD_RISK_TIER}/low,收到 ${requestedRisk || '(empty)'}`);
376
+ }
377
+
378
+ const changeKind = normalizeQuickChangeKind(options.changeKind || options.kind);
379
+ if (!QUICK_SDD_ALLOWED_KINDS.includes(changeKind)) {
380
+ errors.push(
381
+ `quick change 必须声明可解释的 L1 changeKind:${QUICK_SDD_ALLOWED_KINDS.join(', ')};收到 ${changeKind || '(empty)'}`
382
+ );
383
+ }
384
+ if (
385
+ affected.forms.length > 0 ||
386
+ affected.automations.length > 0 ||
387
+ affected.workflows.length > 0 ||
388
+ affected.jsCodeNodes.length > 0 ||
389
+ affected.resources ||
390
+ affected.backend ||
391
+ affected.contracts
392
+ ) {
393
+ errors.push('quick change 不能覆盖表单、Automation、Workflow、JS_CODE、通用资源、后端或契约变更');
394
+ }
395
+ if (affected.runtime && affected.pages.length === 0) {
396
+ errors.push('quick change 仅允许由精确 page target 引起的 Runtime 发布');
397
+ }
398
+
399
+ const logicalTargets = [...affected.pages, ...affected.functions];
400
+ if (logicalTargets.length === 0) {
401
+ errors.push('quick change 至少需要一个精确 page 或 function target');
402
+ }
403
+ if (logicalTargets.length > QUICK_SDD_MAX_TARGETS) {
404
+ errors.push(`quick change 最多允许 ${QUICK_SDD_MAX_TARGETS} 个逻辑 target`);
405
+ }
406
+ if (logicalTargets.some(item => !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(item))) {
407
+ errors.push('quick change target 必须是精确安全 code,不能包含路径或通配符');
408
+ }
409
+ if (affected.files.length === 0) {
410
+ errors.push('quick change 必须声明精确 files');
411
+ }
412
+ if (affected.files.length > QUICK_SDD_MAX_FILES) {
413
+ errors.push(`quick change 最多允许 ${QUICK_SDD_MAX_FILES} 个文件`);
414
+ }
415
+
416
+ const selectedPages = new Set(affected.pages);
417
+ const selectedFunctions = new Set(affected.functions);
418
+ const riskAssessment = normalizeQuickRiskAssessment(options.riskAssessment);
419
+ if (selectedFunctions.size > 0) {
420
+ if (!riskAssessment?.confirmed) {
421
+ errors.push('Function quick change 必须提供 riskAssessment.confirmed=true 并逐项排除高风险行为');
422
+ }
423
+ for (const field of QUICK_FUNCTION_SAFETY_FIELDS) {
424
+ if (riskAssessment?.[field] !== false) {
425
+ errors.push(`Function quick change 必须明确确认 riskAssessment.${field}=false`);
426
+ }
427
+ }
428
+ if (!['metadata', 'narrow-fix'].includes(changeKind)) {
429
+ errors.push('Function quick change 仅允许 changeKind=metadata 或 narrow-fix');
430
+ }
431
+ } else if (changeKind === 'binding' && riskAssessment?.crossResource !== false) {
432
+ errors.push('binding quick change 必须明确确认 riskAssessment.crossResource=false');
433
+ }
434
+ for (const rawFile of normalizeAffected(options).files) {
435
+ if (!normalizeQuickScopeFile(rawFile)) {
436
+ errors.push(`quick change file 不是精确安全路径: ${rawFile}`);
437
+ }
438
+ }
439
+ for (const file of affected.files) {
440
+ const pageCode = file.match(/^src\/pages\/([^/]+)\//)?.[1];
441
+ const functionCode = file.match(/^src\/functions\/([^/]+)\//)?.[1];
442
+ const functionManifestCode = file.match(/^src\/resources\/functions\/([^/]+)\.json$/)?.[1];
443
+ if (pageCode && selectedPages.has(pageCode)) continue;
444
+ if (functionCode && selectedFunctions.has(functionCode)) continue;
445
+ if (functionManifestCode && selectedFunctions.has(functionManifestCode)) continue;
446
+ errors.push(`quick change file 超出精确 page/function scope: ${file}`);
447
+ }
448
+ for (const pageCode of selectedPages) {
449
+ if (!affected.files.some(file => file.startsWith(`src/pages/${pageCode}/`))) {
450
+ errors.push(`page ${pageCode} 缺少对应精确 file`);
451
+ }
452
+ }
453
+ for (const functionCode of selectedFunctions) {
454
+ const hasSource = affected.files.some(file => file.startsWith(`src/functions/${functionCode}/`));
455
+ const hasManifest = affected.files.includes(`src/resources/functions/${functionCode}.json`);
456
+ if (!hasSource && !hasManifest) {
457
+ errors.push(`function ${functionCode} 缺少对应源码或 function manifest file`);
458
+ }
459
+ }
460
+
461
+ return {
462
+ valid: errors.length === 0,
463
+ errors: Array.from(new Set(errors)),
464
+ affected,
465
+ risk: {
466
+ tier: QUICK_SDD_RISK_TIER,
467
+ level: 'low',
468
+ mode: 'quick',
469
+ changeKind,
470
+ assessment: riskAssessment,
471
+ prohibitedFunctionImpacts: QUICK_FUNCTION_SAFETY_FIELDS,
472
+ exactScope: true,
473
+ maxTargets: QUICK_SDD_MAX_TARGETS,
474
+ maxFiles: QUICK_SDD_MAX_FILES,
475
+ },
476
+ };
477
+ }
478
+
479
+ function assertQuickSddScope(options = {}) {
480
+ const validation = validateQuickSddScope(options);
481
+ if (validation.valid) return validation;
482
+ const error = new Error(`不能创建 quick SDD change: ${validation.errors[0]}`);
483
+ error.code = 'SDD_QUICK_SCOPE_REJECTED';
484
+ error.details = validation.errors;
485
+ throw error;
486
+ }
487
+
314
488
  function extractCodeFromPath(file, prefix) {
315
489
  if (!file.startsWith(prefix)) return null;
316
490
  const rest = file.slice(prefix.length);
@@ -323,7 +497,7 @@ function coversCodeTarget(normalized, key, code) {
323
497
  }
324
498
 
325
499
  function createChangeMetadata(changeId, options = {}) {
326
- return {
500
+ const metadata = {
327
501
  schemaVersion: SDD_SCHEMA_VERSION,
328
502
  id: changeId,
329
503
  title: options.title || changeId,
@@ -344,6 +518,27 @@ function createChangeMetadata(changeId, options = {}) {
344
518
  bypasses: [],
345
519
  },
346
520
  };
521
+ if (options.changeMode) metadata.changeMode = options.changeMode;
522
+ if (options.riskTier) metadata.riskTier = options.riskTier;
523
+ if (options.changeKind) metadata.changeKind = options.changeKind;
524
+ if (options.riskMetadata) metadata.riskMetadata = options.riskMetadata;
525
+ if (options.scopePolicy) metadata.scopePolicy = options.scopePolicy;
526
+ try {
527
+ const sourceBase =
528
+ options.sourceBase ||
529
+ readGitSourceBase(options.cwd || process.cwd(), options.sourceBaseRef || 'HEAD');
530
+ if (sourceBase?.baseCommit && sourceBase?.treeHash) {
531
+ metadata.sourceBase = {
532
+ repo: sourceBase.repo,
533
+ baseCommit: sourceBase.baseCommit,
534
+ treeHash: sourceBase.treeHash,
535
+ };
536
+ }
537
+ } catch {
538
+ // SDD can still be used before a workspace is initialized as a Git repo.
539
+ // Release begin will require a reproducible source base for stale-worktree protection.
540
+ }
541
+ return metadata;
347
542
  }
348
543
 
349
544
  function formatAffectedList(items) {
@@ -406,18 +601,53 @@ function createDesignContent(meta) {
406
601
  `;
407
602
  }
408
603
 
409
- function createTasksContent() {
604
+ function createTasksContent(meta = {}) {
605
+ if (meta.changeMode === 'quick') {
606
+ return [
607
+ '# Tasks',
608
+ '',
609
+ '## Implementation',
610
+ '',
611
+ '- [x] Record exact L1 scope and explicit approval basis',
612
+ '- [ ] Implement the focused change',
613
+ '',
614
+ '## Prepublish',
615
+ '',
616
+ '- [ ] Record implementation checks and dry-run evidence',
617
+ '- [ ] Confirm the exact release plan',
618
+ '',
619
+ '## Postpublish',
620
+ '',
621
+ '- [ ] Record publish/deploy result',
622
+ '',
623
+ '## Archive',
624
+ '',
625
+ '- [ ] Confirm the released change is ready to archive',
626
+ '',
627
+ ].join('\n');
628
+ }
410
629
  return [
411
630
  '# Tasks',
412
631
  '',
632
+ '## Implementation',
633
+ '',
413
634
  '- [ ] Confirm proposal and affected scope',
414
635
  '- [ ] Sharpen delta specs into behavior-first requirements and scenarios',
415
636
  '- [ ] Implement changes',
416
637
  '- [ ] Update coverage.json if affected resources change',
638
+ '',
639
+ '## Prepublish',
640
+ '',
417
641
  '- [ ] Record verification evidence',
418
- '- [ ] Run `openxiangda sdd verify <change> --changed`',
419
642
  '- [ ] Run release plan/check commands',
420
- '- [ ] Archive the change after release',
643
+ '',
644
+ '## Postpublish',
645
+ '',
646
+ '- [ ] Record publish/deploy result',
647
+ '',
648
+ '## Archive',
649
+ '',
650
+ '- [ ] Confirm the released change is ready to archive',
421
651
  '',
422
652
  ].join('\n');
423
653
  }
@@ -428,7 +658,7 @@ function createEvidenceContent() {
428
658
  '',
429
659
  '## Verification',
430
660
  '',
431
- '- [ ] Record `openxiangda sdd verify <change> --changed` result',
661
+ '- [ ] Record implementation test/check output',
432
662
  '- [ ] Record workspace/resource/runtime check or dry-run output',
433
663
  '',
434
664
  '## Release',
@@ -547,7 +777,7 @@ function proposeSddChange(options = {}) {
547
777
  writeJsonFile(path.join(changeDir, RELEASE_FILE), release);
548
778
  fs.writeFileSync(path.join(changeDir, 'proposal.md'), createProposalContent(meta));
549
779
  fs.writeFileSync(path.join(changeDir, 'design.md'), createDesignContent(meta));
550
- fs.writeFileSync(path.join(changeDir, 'tasks.md'), createTasksContent());
780
+ fs.writeFileSync(path.join(changeDir, 'tasks.md'), createTasksContent(meta));
551
781
  fs.writeFileSync(path.join(changeDir, 'evidence.md'), createEvidenceContent());
552
782
  fs.writeFileSync(
553
783
  path.join(changeDir, 'specs', domain, 'spec.md'),
@@ -603,6 +833,8 @@ function approveSddChange(options = {}) {
603
833
  approvedAt: nowIso(),
604
834
  approvedBy: options.approvedBy || os.userInfo().username || 'unknown',
605
835
  approvalSummary: options.summary || options.note || loaded.meta.approvalSummary || '',
836
+ ...(options.approvalMode ? { approvalMode: options.approvalMode } : {}),
837
+ ...(options.approvalBasis ? { approvalBasis: options.approvalBasis } : {}),
606
838
  updatedAt: nowIso(),
607
839
  };
608
840
  writeJsonFile(loaded.metaFile, meta);
@@ -619,6 +851,85 @@ function approveSddChange(options = {}) {
619
851
  };
620
852
  }
621
853
 
854
+ function proposeQuickSddChange(options = {}) {
855
+ const validation = assertQuickSddScope(options);
856
+ return proposeSddChange({
857
+ ...options,
858
+ affected: validation.affected,
859
+ riskLevel: validation.risk.level,
860
+ riskTier: validation.risk.tier,
861
+ changeMode: validation.risk.mode,
862
+ changeKind: validation.risk.changeKind,
863
+ riskMetadata: {
864
+ tier: validation.risk.tier,
865
+ level: validation.risk.level,
866
+ classification: 'explicit-l1-quick',
867
+ changeKind: validation.risk.changeKind,
868
+ exactScope: true,
869
+ assessment: validation.risk.assessment,
870
+ prohibitedFunctionImpacts: validation.risk.prohibitedFunctionImpacts,
871
+ },
872
+ scopePolicy: {
873
+ exact: true,
874
+ allowedTargets: ['pages', 'functions'],
875
+ maxTargets: validation.risk.maxTargets,
876
+ maxFiles: validation.risk.maxFiles,
877
+ },
878
+ });
879
+ }
880
+
881
+ function approveQuickSddChange(options = {}) {
882
+ const loaded = loadSddChange(options);
883
+ if (loaded.meta.changeMode !== 'quick' || loaded.meta.riskTier !== QUICK_SDD_RISK_TIER) {
884
+ throw new Error(`change ${loaded.changeId} 不是可 quick approve 的 L1 change`);
885
+ }
886
+ assertQuickSddScope({
887
+ affected: loaded.meta.affected,
888
+ riskTier: loaded.meta.riskTier,
889
+ changeKind: loaded.meta.changeKind,
890
+ riskAssessment: loaded.meta.riskMetadata?.assessment,
891
+ });
892
+ const summary = String(
893
+ options.summary || options.note || options.userIntent || ''
894
+ ).trim();
895
+ if (!summary) {
896
+ throw new Error('quick approve 需要 summary/userIntent 记录用户的明确变更意图');
897
+ }
898
+ return approveSddChange({
899
+ ...options,
900
+ summary,
901
+ approvalMode: 'quick-explicit-intent',
902
+ approvalBasis: 'explicit-user-request',
903
+ });
904
+ }
905
+
906
+ function createQuickSddChange(options = {}) {
907
+ const summary = String(
908
+ options.approvalSummary || options.summary || options.note || options.userIntent || ''
909
+ ).trim();
910
+ if (!summary) {
911
+ throw new Error('quick change 需要 approvalSummary/summary/userIntent 才能创建并批准');
912
+ }
913
+ assertQuickSddScope(options);
914
+ const proposed = proposeQuickSddChange(options);
915
+ const approved = approveQuickSddChange({
916
+ cwd: options.cwd,
917
+ configText: options.configText,
918
+ changeId: proposed.change.id,
919
+ summary,
920
+ approvedBy: options.approvedBy,
921
+ });
922
+ return {
923
+ ...proposed,
924
+ change: approved.change,
925
+ approved: true,
926
+ nextActions: [
927
+ `Implement only the exact L1 scope in openspec/changes/${proposed.change.id}`,
928
+ `Run staged verification: implementation -> prepublish -> postpublish -> archive`,
929
+ ],
930
+ };
931
+ }
932
+
622
933
  function includesAll(list, required) {
623
934
  if (!required || required.length === 0) return true;
624
935
  if ((list || []).includes('*')) return true;
@@ -693,13 +1004,25 @@ function affectedCoversHighRiskFiles(affected, files = []) {
693
1004
  if (!normalized.runtime && !includesAll(normalized.pages, [pageCode])) missing.push(file);
694
1005
  } else if (file.startsWith('src/resources/functions/')) {
695
1006
  const code = extractCodeFromPath(file, 'src/resources/functions/');
696
- if (code && !coversCodeTarget(normalized, 'functions', code)) missing.push(file);
1007
+ const isScopedContainer =
1008
+ normalized.resources || normalized.backend || normalized.functions.length > 0;
1009
+ if (code && !isScopedContainer && !coversCodeTarget(normalized, 'functions', code)) {
1010
+ missing.push(file);
1011
+ }
697
1012
  } else if (file.startsWith('src/resources/automations/')) {
698
1013
  const code = extractCodeFromPath(file, 'src/resources/automations/');
699
- if (code && !coversCodeTarget(normalized, 'automations', code)) missing.push(file);
1014
+ const isScopedContainer =
1015
+ normalized.resources || normalized.backend || normalized.automations.length > 0;
1016
+ if (code && !isScopedContainer && !coversCodeTarget(normalized, 'automations', code)) {
1017
+ missing.push(file);
1018
+ }
700
1019
  } else if (file.startsWith('src/resources/workflows/')) {
701
1020
  const code = extractCodeFromPath(file, 'src/resources/workflows/');
702
- if (code && !coversCodeTarget(normalized, 'workflows', code)) missing.push(file);
1021
+ const isScopedContainer =
1022
+ normalized.resources || normalized.backend || normalized.workflows.length > 0;
1023
+ if (code && !isScopedContainer && !coversCodeTarget(normalized, 'workflows', code)) {
1024
+ missing.push(file);
1025
+ }
703
1026
  } else if (file.startsWith('src/resources/js-code-nodes/')) {
704
1027
  const code = extractCodeFromPath(file, 'src/resources/js-code-nodes/');
705
1028
  if (code && !coversCodeTarget(normalized, 'jsCodeNodes', code)) missing.push(file);
@@ -824,10 +1147,14 @@ function summarizeCoverage(coverage) {
824
1147
  };
825
1148
  }
826
1149
 
827
- function chooseActiveChange(changes, targets = {}, highRiskFiles = []) {
1150
+ function chooseActiveChange(changes, targets = {}, highRiskFiles = [], options = {}) {
1151
+ const requestedChangeId = options.changeId
1152
+ ? normalizeChangeId(options.changeId)
1153
+ : null;
828
1154
  const candidates = changes
829
1155
  .filter(item => item.valid && !['archived', 'rejected'].includes(item.metadata.status))
830
- .filter(item => item.metadata.status === 'approved' && item.metadata.approvedAt);
1156
+ .filter(item => item.metadata.status === 'approved' && item.metadata.approvedAt)
1157
+ .filter(item => !requestedChangeId || item.id === requestedChangeId);
831
1158
  return (
832
1159
  candidates.find(item => {
833
1160
  const legacyMode = (item.metadata?.schemaVersion || LEGACY_SDD_SCHEMA_VERSION) !== SDD_SCHEMA_VERSION;
@@ -836,10 +1163,55 @@ function chooseActiveChange(changes, targets = {}, highRiskFiles = []) {
836
1163
  coverageCoversTargets(item.coverage, item.metadata.affected, targets, { allowFallback }).covered &&
837
1164
  coverageCoversHighRiskFiles(item.coverage, item.metadata.affected, highRiskFiles, { allowFallback }).covered
838
1165
  );
839
- }) || candidates[0] || null
1166
+ }) || null
840
1167
  );
841
1168
  }
842
1169
 
1170
+ function getSddChangeScope(options = {}) {
1171
+ const loaded = loadSddChange(options);
1172
+ const affected = normalizeAffected(loaded.meta.affected || {});
1173
+ const coverage = normalizeCoverageResources(loaded.coverage, {});
1174
+ const releaseTargets = loaded.release && !loaded.release.__invalid
1175
+ ? loaded.release.targets || {}
1176
+ : {};
1177
+ const mergeList = key => Array.from(new Set([
1178
+ ...splitList(releaseTargets[key]),
1179
+ ...splitList(coverage[key]),
1180
+ ...splitList(affected[key]),
1181
+ ])).sort();
1182
+ const targets = {
1183
+ forms: mergeList('forms'),
1184
+ pages: mergeList('pages'),
1185
+ functions: mergeList('functions'),
1186
+ automations: mergeList('automations'),
1187
+ workflows: mergeList('workflows'),
1188
+ jsCodeNodes: mergeList('jsCodeNodes'),
1189
+ resources: Boolean(releaseTargets.resources || coverage.resources || affected.resources),
1190
+ runtime: Boolean(releaseTargets.runtime || coverage.runtime || affected.runtime),
1191
+ other: Array.from(new Set(splitList(releaseTargets.other))).sort(),
1192
+ };
1193
+ const recordedChangedFiles = splitList(loaded.release?.changedFiles);
1194
+ const ownedFiles = Array.from(new Set([
1195
+ ...splitList(coverage.files),
1196
+ ...splitList(affected.files),
1197
+ ])).sort();
1198
+ const changedFiles = Array.from(new Set([
1199
+ ...recordedChangedFiles,
1200
+ ...ownedFiles,
1201
+ ])).sort();
1202
+ return {
1203
+ changeId: loaded.changeId,
1204
+ change: loaded.meta,
1205
+ coverage: loaded.coverage,
1206
+ release: loaded.release,
1207
+ targets,
1208
+ changedFiles,
1209
+ recordedChangedFiles,
1210
+ ownedFiles,
1211
+ commands: Array.isArray(loaded.release?.commands) ? loaded.release.commands : [],
1212
+ };
1213
+ }
1214
+
843
1215
  function buildNextActions(governance, activeChanges, activeChange, targets, highRiskFiles) {
844
1216
  if (!governance.enabled) return ['Enable SDD governance or run without SDD for low-risk work.'];
845
1217
  if (!governance.initialized) return ['Run `openxiangda sdd init --upgrade`.'];
@@ -916,13 +1288,21 @@ function buildSddGovernanceReport(options = {}) {
916
1288
  };
917
1289
  }
918
1290
 
1291
+ const requestedChangeId = options.changeId
1292
+ ? normalizeChangeId(options.changeId)
1293
+ : null;
919
1294
  const changes = listChanges(governance.rootDir);
920
1295
  activeChanges = changes.filter(
921
1296
  item => item.valid && !['archived', 'rejected'].includes(item.metadata.status)
922
1297
  );
923
- activeChange = chooseActiveChange(activeChanges, targets, highRiskFiles);
1298
+ activeChange = chooseActiveChange(activeChanges, targets, highRiskFiles, {
1299
+ changeId: requestedChangeId,
1300
+ });
924
1301
  coverage = activeChange?.coverage || null;
925
- const approved = getApprovedActiveChanges(governance.rootDir);
1302
+ const approvedAll = getApprovedActiveChanges(governance.rootDir);
1303
+ const approved = requestedChangeId
1304
+ ? approvedAll.filter(item => item.id === requestedChangeId)
1305
+ : approvedAll;
926
1306
  if (!governance.strictHighRisk || (!hasHighRiskTargets(targets) && highRiskFiles.length === 0)) {
927
1307
  checks.push({ name: 'sdd', status: 'ok', message: `${activeChanges.length} active changes` });
928
1308
  return {
@@ -941,7 +1321,20 @@ function buildSddGovernanceReport(options = {}) {
941
1321
  };
942
1322
  }
943
1323
 
944
- if (approved.length === 0) {
1324
+ const requestedChange = requestedChangeId
1325
+ ? activeChanges.find(item => item.id === requestedChangeId)
1326
+ : null;
1327
+ if (requestedChangeId && !requestedChange) {
1328
+ errors.push({
1329
+ name: 'sdd-change-not-found',
1330
+ message: `指定的 SDD change 不存在或已归档: ${requestedChangeId}`,
1331
+ });
1332
+ } else if (requestedChangeId && approved.length === 0) {
1333
+ errors.push({
1334
+ name: 'sdd-change-not-approved',
1335
+ message: `指定的 SDD change 尚未 approve: ${requestedChangeId}`,
1336
+ });
1337
+ } else if (approved.length === 0) {
945
1338
  errors.push({
946
1339
  name: 'sdd-approved-change-required',
947
1340
  message: '高风险变更发布前必须存在 approved SDD change;先运行 openxiangda sdd propose <change> 并经 openxiangda sdd approve <change> 确认。',
@@ -949,7 +1342,9 @@ function buildSddGovernanceReport(options = {}) {
949
1342
  } else if (!activeChange) {
950
1343
  errors.push({
951
1344
  name: 'sdd-coverage-required',
952
- message: 'approved SDD change 的 coverage.json 未覆盖本次发布目标或高风险文件。',
1345
+ message: requestedChangeId
1346
+ ? `指定 change ${requestedChangeId} 的 coverage.json 未覆盖本次发布目标或高风险文件。`
1347
+ : 'approved SDD change 的 coverage.json 未覆盖本次发布目标或高风险文件。',
953
1348
  });
954
1349
  } else {
955
1350
  const verify = verifySddChange({
@@ -958,6 +1353,7 @@ function buildSddGovernanceReport(options = {}) {
958
1353
  changeId: activeChange.id,
959
1354
  targets,
960
1355
  files,
1356
+ stage: options.stage || options.verifyStage,
961
1357
  releasePlan: {
962
1358
  targets,
963
1359
  changedFiles: files,
@@ -1001,8 +1397,96 @@ function readUncheckedChecklist(file, missingMessage) {
1001
1397
  .map(item => `${path.basename(file)}:${item.index} ${item.line}`);
1002
1398
  }
1003
1399
 
1004
- function readUncheckedTasks(changeDir) {
1005
- return readUncheckedChecklist(path.join(changeDir, 'tasks.md'), 'tasks.md 缺失');
1400
+ function normalizeSddVerifyStage(value) {
1401
+ const stage = String(value || 'default').trim().toLowerCase();
1402
+ if (['default', 'legacy', 'all'].includes(stage)) return 'default';
1403
+ if (stage === 'pre-publish') return 'prepublish';
1404
+ if (stage === 'post-publish') return 'postpublish';
1405
+ if (!SDD_VERIFY_STAGES.includes(stage)) {
1406
+ throw new Error(
1407
+ `未知 SDD verify stage: ${value}。可选 implementation, prepublish, postpublish, archive`
1408
+ );
1409
+ }
1410
+ return stage;
1411
+ }
1412
+
1413
+ function getSddVerifyStagePolicy(stageValue) {
1414
+ const stage = normalizeSddVerifyStage(stageValue);
1415
+ if (stage === 'default') {
1416
+ return {
1417
+ stage,
1418
+ taskStages: new Set(SDD_VERIFY_STAGES),
1419
+ evidenceStages: new Set(SDD_VERIFY_STAGES),
1420
+ requireRelease: true,
1421
+ };
1422
+ }
1423
+ const rank = SDD_VERIFY_STAGES.indexOf(stage);
1424
+ return {
1425
+ stage,
1426
+ taskStages: new Set(SDD_VERIFY_STAGES.slice(0, rank + 1)),
1427
+ evidenceStages:
1428
+ stage === 'implementation'
1429
+ ? new Set()
1430
+ : new Set(SDD_VERIFY_STAGES.slice(1, rank + 1)),
1431
+ requireRelease: stage !== 'implementation',
1432
+ };
1433
+ }
1434
+
1435
+ function inferChecklistStage(kind, section, line) {
1436
+ const explicit = line.match(/sdd-stage\s*:\s*(implementation|prepublish|postpublish|archive)/i)?.[1];
1437
+ if (explicit) return explicit.toLowerCase();
1438
+ const normalizedSection = String(section || '').toLowerCase().replace(/[^a-z]/g, '');
1439
+ if (normalizedSection.includes('implementation')) return 'implementation';
1440
+ if (normalizedSection.includes('prepublish') || normalizedSection.includes('verification')) {
1441
+ return 'prepublish';
1442
+ }
1443
+ if (normalizedSection.includes('postpublish') || normalizedSection === 'release') {
1444
+ return 'postpublish';
1445
+ }
1446
+ if (normalizedSection.includes('archive')) return 'archive';
1447
+ if (/archive|归档/i.test(line)) return 'archive';
1448
+ if (/publish\/deploy result|publish output|release output|发布结果|上线结果/i.test(line)) {
1449
+ return 'postpublish';
1450
+ }
1451
+ if (
1452
+ kind === 'evidence' ||
1453
+ /verification evidence|release plan|workspace\/resource\/runtime check|dry-run|发布计划|验证证据/i.test(line)
1454
+ ) {
1455
+ return 'prepublish';
1456
+ }
1457
+ return 'implementation';
1458
+ }
1459
+
1460
+ function readUncheckedChecklistForStages(file, options = {}) {
1461
+ const requiredStages = options.requiredStages || new Set(SDD_VERIFY_STAGES);
1462
+ if (requiredStages.size === 0) return [];
1463
+ if (!fs.existsSync(file)) {
1464
+ return [options.missingMessage || `${path.basename(file)} 缺失`];
1465
+ }
1466
+ const result = [];
1467
+ let section = '';
1468
+ const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
1469
+ for (let index = 0; index < lines.length; index += 1) {
1470
+ const line = lines[index];
1471
+ const heading = line.match(/^##\s+(.+)$/);
1472
+ if (heading) {
1473
+ section = heading[1].trim();
1474
+ continue;
1475
+ }
1476
+ if (!/^-\s*\[\s\]/.test(line)) continue;
1477
+ const stage = inferChecklistStage(options.kind || 'tasks', section, line);
1478
+ if (!requiredStages.has(stage)) continue;
1479
+ result.push(`${path.basename(file)}:${index + 1} [${stage}] ${line}`);
1480
+ }
1481
+ return result;
1482
+ }
1483
+
1484
+ function readUncheckedTasks(changeDir, stagePolicy = getSddVerifyStagePolicy('default')) {
1485
+ return readUncheckedChecklistForStages(path.join(changeDir, 'tasks.md'), {
1486
+ kind: 'tasks',
1487
+ requiredStages: stagePolicy.taskStages,
1488
+ missingMessage: 'tasks.md 缺失',
1489
+ });
1006
1490
  }
1007
1491
 
1008
1492
  function validateCoverageManifest(coverage, meta, targets, highRiskFiles, legacyMode = false) {
@@ -1151,7 +1635,8 @@ function validateDeltaSpecs(changeDir) {
1151
1635
  };
1152
1636
  }
1153
1637
 
1154
- function buildUpdatedReleaseManifest(loaded, releasePlan) {
1638
+ function buildUpdatedReleaseManifest(loaded, releasePlan, verificationStage = 'default') {
1639
+ const verifiedAt = nowIso();
1155
1640
  return {
1156
1641
  schemaVersion: SDD_SCHEMA_VERSION,
1157
1642
  changeId: loaded.changeId,
@@ -1159,12 +1644,17 @@ function buildUpdatedReleaseManifest(loaded, releasePlan) {
1159
1644
  targets: releasePlan?.targets || loaded.release?.targets || {},
1160
1645
  changedFiles: releasePlan?.changedFiles || loaded.release?.changedFiles || [],
1161
1646
  commands: releasePlan?.commands || loaded.release?.commands || [],
1162
- verifiedAt: nowIso(),
1647
+ verifiedAt,
1648
+ verificationStage,
1649
+ verificationStages: {
1650
+ ...(loaded.release?.verificationStages || {}),
1651
+ [verificationStage]: { verifiedAt },
1652
+ },
1163
1653
  };
1164
1654
  }
1165
1655
 
1166
- function updateReleaseManifest(loaded, releasePlan) {
1167
- const release = buildUpdatedReleaseManifest(loaded, releasePlan);
1656
+ function updateReleaseManifest(loaded, releasePlan, verificationStage = 'default') {
1657
+ const release = buildUpdatedReleaseManifest(loaded, releasePlan, verificationStage);
1168
1658
  writeJsonFile(loaded.releaseFile, release);
1169
1659
  return release;
1170
1660
  }
@@ -1202,6 +1692,8 @@ function validateReleaseManifest(release, releasePlan, legacyMode = false) {
1202
1692
 
1203
1693
  function verifySddChange(options = {}) {
1204
1694
  const loaded = loadSddChange(options);
1695
+ const stagePolicy = getSddVerifyStagePolicy(options.stage || options.phase);
1696
+ const verificationStage = stagePolicy.stage;
1205
1697
  const targets = options.targets || options.releasePlan?.targets || {};
1206
1698
  const highRiskFiles = classifyHighRiskFiles(options.files || options.releasePlan?.changedFiles || []);
1207
1699
  const legacyMode = (loaded.meta.schemaVersion || LEGACY_SDD_SCHEMA_VERSION) !== SDD_SCHEMA_VERSION;
@@ -1230,7 +1722,7 @@ function verifySddChange(options = {}) {
1230
1722
  const deltaValidation = validateDeltaSpecs(loaded.changeDir);
1231
1723
  errors.push(...deltaValidation.errors);
1232
1724
  warnings.push(...deltaValidation.warnings);
1233
- const uncheckedTasks = readUncheckedTasks(loaded.changeDir);
1725
+ const uncheckedTasks = readUncheckedTasks(loaded.changeDir, stagePolicy);
1234
1726
  if (uncheckedTasks.length > 0) {
1235
1727
  errors.push({
1236
1728
  name: 'sdd-unfinished-tasks',
@@ -1239,7 +1731,11 @@ function verifySddChange(options = {}) {
1239
1731
  });
1240
1732
  }
1241
1733
  const evidenceFile = path.join(loaded.changeDir, 'evidence.md');
1242
- const uncheckedEvidence = readUncheckedChecklist(evidenceFile, 'evidence.md 缺失');
1734
+ const uncheckedEvidence = readUncheckedChecklistForStages(evidenceFile, {
1735
+ kind: 'evidence',
1736
+ requiredStages: stagePolicy.evidenceStages,
1737
+ missingMessage: 'evidence.md 缺失',
1738
+ });
1243
1739
  if (uncheckedEvidence.length > 0) {
1244
1740
  errors.push({
1245
1741
  name: 'sdd-evidence-incomplete',
@@ -1249,25 +1745,35 @@ function verifySddChange(options = {}) {
1249
1745
  }
1250
1746
  const release =
1251
1747
  options.writeMetadata && options.releasePlan && loaded.release && !loaded.release.__invalid
1252
- ? buildUpdatedReleaseManifest(loaded, options.releasePlan)
1748
+ ? buildUpdatedReleaseManifest(loaded, options.releasePlan, verificationStage)
1253
1749
  : loaded.release;
1254
- const releaseValidation = validateReleaseManifest(release, options.releasePlan, legacyMode);
1255
- errors.push(...releaseValidation.errors);
1256
- warnings.push(...releaseValidation.warnings);
1750
+ if (stagePolicy.requireRelease) {
1751
+ const releaseValidation = validateReleaseManifest(release, options.releasePlan, legacyMode);
1752
+ errors.push(...releaseValidation.errors);
1753
+ warnings.push(...releaseValidation.warnings);
1754
+ }
1257
1755
  const passed = errors.length === 0;
1756
+ const verifiedAt = nowIso();
1258
1757
  const verifiedMeta = {
1259
1758
  ...loaded.meta,
1260
1759
  release: {
1261
1760
  ...(loaded.meta.release || {}),
1262
- plannedCommands: options.releasePlan?.commands || loaded.meta.release?.plannedCommands || release.commands || [],
1263
- lastVerifiedAt: nowIso(),
1761
+ plannedCommands: options.releasePlan?.commands || loaded.meta.release?.plannedCommands || release?.commands || [],
1762
+ lastVerifiedAt: verifiedAt,
1264
1763
  },
1265
- updatedAt: nowIso(),
1764
+ verification: {
1765
+ ...(loaded.meta.verification || {}),
1766
+ [verificationStage]: {
1767
+ passed,
1768
+ verifiedAt,
1769
+ },
1770
+ },
1771
+ updatedAt: verifiedAt,
1266
1772
  };
1267
1773
  let metadataWritten = false;
1268
1774
  if (options.writeMetadata && passed) {
1269
1775
  if (options.releasePlan && loaded.release && !loaded.release.__invalid) {
1270
- updateReleaseManifest(loaded, options.releasePlan);
1776
+ updateReleaseManifest(loaded, options.releasePlan, verificationStage);
1271
1777
  }
1272
1778
  writeJsonFile(loaded.metaFile, verifiedMeta);
1273
1779
  metadataWritten = true;
@@ -1285,6 +1791,12 @@ function verifySddChange(options = {}) {
1285
1791
  metadataWritten,
1286
1792
  targets,
1287
1793
  highRiskFiles,
1794
+ stage: verificationStage,
1795
+ stageRequirements: {
1796
+ tasks: Array.from(stagePolicy.taskStages),
1797
+ evidence: Array.from(stagePolicy.evidenceStages),
1798
+ release: stagePolicy.requireRelease,
1799
+ },
1288
1800
  };
1289
1801
  }
1290
1802
 
@@ -1315,8 +1827,13 @@ function appendBypassLog(options = {}) {
1315
1827
  ];
1316
1828
  fs.appendFileSync(file, entry.join('\n'));
1317
1829
 
1830
+ const requestedChangeId = options.changeId ? normalizeChangeId(options.changeId) : null;
1318
1831
  const approved = listChanges(governance.rootDir).find(
1319
- item => item.valid && item.metadata.status === 'approved' && item.metadata.approvedAt
1832
+ item =>
1833
+ item.valid &&
1834
+ item.metadata.status === 'approved' &&
1835
+ item.metadata.approvedAt &&
1836
+ (!requestedChangeId || item.id === requestedChangeId)
1320
1837
  );
1321
1838
  if (approved) {
1322
1839
  appendToEvidence(approved.dir, [
@@ -1340,13 +1857,17 @@ function buildSddStatus(options = {}) {
1340
1857
  const archived = governance.initialized ? listChanges(governance.rootDir, { includeArchive: true }).filter(item => item.dir.includes(`${path.sep}archive${path.sep}`)) : [];
1341
1858
  const releasePlan = options.releasePlan || {};
1342
1859
  const highRiskFiles = classifyHighRiskFiles(releasePlan.changedFiles || []);
1343
- const activeChange = chooseActiveChange(changes, releasePlan.targets || {}, highRiskFiles);
1860
+ const activeChange = chooseActiveChange(changes, releasePlan.targets || {}, highRiskFiles, {
1861
+ changeId: options.changeId,
1862
+ });
1344
1863
  const report = buildSddGovernanceReport({
1345
1864
  cwd: options.cwd,
1346
1865
  configText: options.configText,
1347
1866
  targets: releasePlan.targets || {},
1348
1867
  files: releasePlan.changedFiles || [],
1349
1868
  commands: releasePlan.commands || [],
1869
+ changeId: options.changeId,
1870
+ stage: options.stage || options.verifyStage,
1350
1871
  });
1351
1872
  return {
1352
1873
  cwd: options.cwd || process.cwd(),
@@ -1461,7 +1982,7 @@ function syncSddChange(options = {}) {
1461
1982
  }
1462
1983
 
1463
1984
  function archiveSddChange(options = {}) {
1464
- const verify = verifySddChange(options);
1985
+ const verify = verifySddChange({ ...options, stage: 'archive' });
1465
1986
  if (!verify.passed && !options.force) {
1466
1987
  const first = verify.errors[0];
1467
1988
  throw new Error(`change 不能归档: ${first.message}`);
@@ -1492,16 +2013,27 @@ function archiveSddChange(options = {}) {
1492
2013
 
1493
2014
  module.exports = {
1494
2015
  DEFAULT_SDD_DIR,
2016
+ QUICK_FUNCTION_SAFETY_FIELDS,
2017
+ QUICK_SDD_ALLOWED_KINDS,
2018
+ QUICK_SDD_RISK_TIER,
1495
2019
  SDD_SCHEMA_VERSION,
2020
+ SDD_VERIFY_STAGES,
1496
2021
  appendBypassLog,
2022
+ approveQuickSddChange,
1497
2023
  archiveSddChange,
1498
2024
  buildSddGovernanceReport,
1499
2025
  buildSddStatus,
2026
+ createQuickSddChange,
1500
2027
  getSddGovernanceConfig,
2028
+ getSddChangeScope,
2029
+ getSddVerifyStagePolicy,
1501
2030
  initSddWorkspace,
1502
2031
  normalizeAffected,
2032
+ normalizeSddVerifyStage,
2033
+ proposeQuickSddChange,
1503
2034
  proposeSddChange,
1504
2035
  approveSddChange,
1505
2036
  syncSddChange,
2037
+ validateQuickSddScope,
1506
2038
  verifySddChange,
1507
2039
  };