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.
- package/README.md +39 -13
- package/lib/change-baseline.js +1077 -0
- package/lib/cli.js +12002 -6723
- package/lib/config.js +272 -7
- package/lib/http.js +2 -0
- package/lib/js-code-build.js +61 -0
- package/lib/publish-context.js +98 -0
- package/lib/publish-lease-heartbeat.js +182 -0
- package/lib/publish-lease.js +190 -0
- package/lib/release-telemetry.js +182 -0
- package/lib/runtime-lineage.js +297 -0
- package/lib/sdd.js +567 -35
- package/lib/source-dependencies.js +742 -0
- package/lib/workspace-init.js +6 -1
- package/lib/worktree-owner.js +219 -0
- package/openxiangda-skills/SKILL.md +103 -228
- package/openxiangda-skills/references/architecture-design.md +2 -1
- package/openxiangda-skills/references/openxiangda-api.md +27 -0
- package/openxiangda-skills/references/pages/page-sdk.md +11 -1
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +12 -2
- package/openxiangda-skills/references/workspace-state.md +2 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +77 -243
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +8 -0
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +18 -0
- package/package.json +20 -1
- package/packages/sdk/dist/{ProcessPreview-DMkzccq4.d.mts → ProcessPreview-Dux73lY_.d.mts} +5 -0
- package/packages/sdk/dist/{ProcessPreview-DMkzccq4.d.ts → ProcessPreview-Dux73lY_.d.ts} +5 -0
- package/packages/sdk/dist/components/index.cjs +29 -1
- package/packages/sdk/dist/components/index.cjs.map +1 -1
- package/packages/sdk/dist/components/index.d.mts +4 -4
- package/packages/sdk/dist/components/index.d.ts +4 -4
- package/packages/sdk/dist/components/index.mjs +29 -1
- package/packages/sdk/dist/components/index.mjs.map +1 -1
- package/packages/sdk/dist/{dataManagementApi-C9O-Bb0j.d.ts → dataManagementApi-BNLH2o7f.d.ts} +2 -1
- package/packages/sdk/dist/{dataManagementApi-gpZkgRDM.d.mts → dataManagementApi-BOcIx4U-.d.mts} +2 -1
- package/packages/sdk/dist/runtime/index.cjs +150 -22
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +3 -3
- package/packages/sdk/dist/runtime/index.d.ts +3 -3
- package/packages/sdk/dist/runtime/index.mjs +150 -22
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +121 -21
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +15 -2
- package/packages/sdk/dist/runtime/react.d.ts +15 -2
- package/packages/sdk/dist/runtime/react.mjs +121 -21
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/packages/sdk/src/build-source/scripts/register.mjs +54 -23
- package/packages/sdk/src/build-source/scripts/sync-schema.mjs +59 -19
- package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +242 -2
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +7 -3
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +11 -4
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +7 -3
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +10 -4
- package/templates/openxiangda-react-spa/AGENTS.md +25 -12
- package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +344 -60
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +5 -1
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +8 -2
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +8 -4
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +8 -2
- package/templates/sy-lowcode-app-workspace/AGENTS.md +14 -4
- 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
|
-
|
|
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
|
-
'
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}) ||
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
1005
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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
|
|
1263
|
-
lastVerifiedAt:
|
|
1761
|
+
plannedCommands: options.releasePlan?.commands || loaded.meta.release?.plannedCommands || release?.commands || [],
|
|
1762
|
+
lastVerifiedAt: verifiedAt,
|
|
1264
1763
|
},
|
|
1265
|
-
|
|
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 =>
|
|
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
|
};
|