kld-sdd 2.6.12 → 2.6.13
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 +20 -0
- package/kld-sdd-guide.html +429 -159
- package/lib/init.js +40 -7
- package/package.json +5 -2
- package/skywalk-sdd/index.cjs +2668 -415
- package/skywalk-sdd/metrics-v3.cjs +1153 -0
- package/skywalk-sdd/ontology/archive-package.cjs +114 -3
- package/skywalk-sdd/ontology/resolve-spec-root.cjs +20 -80
- package/skywalk-sdd/ontology/working-artifacts.cjs +2 -1
- package/templates/hooks/claude/hooks/sdd-apply-test-gate.cjs +39 -9
- package/templates/hooks/claude/hooks/sdd-post-tool.cjs +87 -8
- package/templates/hooks/claude/hooks/sdd-prompt.cjs +3 -1
- package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +39 -9
- package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +87 -9
- package/templates/hooks/codebuddy/hooks/sdd-prompt.cjs +3 -1
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +14 -13
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +20 -20
- package/templates/skills/kld-sdd/opsx-apply/implementer-prompt.md +12 -12
- package/templates/skills/kld-sdd/opsx-apply/reference.md +32 -31
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +44 -6
- package/templates/skills/kld-sdd/opsx-archive/checklist.md +4 -1
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +19 -11
- package/templates/skills/kld-sdd/opsx-check/checklist.md +7 -6
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-design/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +9 -1
- package/templates/skills/kld-sdd/opsx-propose/reference.md +29 -3
- package/templates/skills/kld-sdd/opsx-rules/reference.md +3 -1
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-spec/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +10 -10
- package/templates/skills/kld-sdd/opsx-task/checklist.md +3 -3
- package/templates/skills/kld-sdd/opsx-task/reference.md +3 -3
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +7 -5
- package/templates/skills/kld-sdd/{opsx-tdd-anti-patterns → tdd-anti-patterns}/SKILL.md +4 -4
- package/templates/skills/kld-sdd/{opsx-tdd-anti-patterns → tdd-anti-patterns}/reference.md +4 -4
- package/templates/skills/kld-sdd/{opsx-tdd-core → tdd-core}/SKILL.md +13 -13
- package/templates/skills/kld-sdd/{opsx-tdd-core → tdd-core}/checklist.md +5 -5
- package/templates/skills/kld-sdd/{opsx-tdd-core → tdd-core}/reference.md +12 -12
- package/templates/skills/kld-sdd/{opsx-tdd-metrics → tdd-metrics}/SKILL.md +3 -3
- package/templates/skills/kld-sdd/{opsx-tdd-metrics → tdd-metrics}/checklist.md +2 -2
- package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/SKILL.md +2 -2
- package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/parameterized-testing.md +1 -1
- package/templates/skills/kld-sdd/{opsx-tdd-review → tdd-review}/SKILL.md +5 -5
- package/templates/skills/kld-sdd/{opsx-tdd-review → tdd-review}/checklist.md +5 -5
- package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/SKILL.md +3 -3
- package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/non-tdd-modules.md +1 -1
- package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/refactor-checklist.md +1 -1
- package/templates/skills/kld-sdd/tdd-rules/rules/test-skeleton-telemetry.md +19 -0
- package/templates/skills/kld-sdd/opsx-tdd-rules/rules/test-skeleton-telemetry.md +0 -19
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/cause-effect-clarity.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/clean-test-data.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/existing-test-awareness.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/given-when-then.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/good-test-qualities.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/mock-boundary.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/naming-conventions.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/no-logic-in-tests.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/one-test-one-scenario.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/prefer-public-apis.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/test-behaviors-not-methods.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/argument-matching.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/controller-test-rules.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/domain-service-rules.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/java-test-template.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/json-serialization.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/logging-rules.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/post-generation/compilation-verification.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/post-generation/execution-verification.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/python/py-test-template.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/typescript/ts-test-template.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/controller-strategy.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/dag-generation-rules.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/des-step-annotation.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/exception-path-coverage.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/green-scope-declaration.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/green-yagni-fence.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/multi-validation-split.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/task-type-definitions.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/tdd-strategy-selection.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/test-execution-gate.md +0 -0
|
@@ -4,9 +4,11 @@ const crypto = require('crypto');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { generateUuidV7 } = require('./id.cjs');
|
|
7
|
+
const { resolveRuntimeMetadata } = require('../runtime-metadata.cjs');
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
9
|
+
const RUNTIME_METADATA = resolveRuntimeMetadata();
|
|
10
|
+
const PRODUCER_VERSION = RUNTIME_METADATA.version;
|
|
11
|
+
const PRODUCER = RUNTIME_METADATA.producer;
|
|
10
12
|
const MANIFEST_SCHEMA_VERSION = 'kld-sdd-archive-manifest/v2';
|
|
11
13
|
const CANONICAL_FACTS_SCHEMA_VERSION = 'kld-sdd-canonical-facts/v2';
|
|
12
14
|
const CONVERSION_REPORT_SCHEMA_VERSION = 'kld-sdd-conversion-report/v1';
|
|
@@ -15,7 +17,7 @@ const MANIFEST_PATH = 'archive-manifest.json';
|
|
|
15
17
|
const ONTOLOGY_PATH = 'archive-ontology.json';
|
|
16
18
|
const CANONICAL_FACTS_PATH = 'canonical-facts.json';
|
|
17
19
|
const CONVERSION_REPORT_PATH = 'conversion-report.json';
|
|
18
|
-
const PACKAGE_SUFFIXES = new Set(['.md', '.json', '.jsonl', '.yaml', '.yml']);
|
|
20
|
+
const PACKAGE_SUFFIXES = new Set(['.md', '.html', '.json', '.jsonl', '.yaml', '.yml']);
|
|
19
21
|
|
|
20
22
|
function sha256(content) {
|
|
21
23
|
return crypto.createHash('sha256').update(content).digest('hex');
|
|
@@ -495,6 +497,77 @@ function writeArchiveZip(archiveDir, packagePath, manifest) {
|
|
|
495
497
|
return packagePath;
|
|
496
498
|
}
|
|
497
499
|
|
|
500
|
+
function readStoredZipEntries(packagePath) {
|
|
501
|
+
const bytes = fs.readFileSync(packagePath);
|
|
502
|
+
const entries = new Map();
|
|
503
|
+
let offset = 0;
|
|
504
|
+
while (offset + 4 <= bytes.length && bytes.readUInt32LE(offset) === 0x04034b50) {
|
|
505
|
+
if (offset + 30 > bytes.length) {
|
|
506
|
+
throw new Error('Archive Package ZIP local header 不完整');
|
|
507
|
+
}
|
|
508
|
+
const flags = bytes.readUInt16LE(offset + 6);
|
|
509
|
+
const method = bytes.readUInt16LE(offset + 8);
|
|
510
|
+
const checksum = bytes.readUInt32LE(offset + 14);
|
|
511
|
+
const compressedSize = bytes.readUInt32LE(offset + 18);
|
|
512
|
+
const uncompressedSize = bytes.readUInt32LE(offset + 22);
|
|
513
|
+
const nameLength = bytes.readUInt16LE(offset + 26);
|
|
514
|
+
const extraLength = bytes.readUInt16LE(offset + 28);
|
|
515
|
+
const nameStart = offset + 30;
|
|
516
|
+
const contentStart = nameStart + nameLength + extraLength;
|
|
517
|
+
const contentEnd = contentStart + compressedSize;
|
|
518
|
+
if ((flags & 0x0800) !== 0x0800 || method !== 0 || contentEnd > bytes.length) {
|
|
519
|
+
throw new Error('Archive Package ZIP 只允许完整的 UTF-8 stored entry');
|
|
520
|
+
}
|
|
521
|
+
const name = bytes.subarray(nameStart, nameStart + nameLength).toString('utf8');
|
|
522
|
+
const safeName = safePackagePath(name);
|
|
523
|
+
if (entries.has(safeName)) {
|
|
524
|
+
throw new Error(`Archive Package ZIP 包含重复路径: ${safeName}`);
|
|
525
|
+
}
|
|
526
|
+
const content = bytes.subarray(contentStart, contentEnd);
|
|
527
|
+
if (compressedSize !== uncompressedSize || crc32(content) !== checksum) {
|
|
528
|
+
throw new Error(`Archive Package ZIP entry 校验失败: ${safeName}`);
|
|
529
|
+
}
|
|
530
|
+
entries.set(safeName, content);
|
|
531
|
+
offset = contentEnd;
|
|
532
|
+
}
|
|
533
|
+
if (entries.size === 0 || offset + 4 > bytes.length || bytes.readUInt32LE(offset) !== 0x02014b50) {
|
|
534
|
+
throw new Error('Archive Package ZIP 缺少 central directory');
|
|
535
|
+
}
|
|
536
|
+
return entries;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function verifyArchivePackage(archiveDir, packagePath, manifest) {
|
|
540
|
+
const expectedPaths = [MANIFEST_PATH, ...(manifest.files || []).map((item) => safePackagePath(item.path))];
|
|
541
|
+
const expectedUnique = new Set(expectedPaths);
|
|
542
|
+
if (expectedUnique.size !== expectedPaths.length) {
|
|
543
|
+
throw new Error('Archive Package Manifest 包含重复路径');
|
|
544
|
+
}
|
|
545
|
+
const entries = readStoredZipEntries(packagePath);
|
|
546
|
+
const actualPaths = [...entries.keys()];
|
|
547
|
+
if (
|
|
548
|
+
actualPaths.length !== expectedPaths.length
|
|
549
|
+
|| expectedPaths.some((entryPath) => !entries.has(entryPath))
|
|
550
|
+
) {
|
|
551
|
+
throw new Error('Archive Package ZIP 与 Manifest 文件清单不一致');
|
|
552
|
+
}
|
|
553
|
+
const manifestBytes = fs.readFileSync(path.join(archiveDir, MANIFEST_PATH));
|
|
554
|
+
if (!entries.get(MANIFEST_PATH)?.equals(manifestBytes)) {
|
|
555
|
+
throw new Error('Archive Package ZIP 中的 Manifest 不是最终落盘版本');
|
|
556
|
+
}
|
|
557
|
+
for (const item of manifest.files || []) {
|
|
558
|
+
const content = entries.get(item.path);
|
|
559
|
+
const expectedHash = `sha256:${sha256(content)}`;
|
|
560
|
+
if (item.content_hash !== expectedHash) {
|
|
561
|
+
throw new Error(`Archive Package 内容哈希不一致: ${item.path}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
entry_count: entries.size,
|
|
566
|
+
package_hash: `sha256:${sha256(fs.readFileSync(packagePath))}`,
|
|
567
|
+
paths: actualPaths,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
498
571
|
function materializeArchivePackage(options = {}) {
|
|
499
572
|
const projectRoot = path.resolve(options.projectRoot || process.cwd());
|
|
500
573
|
const archiveDir = path.resolve(requiredText(options.archiveDir, 'archiveDir', 'Archive Package'));
|
|
@@ -588,6 +661,41 @@ function materializeArchivePackage(options = {}) {
|
|
|
588
661
|
};
|
|
589
662
|
}
|
|
590
663
|
|
|
664
|
+
function finalizeArchivePackageAfterReports(options = {}) {
|
|
665
|
+
const projectRoot = path.resolve(options.projectRoot || process.cwd());
|
|
666
|
+
const changeName = requiredText(options.changeName, 'changeName', 'Archive finalizer');
|
|
667
|
+
const runIdentity = requiredText(options.runIdentity, 'runIdentity', 'Archive finalizer');
|
|
668
|
+
const { withChangeLock } = require('./change-lock.cjs');
|
|
669
|
+
return withChangeLock(projectRoot, changeName, options.lockOptions || {}, (lock) => {
|
|
670
|
+
const prepared = typeof options.prepare === 'function'
|
|
671
|
+
? options.prepare(lock)
|
|
672
|
+
: {};
|
|
673
|
+
const produced = materializeArchivePackage({
|
|
674
|
+
...options,
|
|
675
|
+
...prepared,
|
|
676
|
+
projectRoot,
|
|
677
|
+
changeName,
|
|
678
|
+
});
|
|
679
|
+
if (!produced.package_path) {
|
|
680
|
+
throw new Error('Archive finalizer 必须生成 ZIP');
|
|
681
|
+
}
|
|
682
|
+
const verification = verifyArchivePackage(
|
|
683
|
+
path.resolve(prepared.archiveDir || options.archiveDir),
|
|
684
|
+
produced.package_path,
|
|
685
|
+
produced.manifest,
|
|
686
|
+
);
|
|
687
|
+
return {
|
|
688
|
+
...produced,
|
|
689
|
+
finalizer: {
|
|
690
|
+
run_identity: runIdentity,
|
|
691
|
+
lock_token: lock.token,
|
|
692
|
+
verified: true,
|
|
693
|
+
...verification,
|
|
694
|
+
},
|
|
695
|
+
};
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
591
699
|
module.exports = {
|
|
592
700
|
PRODUCER_VERSION,
|
|
593
701
|
PRODUCER,
|
|
@@ -605,5 +713,8 @@ module.exports = {
|
|
|
605
713
|
buildCanonicalFacts,
|
|
606
714
|
collectPackageFilePaths,
|
|
607
715
|
writeArchiveZip,
|
|
716
|
+
readStoredZipEntries,
|
|
717
|
+
verifyArchivePackage,
|
|
608
718
|
materializeArchivePackage,
|
|
719
|
+
finalizeArchivePackageAfterReports,
|
|
609
720
|
};
|
|
@@ -5,98 +5,38 @@ const path = require('path');
|
|
|
5
5
|
const sddConfig = require('./sdd-config.cjs');
|
|
6
6
|
const workspaceLayout = require('./workspace-layout.cjs');
|
|
7
7
|
|
|
8
|
-
function
|
|
9
|
-
const
|
|
10
|
-
return
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
);
|
|
8
|
+
function resolveHint(anchorRoot) {
|
|
9
|
+
const hintPath = path.join(anchorRoot, '.sdd-spec-root');
|
|
10
|
+
if (!fs.existsSync(hintPath)) return null;
|
|
11
|
+
const hint = fs.readFileSync(hintPath, 'utf8').trim();
|
|
12
|
+
if (!hint) return null;
|
|
13
|
+
const resolved = path.resolve(anchorRoot, hint);
|
|
14
|
+
return fs.existsSync(resolved) ? resolved : null;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* 解析顺序:
|
|
22
|
-
* 1. start 本身已是 spec 包根
|
|
23
|
-
* 2. KLD_SDD_SPEC_ROOT(绝对或相对 start)
|
|
24
|
-
* 3. 单仓 layout: mono → spec_path / 子目录 *-sdd-specs
|
|
25
|
-
* 4. 工作区 layout → specRepo
|
|
26
|
-
* 5. start 下嵌套包裹包
|
|
27
|
-
* 6. 代码仓 git config sdd.specPath(含仓内 nested 包裹)
|
|
28
|
-
* 7. 向上最多 3 层找 mono / 嵌套包(Git 根、工作区根常见)
|
|
29
|
-
* 8. 回退 start
|
|
30
|
-
*/
|
|
31
|
-
function resolveSpecProjectRoot(startDir = process.cwd(), options = {}) {
|
|
32
|
-
const start = path.resolve(startDir || process.cwd());
|
|
33
|
-
if (options.skipResolve || options.forCodeRepo) return start;
|
|
34
|
-
if (isSpecPackageRoot(start)) return start;
|
|
17
|
+
function resolveSpecProjectRoot(projectRoot = process.cwd()) {
|
|
18
|
+
const root = path.resolve(projectRoot);
|
|
19
|
+
const hinted = resolveHint(root);
|
|
20
|
+
if (hinted) return hinted;
|
|
35
21
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const candidate = path.isAbsolute(envRoot) ? envRoot : path.resolve(start, envRoot);
|
|
39
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
22
|
+
if (sddConfig.isMonoLayout(root)) {
|
|
23
|
+
return sddConfig.resolveMonoSpecRoot(root);
|
|
40
24
|
}
|
|
41
25
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (fs.existsSync(monoRoot)) return monoRoot;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const layout = workspaceLayout.detectWorkspaceLayout(start);
|
|
48
|
-
if (layout.specRepo && fs.existsSync(layout.specRepo.abs)) {
|
|
49
|
-
return layout.specRepo.abs;
|
|
50
|
-
}
|
|
26
|
+
const layout = workspaceLayout.detectWorkspaceLayout(root);
|
|
27
|
+
if (layout.specRepo) return layout.specRepo.abs;
|
|
51
28
|
|
|
52
|
-
const nested = workspaceLayout.findNestedSpecPackage(
|
|
29
|
+
const nested = workspaceLayout.findNestedSpecPackage(root);
|
|
53
30
|
if (nested) return nested.abs;
|
|
54
31
|
|
|
55
|
-
|
|
56
|
-
if (specPathResult.ok) {
|
|
57
|
-
const valid = sddConfig.validateSpecPath(specPathResult.path, { allowNestedPackage: true });
|
|
58
|
-
if (valid.ok) return valid.path;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let dir = start;
|
|
62
|
-
for (let depth = 0; depth < 3; depth += 1) {
|
|
63
|
-
const parent = path.dirname(dir);
|
|
64
|
-
if (parent === dir) break;
|
|
65
|
-
if (sddConfig.isMonoLayout(parent)) {
|
|
66
|
-
const monoRoot = sddConfig.resolveMonoSpecRoot(parent);
|
|
67
|
-
if (fs.existsSync(monoRoot)) return monoRoot;
|
|
68
|
-
}
|
|
69
|
-
const parentNested = workspaceLayout.findNestedSpecPackage(parent);
|
|
70
|
-
if (parentNested) return parentNested.abs;
|
|
71
|
-
const parentLayout = workspaceLayout.detectWorkspaceLayout(parent);
|
|
72
|
-
if (parentLayout.specRepo && fs.existsSync(parentLayout.specRepo.abs)) {
|
|
73
|
-
return parentLayout.specRepo.abs;
|
|
74
|
-
}
|
|
75
|
-
dir = parent;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return start;
|
|
32
|
+
return root;
|
|
79
33
|
}
|
|
80
34
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
* '.' 或未显式绝对 spec 根时走 resolve;link-spec 等代码仓命令传 forCodeRepo:true。
|
|
84
|
-
*/
|
|
85
|
-
function resolveProjectArg(projectArg, startDir = process.cwd(), options = {}) {
|
|
86
|
-
const raw = projectArg == null || projectArg === '' ? '.' : String(projectArg);
|
|
87
|
-
const resolved = path.resolve(startDir, raw);
|
|
88
|
-
if (options.forCodeRepo) return resolved;
|
|
89
|
-
if (raw !== '.' && raw !== './' && path.isAbsolute(raw)) {
|
|
90
|
-
return isSpecPackageRoot(resolved) ? resolved : resolveSpecProjectRoot(resolved, options);
|
|
91
|
-
}
|
|
92
|
-
if (raw !== '.' && raw !== './') {
|
|
93
|
-
if (isSpecPackageRoot(resolved)) return resolved;
|
|
94
|
-
}
|
|
95
|
-
return resolveSpecProjectRoot(startDir, options);
|
|
35
|
+
function resolveProjectArg(projectArg = '.', cwd = process.cwd()) {
|
|
36
|
+
return resolveSpecProjectRoot(path.resolve(cwd, projectArg));
|
|
96
37
|
}
|
|
97
38
|
|
|
98
39
|
module.exports = {
|
|
99
|
-
isSpecPackageRoot,
|
|
100
|
-
resolveSpecProjectRoot,
|
|
101
40
|
resolveProjectArg,
|
|
41
|
+
resolveSpecProjectRoot,
|
|
102
42
|
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { resolveRuntimeMetadata } = require('../runtime-metadata.cjs');
|
|
5
6
|
|
|
6
7
|
const WORKING_ARTIFACT_SCHEMA_VERSION = 'kld-sdd-working-artifact-facts/v1';
|
|
7
8
|
const ARTIFACT_INDEX_SCHEMA_VERSION = 'kld-sdd-working-artifact-index/v1';
|
|
@@ -141,7 +142,7 @@ function compareFacts(left, right) {
|
|
|
141
142
|
|
|
142
143
|
function buildWorkingArtifactFacts(result, options = {}) {
|
|
143
144
|
const state = result.state || {};
|
|
144
|
-
const producerVersion = String(options.producerVersion ||
|
|
145
|
+
const producerVersion = String(options.producerVersion || resolveRuntimeMetadata().version);
|
|
145
146
|
const reviewStatus = state.review_status === 'pending' ? 'pending' : 'draft';
|
|
146
147
|
const rawEntities = Array.isArray(state.entities) ? state.entities : [];
|
|
147
148
|
const entityByAnchor = new Map(rawEntities.map((entity) => [
|
|
@@ -144,6 +144,26 @@ function isRealTestDetails(testResults) {
|
|
|
144
144
|
return false;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
function isPassingTestDetails(testResults) {
|
|
148
|
+
if (!isRealTestDetails(testResults)) return false;
|
|
149
|
+
const failed = Number(testResults.failed);
|
|
150
|
+
const exitCode = Number(testResults.exit_code);
|
|
151
|
+
return (!Number.isFinite(failed) || failed === 0)
|
|
152
|
+
&& (!Number.isFinite(exitCode) || exitCode === 0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isStrictTestEvidence(event, changeName) {
|
|
156
|
+
const testResults = event?.details?.test_results;
|
|
157
|
+
if (event?.type !== 'test_result' || event.change !== changeName) return false;
|
|
158
|
+
if (!event.run_id || !event.source || testResults?.evidence_tier !== 'strict') return false;
|
|
159
|
+
if (!['green', 'refactor', 'regression'].includes(testResults.tdd_phase)) return false;
|
|
160
|
+
if (!Number.isInteger(testResults.exit_code) || testResults.counts_known !== true) return false;
|
|
161
|
+
return event.result === 'success'
|
|
162
|
+
&& testResults.exit_code === 0
|
|
163
|
+
&& Number(testResults.failed) === 0
|
|
164
|
+
&& isPassingTestDetails(testResults);
|
|
165
|
+
}
|
|
166
|
+
|
|
147
167
|
function loadChangeEvents(projectRoot, changeName) {
|
|
148
168
|
const safe = safeChangeName(changeName);
|
|
149
169
|
const dir = path.join(projectRoot, 'skywalk-sdd', 'events', safe);
|
|
@@ -170,33 +190,36 @@ function loadChangeEvents(projectRoot, changeName) {
|
|
|
170
190
|
function hasRealTestExecution(projectRoot, changeName, capability, sinceTimestamp) {
|
|
171
191
|
const since = sinceTimestamp ? new Date(sinceTimestamp).getTime() : 0;
|
|
172
192
|
const events = loadChangeEvents(projectRoot, changeName);
|
|
193
|
+
let legacyEvidence = null;
|
|
173
194
|
|
|
174
195
|
for (const event of events) {
|
|
175
196
|
const ts = new Date(event.timestamp || 0).getTime();
|
|
176
197
|
if (since && ts < since) continue;
|
|
177
198
|
|
|
178
|
-
if (event.type === 'stage_end' && event.command === 'test' &&
|
|
179
|
-
|
|
180
|
-
return { kind: 'opsx-test', event };
|
|
199
|
+
if (event.type === 'stage_end' && event.command === 'test' && event.result === 'success') {
|
|
200
|
+
legacyEvidence ||= { kind: 'legacy-opsx-test', event };
|
|
181
201
|
}
|
|
182
202
|
|
|
183
203
|
if (event.type === 'test_result') {
|
|
184
204
|
const tr = event.details && event.details.test_results;
|
|
185
|
-
if (
|
|
186
|
-
return { kind: '
|
|
205
|
+
if (isStrictTestEvidence(event, changeName)) {
|
|
206
|
+
return { kind: 'strict-test-result', event };
|
|
207
|
+
}
|
|
208
|
+
if (event.result === 'success' && isPassingTestDetails(tr)) {
|
|
209
|
+
legacyEvidence ||= { kind: 'legacy-test-result', event };
|
|
187
210
|
}
|
|
188
211
|
}
|
|
189
212
|
|
|
190
213
|
if (event.type === 'task_update' && (event.command === 'apply' || !event.command)) {
|
|
191
214
|
const tr = event.details && event.details.test_results;
|
|
192
|
-
if (
|
|
215
|
+
if (event.result !== 'failure' && isPassingTestDetails(tr)) {
|
|
193
216
|
if (!capability || !event.capability || event.capability === capability) {
|
|
194
|
-
|
|
217
|
+
legacyEvidence ||= { kind: 'legacy-task-update', event };
|
|
195
218
|
}
|
|
196
219
|
}
|
|
197
220
|
}
|
|
198
221
|
}
|
|
199
|
-
return
|
|
222
|
+
return legacyEvidence;
|
|
200
223
|
}
|
|
201
224
|
|
|
202
225
|
function isApplyCompletionBash(command) {
|
|
@@ -281,9 +304,10 @@ function shouldGate(projectRoot, applyEvent) {
|
|
|
281
304
|
applyEvent.capability,
|
|
282
305
|
applyEvent.timestamp
|
|
283
306
|
);
|
|
284
|
-
if (evidence) {
|
|
307
|
+
if (evidence?.kind === 'strict-test-result') {
|
|
285
308
|
return null;
|
|
286
309
|
}
|
|
310
|
+
if (evidence) return { strategy, changeName: applyEvent.change, capability: applyEvent.capability, legacyEvidence: evidence };
|
|
287
311
|
return { strategy, changeName: applyEvent.change, capability: applyEvent.capability };
|
|
288
312
|
}
|
|
289
313
|
|
|
@@ -441,6 +465,11 @@ if (require.main === module) {
|
|
|
441
465
|
if (!gate) {
|
|
442
466
|
process.exit(0);
|
|
443
467
|
}
|
|
468
|
+
if (gate.legacyEvidence) {
|
|
469
|
+
recordWarning(projectRoot, { ...activeApply, test_strategy: gate.strategy }, 'legacy_test_evidence',
|
|
470
|
+
'仅检测到旧版测试证据;本次兼容放行,请补录包含 run-id、exit code、TDD phase 和严格身份字段的 test_result');
|
|
471
|
+
process.exit(0);
|
|
472
|
+
}
|
|
444
473
|
|
|
445
474
|
const reason = buildReason(gate.strategy, gate.changeName, gate.capability);
|
|
446
475
|
recordWarning(projectRoot, { ...activeApply, test_strategy: gate.strategy }, 'apply_test_missing', 'Apply test gate blocked: no unit test evidence');
|
|
@@ -458,5 +487,6 @@ module.exports = {
|
|
|
458
487
|
detectMissingTddRed,
|
|
459
488
|
hasExistingWarning,
|
|
460
489
|
isRealTestDetails,
|
|
490
|
+
isStrictTestEvidence,
|
|
461
491
|
requiresUnitTests,
|
|
462
492
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
4
5
|
const { execFileSync } = require('child_process');
|
|
5
6
|
|
|
6
7
|
function readStdin() {
|
|
@@ -75,13 +76,48 @@ function latestActiveStage(projectRoot) {
|
|
|
75
76
|
// v2.5 契约变更:返回类型从 string 变为对象(含 exit_code_known 标注);旧调用方 `inferResult(input) === 'success'` 需改为 `.result === 'success'`
|
|
76
77
|
function inferResult(input) {
|
|
77
78
|
const response = input.tool_response || input.toolResponse || {};
|
|
78
|
-
if (typeof response.exit_code === 'number') return { result: response.exit_code === 0 ? 'success' : 'failure', exit_code_known: true };
|
|
79
|
-
if (typeof response.exitCode === 'number') return { result: response.exitCode === 0 ? 'success' : 'failure', exit_code_known: true };
|
|
80
|
-
if (typeof response.success === 'boolean') return { result: response.success ? 'success' : 'failure', exit_code_known: true };
|
|
79
|
+
if (typeof response.exit_code === 'number') return { result: response.exit_code === 0 ? 'success' : 'failure', exit_code_known: true, exit_code: response.exit_code };
|
|
80
|
+
if (typeof response.exitCode === 'number') return { result: response.exitCode === 0 ? 'success' : 'failure', exit_code_known: true, exit_code: response.exitCode };
|
|
81
|
+
if (typeof response.success === 'boolean') return { result: response.success ? 'success' : 'failure', exit_code_known: true, exit_code: response.success ? 0 : 1 };
|
|
81
82
|
// fallback:从 stdout/stderr 识别构建失败信号;无法判定时 partial 且 exit_code_known=false
|
|
82
83
|
const out = String(response.stdout || response.stderr || '');
|
|
83
|
-
if (/BUILD FAILED|error TS\d+|ERR_/i.test(out)) return { result: 'failure', exit_code_known: false };
|
|
84
|
-
return { result: 'partial', exit_code_known: false };
|
|
84
|
+
if (/BUILD FAILED|error TS\d+|ERR_/i.test(out)) return { result: 'failure', exit_code_known: false, exit_code: null };
|
|
85
|
+
return { result: 'partial', exit_code_known: false, exit_code: null };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseTestCounts(output) {
|
|
89
|
+
const text = String(output || '');
|
|
90
|
+
const tapPass = text.match(/#\s*pass(?:ed)?\s+(\d+)/i);
|
|
91
|
+
const tapFail = text.match(/#\s*fail(?:ed)?\s+(\d+)/i);
|
|
92
|
+
const tapSkip = text.match(/#\s*skip(?:ped)?\s+(\d+)/i);
|
|
93
|
+
if (tapPass || tapFail) {
|
|
94
|
+
return {
|
|
95
|
+
counts_known: true,
|
|
96
|
+
passed: Number(tapPass?.[1] || 0),
|
|
97
|
+
failed: Number(tapFail?.[1] || 0),
|
|
98
|
+
skipped: Number(tapSkip?.[1] || 0),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const summary = text.match(/Tests?\s+.*?(\d+)\s+passed(?:.*?(\d+)\s+failed)?(?:.*?(\d+)\s+skipped)?/i);
|
|
102
|
+
if (summary) {
|
|
103
|
+
return {
|
|
104
|
+
counts_known: true,
|
|
105
|
+
passed: Number(summary[1] || 0),
|
|
106
|
+
failed: Number(summary[2] || 0),
|
|
107
|
+
skipped: Number(summary[3] || 0),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return { counts_known: false };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function stableRunId(input, activeStage, command) {
|
|
114
|
+
const explicit = input.tool_use_id || input.toolUseId || input.tool_call_id || input.toolCallId;
|
|
115
|
+
if (explicit) return String(explicit);
|
|
116
|
+
const response = input.tool_response || input.toolResponse || {};
|
|
117
|
+
return `hook-${crypto.createHash('sha256')
|
|
118
|
+
.update(JSON.stringify([activeStage.event_id || '', activeStage.session_id || '', command, response]))
|
|
119
|
+
.digest('hex')
|
|
120
|
+
.slice(0, 20)}`;
|
|
85
121
|
}
|
|
86
122
|
|
|
87
123
|
function inferRecord(command) {
|
|
@@ -113,6 +149,34 @@ function recordTelemetry(projectRoot, activeStage, record, result, exitCodeKnown
|
|
|
113
149
|
const toolInput = (input && (input.tool_input || input.toolInput)) || {};
|
|
114
150
|
const toolResponse = (input && (input.tool_response || input.toolResponse)) || {};
|
|
115
151
|
const details = { [record.detailsKey]: { ...record.details } };
|
|
152
|
+
let strictTestEvent = false;
|
|
153
|
+
let runId = null;
|
|
154
|
+
if (record.type === 'test_result') {
|
|
155
|
+
const output = String(toolResponse.stdout || '') + '\n' + String(toolResponse.stderr || '');
|
|
156
|
+
const counts = parseTestCounts(output);
|
|
157
|
+
const exitCode = typeof toolResponse.exit_code === 'number'
|
|
158
|
+
? toolResponse.exit_code
|
|
159
|
+
: (typeof toolResponse.exitCode === 'number'
|
|
160
|
+
? toolResponse.exitCode
|
|
161
|
+
: (typeof toolResponse.success === 'boolean' ? (toolResponse.success ? 0 : 1) : null));
|
|
162
|
+
const tddPhase = toolInput.tdd_phase || input.tdd_phase || 'regression';
|
|
163
|
+
details.test_results = {
|
|
164
|
+
command: toolInput.command || record.details.command,
|
|
165
|
+
tdd_phase: tddPhase,
|
|
166
|
+
exit_code: exitCode,
|
|
167
|
+
counts_known: counts.counts_known,
|
|
168
|
+
...(counts.counts_known ? {
|
|
169
|
+
passed: counts.passed,
|
|
170
|
+
failed: counts.failed,
|
|
171
|
+
skipped: counts.skipped,
|
|
172
|
+
} : {}),
|
|
173
|
+
duration_ms: Number.isFinite(toolResponse.duration_ms) ? toolResponse.duration_ms : 0,
|
|
174
|
+
failure_type: result === 'failure' ? (exitCode == null ? 'infrastructure' : 'assertion') : 'none',
|
|
175
|
+
expected_failure: tddPhase === 'red' && result === 'failure' && exitCode != null,
|
|
176
|
+
};
|
|
177
|
+
strictTestEvent = exitCode != null && Boolean(activeStage.task_id);
|
|
178
|
+
runId = stableRunId(input || {}, activeStage, details.test_results.command);
|
|
179
|
+
}
|
|
116
180
|
if (record.type === 'build_result') {
|
|
117
181
|
details.build_results.success = result === 'success';
|
|
118
182
|
details.build_results.error_count = result === 'success' ? 0 : null;
|
|
@@ -126,7 +190,7 @@ function recordTelemetry(projectRoot, activeStage, record, result, exitCodeKnown
|
|
|
126
190
|
logCli,
|
|
127
191
|
'record',
|
|
128
192
|
`--type=${record.type}`,
|
|
129
|
-
`--command=${
|
|
193
|
+
`--command=${activeStage.command || activeStage.stage || 'unknown'}`,
|
|
130
194
|
`--project=${projectRoot}`,
|
|
131
195
|
`--change=${activeStage.change || 'general'}`,
|
|
132
196
|
`--agent=${activeStage.agent_type || 'claude-code'}`,
|
|
@@ -135,9 +199,16 @@ function recordTelemetry(projectRoot, activeStage, record, result, exitCodeKnown
|
|
|
135
199
|
`--summary=Claude hook captured ${record.type}`,
|
|
136
200
|
`--details-json=${JSON.stringify(details)}`,
|
|
137
201
|
];
|
|
202
|
+
if (strictTestEvent) {
|
|
203
|
+
args.push(
|
|
204
|
+
'--strict',
|
|
205
|
+
`--run-id=${runId}`,
|
|
206
|
+
`--session-id=${activeStage.session_id || `stage-${activeStage.event_id}`}`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
138
209
|
if (activeStage.capability) args.push(`--capability=${activeStage.capability}`);
|
|
139
210
|
if (activeStage.task_id) args.push(`--task-id=${activeStage.task_id}`);
|
|
140
|
-
if (activeStage.session_id) args.push(`--session-id=${activeStage.session_id}`);
|
|
211
|
+
if (!strictTestEvent && activeStage.session_id) args.push(`--session-id=${activeStage.session_id}`);
|
|
141
212
|
const run = runner || ((a) => execFileSync('node', a, { cwd: projectRoot, stdio: 'ignore' }));
|
|
142
213
|
run(args);
|
|
143
214
|
}
|
|
@@ -164,4 +235,12 @@ if (require.main === module) {
|
|
|
164
235
|
}
|
|
165
236
|
}
|
|
166
237
|
|
|
167
|
-
module.exports = {
|
|
238
|
+
module.exports = {
|
|
239
|
+
inferResult,
|
|
240
|
+
inferRecord,
|
|
241
|
+
parseTestCounts,
|
|
242
|
+
stableRunId,
|
|
243
|
+
recordTelemetry,
|
|
244
|
+
findProjectRoot,
|
|
245
|
+
latestActiveStage,
|
|
246
|
+
};
|
|
@@ -94,8 +94,10 @@ const activeStages = readActiveStages(projectRoot)
|
|
|
94
94
|
|
|
95
95
|
const lines = [
|
|
96
96
|
'SDD Telemetry reminder:',
|
|
97
|
+
'- Resolve the exact change key first; every standard OPSX start/end command must include --change=<change-key>.',
|
|
97
98
|
'- Run node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" start before the OPSX stage work begins.',
|
|
98
|
-
'- Run node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" end
|
|
99
|
+
'- Run node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" end with the same change, stage, session and event identity before stopping.',
|
|
100
|
+
'- change=general is reserved for explicitly allowed explore events and must later use scope_link for historical attribution.',
|
|
99
101
|
'- Hooks are only an enhancement; OPSX skill instructions remain authoritative.',
|
|
100
102
|
];
|
|
101
103
|
|
|
@@ -74,6 +74,26 @@ function isRealTestDetails(testResults) {
|
|
|
74
74
|
return false;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function isPassingTestDetails(testResults) {
|
|
78
|
+
if (!isRealTestDetails(testResults)) return false;
|
|
79
|
+
const failed = Number(testResults.failed);
|
|
80
|
+
const exitCode = Number(testResults.exit_code);
|
|
81
|
+
return (!Number.isFinite(failed) || failed === 0)
|
|
82
|
+
&& (!Number.isFinite(exitCode) || exitCode === 0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isStrictTestEvidence(event, changeName) {
|
|
86
|
+
const testResults = event?.details?.test_results;
|
|
87
|
+
if (event?.type !== 'test_result' || event.change !== changeName) return false;
|
|
88
|
+
if (!event.run_id || !event.source || testResults?.evidence_tier !== 'strict') return false;
|
|
89
|
+
if (!['green', 'refactor', 'regression'].includes(testResults.tdd_phase)) return false;
|
|
90
|
+
if (!Number.isInteger(testResults.exit_code) || testResults.counts_known !== true) return false;
|
|
91
|
+
return event.result === 'success'
|
|
92
|
+
&& testResults.exit_code === 0
|
|
93
|
+
&& Number(testResults.failed) === 0
|
|
94
|
+
&& isPassingTestDetails(testResults);
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
function loadChangeEvents(projectRoot, changeName) {
|
|
78
98
|
const safe = safeChangeName(changeName);
|
|
79
99
|
const dir = path.join(projectRoot, 'skywalk-sdd', 'events', safe);
|
|
@@ -100,33 +120,36 @@ function loadChangeEvents(projectRoot, changeName) {
|
|
|
100
120
|
function hasRealTestExecution(projectRoot, changeName, capability, sinceTimestamp) {
|
|
101
121
|
const since = sinceTimestamp ? new Date(sinceTimestamp).getTime() : 0;
|
|
102
122
|
const events = loadChangeEvents(projectRoot, changeName);
|
|
123
|
+
let legacyEvidence = null;
|
|
103
124
|
|
|
104
125
|
for (const event of events) {
|
|
105
126
|
const ts = new Date(event.timestamp || 0).getTime();
|
|
106
127
|
if (since && ts < since) continue;
|
|
107
128
|
|
|
108
|
-
if (event.type === 'stage_end' && event.command === 'test' &&
|
|
109
|
-
|
|
110
|
-
return { kind: 'opsx-test', event };
|
|
129
|
+
if (event.type === 'stage_end' && event.command === 'test' && event.result === 'success') {
|
|
130
|
+
legacyEvidence ||= { kind: 'legacy-opsx-test', event };
|
|
111
131
|
}
|
|
112
132
|
|
|
113
133
|
if (event.type === 'test_result') {
|
|
114
134
|
const tr = event.details && event.details.test_results;
|
|
115
|
-
if (
|
|
116
|
-
return { kind: '
|
|
135
|
+
if (isStrictTestEvidence(event, changeName)) {
|
|
136
|
+
return { kind: 'strict-test-result', event };
|
|
137
|
+
}
|
|
138
|
+
if (event.result === 'success' && isPassingTestDetails(tr)) {
|
|
139
|
+
legacyEvidence ||= { kind: 'legacy-test-result', event };
|
|
117
140
|
}
|
|
118
141
|
}
|
|
119
142
|
|
|
120
143
|
if (event.type === 'task_update' && (event.command === 'apply' || !event.command)) {
|
|
121
144
|
const tr = event.details && event.details.test_results;
|
|
122
|
-
if (
|
|
145
|
+
if (event.result !== 'failure' && isPassingTestDetails(tr)) {
|
|
123
146
|
if (!capability || !event.capability || event.capability === capability) {
|
|
124
|
-
|
|
147
|
+
legacyEvidence ||= { kind: 'legacy-task-update', event };
|
|
125
148
|
}
|
|
126
149
|
}
|
|
127
150
|
}
|
|
128
151
|
}
|
|
129
|
-
return
|
|
152
|
+
return legacyEvidence;
|
|
130
153
|
}
|
|
131
154
|
|
|
132
155
|
function isApplyCompletionBash(command) {
|
|
@@ -210,9 +233,10 @@ function shouldGate(projectRoot, applyEvent) {
|
|
|
210
233
|
applyEvent.capability,
|
|
211
234
|
applyEvent.timestamp
|
|
212
235
|
);
|
|
213
|
-
if (evidence) {
|
|
236
|
+
if (evidence?.kind === 'strict-test-result') {
|
|
214
237
|
return null;
|
|
215
238
|
}
|
|
239
|
+
if (evidence) return { strategy, changeName: applyEvent.change, capability: applyEvent.capability, legacyEvidence: evidence };
|
|
216
240
|
return { strategy, changeName: applyEvent.change, capability: applyEvent.capability };
|
|
217
241
|
}
|
|
218
242
|
|
|
@@ -374,6 +398,11 @@ if (require.main === module) {
|
|
|
374
398
|
if (!gate) {
|
|
375
399
|
process.exit(0);
|
|
376
400
|
}
|
|
401
|
+
if (gate.legacyEvidence) {
|
|
402
|
+
recordWarning(projectRoot, { ...activeApply, test_strategy: gate.strategy }, 'legacy_test_evidence',
|
|
403
|
+
'仅检测到旧版测试证据;本次兼容放行,请补录包含 run-id、exit code、TDD phase 和严格身份字段的 test_result');
|
|
404
|
+
process.exit(0);
|
|
405
|
+
}
|
|
377
406
|
|
|
378
407
|
const reason = buildReason(gate.strategy, gate.changeName, gate.capability);
|
|
379
408
|
recordWarning(projectRoot, { ...activeApply, test_strategy: gate.strategy }, 'apply_test_missing', 'Apply test gate blocked: no unit test evidence');
|
|
@@ -391,5 +420,6 @@ module.exports = {
|
|
|
391
420
|
detectMissingTddRed,
|
|
392
421
|
hasExistingWarning,
|
|
393
422
|
isRealTestDetails,
|
|
423
|
+
isStrictTestEvidence,
|
|
394
424
|
requiresUnitTests,
|
|
395
425
|
};
|