sillyspec 3.28.8 → 3.28.10

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 (69) hide show
  1. package/.claude/full.json +1 -0
  2. package/.claude/skills/sillyspec-execute/SKILL.md +222 -224
  3. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  4. package/.claude/skills/sillyspec-quick/SKILL.md +118 -117
  5. package/AGENTS.md +3 -2
  6. package/README.md +35 -11
  7. package/SKILL.md +24 -10
  8. package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
  9. package/package.json +1 -1
  10. package/src/change-list.js +73 -20
  11. package/src/change-risk-profile.js +20 -0
  12. package/src/config-schema.js +62 -0
  13. package/src/contract-matrix.js +84 -5
  14. package/src/cross-repo-reconcile.js +134 -0
  15. package/src/db.js +11 -3
  16. package/src/decision-distill.js +15 -3
  17. package/src/design-facts.js +89 -20
  18. package/src/docs-gate.js +105 -6
  19. package/src/doctor-diagnostics.js +224 -2
  20. package/src/friction-ledger.js +171 -0
  21. package/src/hooks/worktree-guard.js +13 -1
  22. package/src/index.js +206 -18
  23. package/src/knowledge-classify.js +382 -0
  24. package/src/knowledge-hits.js +74 -0
  25. package/src/knowledge-stats.js +180 -0
  26. package/src/probe7-anchor-check.js +69 -0
  27. package/src/progress/change-registry.js +183 -8
  28. package/src/progress/shared.js +3 -2
  29. package/src/progress.js +100 -8
  30. package/src/quick-gate-profile.js +251 -0
  31. package/src/quicklog.js +164 -18
  32. package/src/review-dispatch.js +2 -1
  33. package/src/run/command.js +131 -13
  34. package/src/run/complete-handlers.js +511 -21
  35. package/src/run/complete.js +123 -23
  36. package/src/run/concurrent-detect.js +98 -0
  37. package/src/run/gate-snapshot.js +110 -2
  38. package/src/run/gates.js +141 -16
  39. package/src/run/prompt.js +221 -11
  40. package/src/run/quick-audit.js +111 -13
  41. package/src/run/shared.js +147 -11
  42. package/src/run/stage.js +6 -0
  43. package/src/scan-diff.js +61 -30
  44. package/src/scan-postcheck.js +74 -0
  45. package/src/scan-refresh.js +477 -0
  46. package/src/scan-staleness.js +25 -13
  47. package/src/scope-audit.js +456 -56
  48. package/src/stage-contract.js +185 -2
  49. package/src/stage-review.js +23 -7
  50. package/src/stages/brainstorm.js +6 -0
  51. package/src/stages/cmd-existence.js +9 -3
  52. package/src/stages/execute.js +85 -3
  53. package/src/stages/knowledge.js +76 -6
  54. package/src/stages/plan-postcheck.js +32 -5
  55. package/src/stages/quick.js +1 -1
  56. package/src/stages/verify.js +1 -0
  57. package/src/sync-noise.js +170 -6
  58. package/src/sync.js +141 -9
  59. package/src/task-review.js +366 -46
  60. package/src/verify-postcheck.js +212 -55
  61. package/src/verify-probes.js +727 -16
  62. package/src/worktree-apply.js +685 -48
  63. package/src/worktree-deps.js +124 -3
  64. package/src/worktree.js +391 -15
  65. package/templates/agents-instruction.md +2 -2
  66. package/templates/prompts/testcase-design.md +2 -1
  67. package/templates/prompts/verify-probes.md +4 -1
  68. package/.patch16-tmp.mjs +0 -169
  69. package/sa-rename-probe/src/new.js +0 -4
package/src/worktree.js CHANGED
@@ -12,10 +12,14 @@ import { execFileSync } from 'child_process';
12
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, readlinkSync, unlinkSync, copyFileSync } from 'fs';
13
13
  import { join, resolve, dirname, relative, isAbsolute, basename } from 'path';
14
14
  import { createHash } from 'crypto';
15
- import { provisionDeps, checkDepsFreshness, detectEditableInstallEscape } from './worktree-deps.js';
15
+ import { provisionDeps, checkDepsFreshness, detectEditableInstallEscape, sweepForeignNodeModulesJunctions } from './worktree-deps.js';
16
+ import { resolveRuntimeRoot } from './run/shared.js';
16
17
  import { writeAtomicSync } from './fs-atomic.js';
17
18
  import { openDatabase } from './db-engine.js';
18
- import { git, gitQuiet } from './git-helper.js';
19
+ import { git, gitQuiet, unquoteGitPath } from './git-helper.js';
20
+ // foreign-declared 零环独立模块(仅依赖 change-list/git-helper/stages/plan-postcheck,
21
+ // 链上无回边)——contract-matrix/verify-probes/task-review 等处已有同款 import 先例
22
+ import { splitOwnVsForeignDiffFiles } from './foreign-declared.js';
19
23
 
20
24
  // meta.json 会被 hook 进程与其它 CLI 进程并发读取(worktree-guard / getMeta / create 幽灵判定),
21
25
  // 必须原子写:半截 JSON 会让 getMeta 返回 null → 触发幽灵 worktree 强删(可能丢 gitignored 改动)。
@@ -43,6 +47,85 @@ function _chunkPathsPrivate(paths, maxChars = 8000) {
43
47
  return batches
44
48
  }
45
49
 
50
+ // ── supplyFiles 生成物供给(坑③ / FR-03 / D-003@v1)──
51
+ // .gitignore 生成物(如 src/build-id.ts)不在 git 树也不进 untracked overlay
52
+ // (ls-files --others --exclude-standard 尊重 .gitignore),worktree 缺失致构建炸
53
+ // Failed to load url——create step 5.9 按 local.yaml worktree.supplyFiles 从主仓复制供给。
54
+ const SUPPLY_FILES_MAX = 200; // R-04 展开上限帽:超出警告并截断(防 dist/** 类误配展开风暴)
55
+ const SUPPLY_WALK_MAX_DEPTH = 6; // glob 枚举递归深度帽(跳过 node_modules/.git 后的务实兜底)
56
+
57
+ /**
58
+ * supplyFiles glob → 锚定全路径的 RegExp(自实现,零新依赖——D-003@v1)。
59
+ * 分隔符统一 /;正则元字符先转义,再还原通配语义:** → .*(跨层)、* → [^/]*(单层)。
60
+ * @param {string} pattern 相对仓根的 glob(如 src/build-*.ts、dist/** 、gen 目录多层 id.ts)
61
+ * @returns {RegExp} 锚定(^…$)的全路径匹配
62
+ */
63
+ function supplyGlobToRegExp(pattern) {
64
+ const norm = String(pattern).replace(/\\/g, '/');
65
+ let re = '^';
66
+ for (let i = 0; i < norm.length; i++) {
67
+ const ch = norm[i];
68
+ if (ch === '*' && norm[i + 1] === '*') { re += '.*'; i++; }
69
+ else if (ch === '*') re += '[^/]*';
70
+ else re += ch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
71
+ }
72
+ return new RegExp(re + '$');
73
+ }
74
+
75
+ /**
76
+ * 读主仓 .sillyspec/local.yaml 的 worktree.supplyFiles(string[])。
77
+ * 沿 hooks/worktree-guard.js parseSimpleYaml 同款轻量手写解析(本文件不引新 reader):
78
+ * 只取这一个白名单键路径(worktree: 段 → supplyFiles: 子键 → 缩进块列表 / inline flow 数组),
79
+ * 其余段一概不碰(无原型污染面)。文件缺失/解析异常/形态不符 → [](供给步空转,fail-open)。
80
+ * @param {string} mainCwd 主仓根
81
+ * @returns {string[]} 供给清单(配置原文,未展开)
82
+ */
83
+ function readSupplyFilesConfig(mainCwd) {
84
+ let content;
85
+ try {
86
+ content = readFileSync(join(mainCwd, '.sillyspec', 'local.yaml'), 'utf8');
87
+ } catch {
88
+ return [];
89
+ }
90
+ const items = [];
91
+ const stripQuotes = (v) => (
92
+ v.length >= 2 && ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) ? v.slice(1, -1) : v
93
+ );
94
+ let inWorktree = false;
95
+ let inList = false;
96
+ for (const raw of content.split('\n')) {
97
+ const noComment = raw.replace(/\s+#.*$/, ''); // 尾注释剥离(与 worktree-guard 同口径)
98
+ const trimmed = noComment.trim();
99
+ if (!trimmed || trimmed.startsWith('#')) continue;
100
+ const indent = noComment.length - noComment.trimStart().length;
101
+ if (indent === 0) {
102
+ inWorktree = /^worktree:\s*$/.test(trimmed); // worktree-hook: 等其他段不误入
103
+ inList = false;
104
+ continue;
105
+ }
106
+ if (!inWorktree) continue;
107
+ if (indent === 2) {
108
+ const m = trimmed.match(/^supplyFiles:\s*(.*)$/);
109
+ if (!m) { inList = false; continue; }
110
+ inList = true;
111
+ const rest = m[1].trim();
112
+ if (rest.startsWith('[') && rest.endsWith(']')) {
113
+ // inline flow 数组(supplyFiles: [a, b])——块列表之外的另一种常见写法,一并兼容
114
+ for (const part of rest.slice(1, -1).split(',')) {
115
+ const v = stripQuotes(part.trim());
116
+ if (v) items.push(v);
117
+ }
118
+ }
119
+ continue;
120
+ }
121
+ if (inList && trimmed.startsWith('- ')) {
122
+ const v = stripQuotes(trimmed.slice(2).trim());
123
+ if (v) items.push(v);
124
+ }
125
+ }
126
+ return items;
127
+ }
128
+
46
129
  /**
47
130
  * 解除 worktree 内全部 node_modules junction/symlink(根 + meta.depsModules 各子模块)。
48
131
  *
@@ -478,6 +561,44 @@ export class WorktreeManager {
478
561
  return resolve(this.worktreeBase, changeName);
479
562
  }
480
563
 
564
+ /**
565
+ * ql-20260915-003 修复①(坑 junction-foreign-ownership,2026-09-15 用户实证:主仓
566
+ * frontend/node_modules 内 junction 指向历史 worktree 的 .pnpm——跨会话 node_modules
567
+ * 污染):清扫主仓 node_modules(根 + local.yaml modules 子模块)内指向 worktrees 区的
568
+ * 外来 junction/symlink。正确链接方向只有 wt/node_modules → main;main 侧指向 worktrees
569
+ * 区的反向链接一律是 install 兜底/包管理器穿透写坏的污染,发现即清 + warn。
570
+ *
571
+ * 挂点:create 依赖供给前(5.7b,防新供给经污染链接写坏)与 cleanup(把本 worktree 可能
572
+ * 遗留的反向链接一并清)。清扫失败 fail-open 不阻断主流程(治理动作非卡点)。
573
+ *
574
+ * @param {string} label 调用场景(warn/details 文案用)
575
+ * @param {string[]|null} [details] 可选,cleanup details 兼容
576
+ * @returns {{ foreign: Array, removed: Array, failed: Array }} sweep 结果(异常时空集)
577
+ */
578
+ _sweepMainForeignJunctionLinks(label, details = null) {
579
+ try {
580
+ const mainRoot = this._resolveMainRepoRoot();
581
+ const sweep = sweepForeignNodeModulesJunctions(mainRoot, {
582
+ worktreesRoot: this.worktreeBase,
583
+ specBase: join(mainRoot, '.sillyspec'),
584
+ });
585
+ if (sweep.removed.length > 0) {
586
+ console.warn(`⚠️ 检测到主仓 node_modules 内指向历史 worktree 的外来 junction,已移除 ${sweep.removed.length} 个(坑 junction-foreign-ownership,${label})`);
587
+ for (const r of sweep.removed) console.warn(` ${r.link} → ${r.target}`);
588
+ if (details) details.push(`foreign junctions swept (${label}): ${sweep.removed.length}`);
589
+ }
590
+ if (sweep.failed.length > 0) {
591
+ console.warn(`⚠️ 主仓 node_modules 外来 junction 清理未全成(${sweep.failed.length} 个失败,可重跑 sillyspec worktree doctor 或手工 rmdir): ${sweep.failed.map(f => f.link).join('、')}`);
592
+ if (details) details.push(`foreign junction sweep partial fail (${label}): ${sweep.failed.map(f => f.link).join('; ')}`);
593
+ }
594
+ return sweep;
595
+ } catch (e) {
596
+ // fail-open:清扫自身异常不阻断 create/cleanup 主流程
597
+ console.warn(`⚠️ 主仓 node_modules 外来 junction 清扫跳过(${e && e.message ? e.message : e})`);
598
+ return { foreign: [], removed: [], failed: [] };
599
+ }
600
+ }
601
+
481
602
  /**
482
603
  * 读取 worktree 元数据
483
604
  * @param {string} changeName
@@ -683,7 +804,8 @@ export class WorktreeManager {
683
804
  printSyncReport(syncDiagnostic, baseHash, name);
684
805
 
685
806
  // 5.6 Dirty baseline overlay:将主工作区未提交变更同步到 worktree
686
- const baselineResult = this._overlayBaseline(this.cwd, worktreePath);
807
+ // (传变更名启用并行会话声明隔离——他者在途文件不进本变更 baseline,D-001@v1)
808
+ const baselineResult = this._overlayBaseline(this.cwd, worktreePath, name);
687
809
  const baselineFiles = baselineResult.files;
688
810
  const baselineHash = baselineResult.baselineHash;
689
811
 
@@ -696,6 +818,10 @@ export class WorktreeManager {
696
818
 
697
819
  // 5.8 依赖供给(change 2026-06-28-worktree-deps-provision)
698
820
  // baseline overlay 后让 worktree 立即可构建/测试;失败不阻断 create,只记 meta。
821
+ // 5.7b ql-20260915-003 修复①(坑 junction-foreign-ownership):依赖供给前清扫主仓
822
+ // node_modules 内指向历史 worktree 的外来 junction(反向链接污染,install 兜底穿透
823
+ // 写坏的源头)——发现即清 + warn,防新供给再经污染链接写坏主仓依赖。
824
+ this._sweepMainForeignJunctionLinks('worktree create');
699
825
  let deps = {};
700
826
  try {
701
827
  deps = provisionDeps(worktreePath, this.cwd, { specBase: join(this.cwd, '.sillyspec') }) || {};
@@ -703,6 +829,18 @@ export class WorktreeManager {
703
829
  deps = { depsStatus: 'failed', depsError: `provisionDeps crashed: ${e.message}` };
704
830
  }
705
831
 
832
+ // 5.9 生成物供给(坑③ / FR-03 / D-003@v1):.gitignore 生成物不在 git 树也不进 untracked
833
+ // overlay(ls-files --exclude-standard 遵循 .gitignore),worktree 缺失致构建炸
834
+ // Failed to load url——按 local.yaml worktree.supplyFiles 从主仓复制供给,实供清单记
835
+ // meta.supplyFiles。未配置时供给步空转零行为变化;供给物天然不进 assess/apply 面。
836
+ let suppliedFiles = [];
837
+ try {
838
+ suppliedFiles = this._supplyGeneratedFiles(worktreePath);
839
+ } catch (e) {
840
+ // fail-open:供给步整体异常只警告,绝不阻断 create(R-04 约束)
841
+ console.warn(`⚠️ worktree supplyFiles 供给步异常(fail-open 不阻断 create):${e.message}`);
842
+ }
843
+
706
844
  // 6. 写入 meta.json
707
845
  const meta = {
708
846
  name_zh: 'worktree 元数据',
@@ -724,6 +862,8 @@ export class WorktreeManager {
724
862
  depsLockHash: deps.depsLockHash || null,
725
863
  depsCheckedAt: deps.depsCheckedAt || null,
726
864
  ...(deps.depsError ? { depsError: deps.depsError } : {}),
865
+ // 5.9 实供清单(纯记录面,doctor/审计可读;空供给省略字段——存量 meta 缺省兼容)
866
+ ...(suppliedFiles.length > 0 ? { supplyFiles: suppliedFiles } : {}),
727
867
  ...(adoptedBranch ? { adoptedBranch: true } : {}),
728
868
  };
729
869
 
@@ -822,7 +962,7 @@ export class WorktreeManager {
822
962
  baseHash = git(this.cwd, ['rev-parse', 'HEAD']);
823
963
  }
824
964
 
825
- const baselineResult = this._overlayBaseline(this.cwd, this.cwd);
965
+ const baselineResult = this._overlayBaseline(this.cwd, this.cwd, name);
826
966
  const baselineFiles = baselineResult.files;
827
967
  const baselineHash = baselineResult.baselineHash;
828
968
 
@@ -861,6 +1001,117 @@ export class WorktreeManager {
861
1001
  return { branch: meta.branch, worktreePath, baseHash, mode: meta.mode };
862
1002
  }
863
1003
 
1004
+ /**
1005
+ * 生成物供给步(create step 5.9,坑③ / FR-03 / D-003@v1)。
1006
+ *
1007
+ * 读主仓 local.yaml 的 worktree.supplyFiles(string[],精确路径 + glob * 单层 / ** 多层,
1008
+ * 相对仓根),主仓侧展开枚举后逐文件复制进 worktree(mkdir 父目录 + copyFileSync)。
1009
+ * 约束:
1010
+ * - 未配置 / 零匹配:完全静默空转(零行为变化);glob 零命中按缺失警告(不阻断)。
1011
+ * - 展开上限帽 SUPPLY_FILES_MAX=200:超出警告并截断(R-04,防 dist/** 误配展开风暴)。
1012
+ * - 单文件复制异常:warn 后继续,绝不阻断 create(fail-open,不回滚已复制文件)。
1013
+ * - 目标已存在且内容不同:警告后覆盖(baseline overlay / 已检出版本可能已在场)。
1014
+ * - 枚举跳过 node_modules/.git,递归深度帽 SUPPLY_WALK_MAX_DEPTH=6;匹配源只列文件
1015
+ * (statSync 跟随 junction/symlink,与供给语义一致)。
1016
+ * - 供给物是 gitignore 物,天然不进 assess/apply 面(ls-files --exclude-standard),无需过滤。
1017
+ *
1018
+ * @private
1019
+ * @param {string} worktreePath worktree 根目录(源恒为主仓 this.cwd)
1020
+ * @returns {string[]} 实供清单(POSIX 相对路径,写入 meta.supplyFiles)
1021
+ */
1022
+ _supplyGeneratedFiles(worktreePath) {
1023
+ const patterns = readSupplyFilesConfig(this.cwd);
1024
+ if (!Array.isArray(patterns) || patterns.length === 0) return [];
1025
+ const mainCwd = this.cwd;
1026
+ const matches = []; // 展开命中(去重后,跨配置项合并)
1027
+ const missing = []; // 精确路径缺失 / glob 零命中 / 越界形态
1028
+ const seen = new Set();
1029
+ for (const raw of patterns) {
1030
+ if (typeof raw !== 'string' || !raw.trim()) continue;
1031
+ const pattern = raw.trim().replace(/\\/g, '/').replace(/^\.\//, '');
1032
+ // 越界形态(绝对路径 / 盘符 / .. 段)不供给——供给面必须落在主仓根内
1033
+ if (!pattern || pattern.startsWith('/') || /^[a-zA-Z]:/.test(pattern) || pattern.split('/').includes('..')) {
1034
+ missing.push(pattern || JSON.stringify(raw));
1035
+ continue;
1036
+ }
1037
+ if (!pattern.includes('*')) {
1038
+ // 精确路径:直接查主仓存在性(只列文件)
1039
+ let isFile = false;
1040
+ try { isFile = statSync(join(mainCwd, pattern)).isFile(); } catch {}
1041
+ if (isFile) {
1042
+ if (!seen.has(pattern)) { seen.add(pattern); matches.push(pattern); }
1043
+ } else {
1044
+ missing.push(pattern);
1045
+ }
1046
+ continue;
1047
+ }
1048
+ // glob:静态前缀(首个含 * 段之前)作枚举根,正则锚定全路径匹配。
1049
+ // 深度帽:** 限静态根下 SUPPLY_WALK_MAX_DEPTH 层;纯 * 模式路径段数固定,超段即停。
1050
+ const segs = pattern.split('/');
1051
+ let baseLen = 0;
1052
+ while (baseLen < segs.length && !segs[baseLen].includes('*')) baseLen++;
1053
+ const baseRel = segs.slice(0, baseLen).join('/');
1054
+ const re = supplyGlobToRegExp(pattern);
1055
+ const depthCap = pattern.includes('**') ? baseLen + SUPPLY_WALK_MAX_DEPTH : segs.length;
1056
+ const found = [];
1057
+ const walk = (absDir, relDir, segCount) => {
1058
+ if (segCount >= depthCap) return;
1059
+ let entries;
1060
+ try { entries = readdirSync(absDir, { withFileTypes: true }); } catch { return; }
1061
+ for (const e of entries) {
1062
+ if (e.name === 'node_modules' || e.name === '.git') continue;
1063
+ const rel = relDir ? `${relDir}/${e.name}` : e.name;
1064
+ if (e.isDirectory()) {
1065
+ walk(join(absDir, e.name), rel, segCount + 1);
1066
+ continue;
1067
+ }
1068
+ let isFile = false;
1069
+ try { isFile = statSync(join(absDir, e.name)).isFile(); } catch {}
1070
+ if (isFile && re.test(rel)) found.push(rel);
1071
+ }
1072
+ };
1073
+ walk(join(mainCwd, baseRel), baseRel, baseLen);
1074
+ if (found.length === 0) {
1075
+ missing.push(pattern); // glob 零命中同缺失口径(GWT-2)
1076
+ continue;
1077
+ }
1078
+ for (const rel of found) {
1079
+ if (!seen.has(rel)) { seen.add(rel); matches.push(rel); }
1080
+ }
1081
+ }
1082
+ if (missing.length > 0) {
1083
+ console.warn(`⚠️ worktree supplyFiles 主仓侧未命中 ${missing.length} 项(不阻断 create,按需核对清单):${missing.join('、')}`);
1084
+ }
1085
+ // R-04 展开上限帽:排序保截断稳定可复现
1086
+ matches.sort();
1087
+ const capped = matches.slice(0, SUPPLY_FILES_MAX);
1088
+ if (matches.length > SUPPLY_FILES_MAX) {
1089
+ console.warn(`⚠️ worktree supplyFiles 展开命中 ${matches.length} 文件超上限 ${SUPPLY_FILES_MAX},已截断只供给前 ${SUPPLY_FILES_MAX}(检查 glob 是否误配,如 dist/** 全量展开)`);
1090
+ }
1091
+ const supplied = [];
1092
+ for (const rel of capped) {
1093
+ const srcAbs = join(mainCwd, rel);
1094
+ const dstAbs = join(worktreePath, rel);
1095
+ try {
1096
+ if (existsSync(dstAbs)) {
1097
+ // 目标已存在且内容不同 → 警告后覆盖(内容比对失败不拦复制)
1098
+ try {
1099
+ if (!readFileSync(dstAbs).equals(readFileSync(srcAbs))) {
1100
+ console.warn(`⚠️ worktree supplyFiles 覆盖 worktree 内已存在且内容不同的文件:${rel}`);
1101
+ }
1102
+ } catch {}
1103
+ }
1104
+ mkdirSync(dirname(dstAbs), { recursive: true });
1105
+ copyFileSync(srcAbs, dstAbs);
1106
+ supplied.push(rel);
1107
+ } catch (e) {
1108
+ // fail-open:单文件复制异常 warn 继续,不回滚已复制文件(R-04 约束)
1109
+ console.warn(`⚠️ worktree supplyFiles 复制失败(跳过继续):${rel} — ${e.message}`);
1110
+ }
1111
+ }
1112
+ return supplied;
1113
+ }
1114
+
864
1115
  /**
865
1116
  * 构建 isolation 信息对象,用于写入 sillyspec.db(_writeIsolationToDB,isolation_status 列)
866
1117
  * @param {string} changeName
@@ -1046,16 +1297,76 @@ export class WorktreeManager {
1046
1297
  }
1047
1298
  }
1048
1299
 
1300
+ /**
1301
+ * ql-20260915-003 修复②(坑 cleanup-meta-fallback,2026-09-15 用户实证:cleanup 报
1302
+ * 「mode: null 跳过清理」但 worktree 目录实际创建过——meta 在 apply 后丢失 + cwd/specRoot
1303
+ * 漂移让 getWorktreePath 解析到别处时,meta/worktreePath/metaDir 三缺被误判「什么都不
1304
+ * 存在」早退):meta 缺失时的 worktree 残留兜底探测。两路探针(异常一律 fail-open 视为
1305
+ * 未命中):
1306
+ * (a) `git worktree list --porcelain` 中分支 refs/heads/sillyspec/<name> 的注册路径——
1307
+ * git 注册是权威口径,不受本进程 worktreeBase 解析漂移影响;目录在不在都算命中
1308
+ * (注册残留本身就该 prune,目录在则连目录一起清)
1309
+ * (b) resolveRuntimeRoot 口径(cwd/.sillyspec 为 localSpecBase)下 worktrees/<name>
1310
+ * 目录存在性——平台 runtimeRoot / cwd 漂移时的第二解析口径
1311
+ * 全不命中 → hit:null + candidates(三路探针描述,供 CLI 诊断式跳过输出)。
1312
+ * @param {string} name changeName(已过 validateChangeName)
1313
+ * @returns {{ hit: { path: string, via: string }|null, candidates: string[] }}
1314
+ */
1315
+ _probeWorktreeRemnants(name) {
1316
+ const stdPath = this.getWorktreePath(name);
1317
+ const candidates = [`meta/目录探针(getWorktreePath 口径): ${stdPath} 不存在`];
1318
+ try {
1319
+ const out = gitQuiet(this.cwd, ['worktree', 'list', '--porcelain'], { timeout: 30000 });
1320
+ const wantBranch = `branch refs/heads/${BRANCH_PREFIX}${name}`;
1321
+ let curPath = null;
1322
+ let regHit = null;
1323
+ for (const line of String(out || '').split('\n')) {
1324
+ if (line.startsWith('worktree ')) curPath = line.slice('worktree '.length).trim();
1325
+ else if (curPath && line.trim() === wantBranch) { regHit = curPath; break; }
1326
+ }
1327
+ if (regHit) {
1328
+ candidates.push(`git 分支探针: 分支 ${BRANCH_PREFIX}${name} 注册于 ${regHit} —— 命中`);
1329
+ return { hit: { path: regHit, via: `git worktree list 分支 ${BRANCH_PREFIX}${name} 注册路径` }, candidates };
1330
+ }
1331
+ candidates.push(`git 分支探针: git worktree list 无分支 ${BRANCH_PREFIX}${name} 注册`);
1332
+ } catch {
1333
+ candidates.push(`git 分支探针: git worktree list 失败(fail-open 视为未命中)`);
1334
+ }
1335
+ try {
1336
+ const alt = join(resolveRuntimeRoot(null, join(this.cwd, '.sillyspec')), 'worktrees', name);
1337
+ if (existsSync(alt)) {
1338
+ candidates.push(`runtimeRoot 口径目录探针: ${alt} 存在 —— 命中`);
1339
+ return { hit: { path: alt, via: `resolveRuntimeRoot 口径目录 ${alt}` }, candidates };
1340
+ }
1341
+ candidates.push(`runtimeRoot 口径目录探针: ${alt} 不存在`);
1342
+ } catch {
1343
+ candidates.push('runtimeRoot 口径目录探针: 解析失败(fail-open 视为未命中)');
1344
+ }
1345
+ return { hit: null, candidates };
1346
+ }
1347
+
1049
1348
  cleanup(changeName, { force = false, maxRetries = 3 } = {}) {
1050
1349
  const name = validateChangeName(changeName);
1051
1350
  const meta = this.getMeta(name);
1052
- const worktreePath = this.getWorktreePath(name);
1351
+ let worktreePath = this.getWorktreePath(name);
1053
1352
  const metaDir = join(this.worktreeBase, name);
1054
1353
  const details = [];
1055
1354
 
1056
- // 幂等:什么都不存在 → 直接跳过
1355
+ // 幂等:什么都不存在 → 直接跳过。
1356
+ // ql-20260915-003 修复②(坑 cleanup-meta-fallback):meta 缺失时不轻信「三缺 = 什么都不
1357
+ // 存在」——先跑 _probeWorktreeRemnants 兜底(git 注册路径 + resolveRuntimeRoot 口径目录),
1358
+ // 任一命中 → 不走早退,按命中路径继续清理(mode 推断 'worktree',details 记兜底依据);
1359
+ // 全不命中才跳过,跳过结果带 probePaths 探针清单(CLI 诊断式输出 + doctor 指引)。
1057
1360
  if (!meta && !existsSync(worktreePath) && !existsSync(metaDir)) {
1058
- return { result: 'skipped', mode: null, details };
1361
+ const probes = this._probeWorktreeRemnants(name);
1362
+ if (probes.hit) {
1363
+ worktreePath = probes.hit.path;
1364
+ const note = `meta 缺失,目录/分支探测兜底(命中:${probes.hit.via})`;
1365
+ details.push(note);
1366
+ console.warn(`⚠️ ${note}`);
1367
+ } else {
1368
+ return { result: 'skipped', mode: null, details, probePaths: probes.candidates };
1369
+ }
1059
1370
  }
1060
1371
 
1061
1372
  const mode = meta?.mode || 'worktree';
@@ -1076,7 +1387,11 @@ export class WorktreeManager {
1076
1387
  if (!force) {
1077
1388
  const check = this.hasUnappliedChanges(name);
1078
1389
  if (check.hasChanges) {
1079
- console.error(`🚫 worktree cleanup 拒绝:${check.changedFiles.length} 个交付变更未落地主工作区 HEAD,清理会丢失代码。`);
1390
+ // ql-20260915-003 修复② 配套:no-meta fail-closed 拦截时 changedFiles 为空,裸报
1391
+ // 「0 个交付变更」让人误解——0 文件时改报无法判定的原因(保守保留语义不变)。
1392
+ console.error(check.changedFiles.length > 0
1393
+ ? `🚫 worktree cleanup 拒绝:${check.changedFiles.length} 个交付变更未落地主工作区 HEAD,清理会丢失代码。`
1394
+ : `🚫 worktree cleanup 拒绝:无法判定交付是否已全部落地(${check.reason || 'meta 缺失,保守保留'}),保守不清理。`);
1080
1395
  for (const f of check.changedFiles) console.error(` ${f}`);
1081
1396
  console.error(' 请先落地(sillyspec worktree apply <name>)或 commit 到分支,或显式 --force 强制清理。');
1082
1397
  return { result: 'blocked', mode, details: [...details, 'blocked: uncommitted deliverable changes'], residual: [] };
@@ -1098,6 +1413,11 @@ export class WorktreeManager {
1098
1413
  unlinkNodeModulesLinks(worktreePath, meta, details)
1099
1414
  }
1100
1415
 
1416
+ // ql-20260915-003 修复①(坑 junction-foreign-ownership):cleanup 时同款清扫主仓
1417
+ // node_modules——把本 worktree(及其他历史 worktree)可能遗留的反向链接一并清,
1418
+ // 否则污染 junction 永久滞留主仓,直到下次人工 --force 重装依赖才发现。
1419
+ this._sweepMainForeignJunctionLinks('worktree cleanup', details);
1420
+
1101
1421
  // 1. git worktree remove(带 retry)—— in-place 跳过:无 git worktree 注册,且 worktreePath 即主工作区
1102
1422
  let gitRemoveOk = false;
1103
1423
  if (!isInPlace && existsSync(worktreePath)) {
@@ -1787,11 +2107,18 @@ export class WorktreeManager {
1787
2107
  * baseline,apply 回 main 即随本变更交付,需人工隔离。spec 文档不参与 worktree 内
1788
2108
  * 构建/测试(execute 的 spec 读写经 specDriftAnchor 锚回主仓),排除零功能损失;
1789
2109
  * 排除清单显式打印保持可见(哪些跨变更文件被隔离、去哪了)。
2110
+ *
2111
+ * 并行会话声明隔离(坑 execute-baseline-overlay-carries-broken-parallel-wip,D-001@v1):
2112
+ * changeName 非空时,他者**显式声明**(他 quick 会话 guard.allowedFiles / 他变更 design
2113
+ * §6,own 优先)的在途文件三道剔除——staged/unstaged patch 道 :(exclude) pathspec 收窄
2114
+ * (worktree 保持基线 HEAD 版本)、untracked 道不复制;隔离清单(文件←归属者)显式打印
2115
+ * 一行。oracle 异常 fail-open 退回现状全量 overlay,绝不让 create 抛错。
1790
2116
  * @param {string} mainCwd - 主工作区路径
1791
2117
  * @param {string} worktreePath - worktree 路径
2118
+ * @param {string} [changeName] - 本变更名;缺省/空串不切分(存量两参直调路径零回归)
1792
2119
  * @returns {Array<string>} overlay 的文件列表
1793
2120
  */
1794
- _overlayBaseline(mainCwd, worktreePath) {
2121
+ _overlayBaseline(mainCwd, worktreePath, changeName = null) {
1795
2122
  const files = [];
1796
2123
  const errors = [];
1797
2124
  // git pathspec 排除(execFileSync 数组传参不经 shell,字面安全):.sillyspec/ 整目录
@@ -1802,18 +2129,53 @@ export class WorktreeManager {
1802
2129
  if (norm === '.sillyspec' || norm.startsWith('.sillyspec/')) excluded.push(f)
1803
2130
  }
1804
2131
  }
2132
+ // quotepath 归一:diff --name-only / ls-files 输出的非 ASCII 路径带双引号包裹,
2133
+ // 与声明集比对、生成 pathspec 排除串前先按 unquoteGitPath 口径剥引号再反斜杠→正斜杠
2134
+ const normalizeGitPath = (raw) => {
2135
+ let p = raw;
2136
+ if (p.length >= 2 && p.startsWith('"') && p.endsWith('"')) p = unquoteGitPath(p.slice(1, -1));
2137
+ return p.replace(/\\/g, '/');
2138
+ }
2139
+ const isSpecPath = (n) => n === '.sillyspec' || n.startsWith('.sillyspec/');
2140
+ // 他者声明文件的每道切分(staged/unstaged/untracked 各调一次,非逐文件):
2141
+ // 对该道全量输出的非 .sillyspec 集跑 own/foreign oracle,foreign → pathspec 排除串 +
2142
+ // Map(路径→owners[])+ 隔离明细;oracle 抛错 fail-open 退回该道全量(不阻断 create)
2143
+ const foreignIsolated = []; // 「文件←归属者」明细(三道汇总,尾部去重打印)
2144
+ const splitForeignLane = (rawAll) => {
2145
+ const excludeSpecs = [];
2146
+ const foreignSet = new Map();
2147
+ if (!changeName) return { excludeSpecs, foreignSet };
2148
+ try {
2149
+ const candidates = rawAll.split('\n').filter(Boolean).map(normalizeGitPath).filter(n => !isSpecPath(n));
2150
+ if (candidates.length === 0) return { excludeSpecs, foreignSet };
2151
+ const { foreign } = splitOwnVsForeignDiffFiles(mainCwd, changeName, candidates);
2152
+ for (const e of foreign) {
2153
+ foreignSet.set(e.file, e.owners);
2154
+ // 剥引号后的实际路径入 pathspec(git() 数组传参字面安全,与 :(exclude).sillyspec 同机制)
2155
+ excludeSpecs.push(':(exclude)' + e.file);
2156
+ foreignIsolated.push(`${e.file}←${e.owners.join(',')}`);
2157
+ }
2158
+ } catch (e) {
2159
+ console.warn(`⚠️ baseline overlay 并行会话声明切分失败(${e.message}),本道退回全量 overlay`);
2160
+ }
2161
+ return { excludeSpecs, foreignSet };
2162
+ }
1805
2163
 
1806
2164
  try {
1807
2165
  const excludedSpecFiles = [];
1808
- // staged 变更(pathspec 排除 .sillyspec/——跨变更 spec 文档不进 baseline)
1809
- const staged = gitQuiet(mainCwd, ['diff', '--cached', '--name-only', '--', '.', EXCLUDE_PATHSPEC], { timeout: 30000 }) || ''
2166
+ // staged 变更(pathspec 排除 .sillyspec/——跨变更 spec 文档不进 baseline;
2167
+ // 他者声明文件切分后一并排除,--name-only 与 --binary 两组 diff 命令同步收窄,
2168
+ // files 清单只收 own——staged 输出即已剔除 foreign 的剩余部分)
1810
2169
  const stagedAll = gitQuiet(mainCwd, ['diff', '--cached', '--name-only'], { timeout: 30000 }) || ''
1811
2170
  collectExcluded(stagedAll, excludedSpecFiles)
2171
+ const stagedForeign = splitForeignLane(stagedAll)
2172
+ const stagedPathspec = ['--', '.', EXCLUDE_PATHSPEC, ...stagedForeign.excludeSpecs]
2173
+ const staged = gitQuiet(mainCwd, ['diff', '--cached', '--name-only', ...stagedPathspec], { timeout: 30000 }) || ''
1812
2174
  if (staged) {
1813
2175
  try {
1814
2176
  // 用 Buffer 模式读取,避免二进制 patch 被 UTF-8 解码损坏
1815
2177
  // QUAL-01 收口:git-helper 新增 encoding:'buffer' 支持(二进制输出专用,跳过 trim)
1816
- const patchBuf = git(mainCwd, ['diff', '--cached', '--binary', '--', '.', EXCLUDE_PATHSPEC], { encoding: 'buffer', timeout: 30000 });
2178
+ const patchBuf = git(mainCwd, ['diff', '--cached', '--binary', ...stagedPathspec], { encoding: 'buffer', timeout: 30000 });
1817
2179
  if (patchBuf && patchBuf.length > 0) {
1818
2180
  const patchFile = join(worktreePath, '.sillyspec-baseline-staged.patch');
1819
2181
  try {
@@ -1831,14 +2193,16 @@ export class WorktreeManager {
1831
2193
  files.push(...staged.split('\n').filter(Boolean));
1832
2194
  }
1833
2195
 
1834
- // unstaged 变更(同上 pathspec 排除)
1835
- const unstaged = gitQuiet(mainCwd, ['diff', '--name-only', '--', '.', EXCLUDE_PATHSPEC], { timeout: 30000 }) || ''
2196
+ // unstaged 变更(同上 pathspec 排除 + 他者声明剔除;--binary 不带 --cached)
1836
2197
  const unstagedAll = gitQuiet(mainCwd, ['diff', '--name-only'], { timeout: 30000 }) || ''
1837
2198
  collectExcluded(unstagedAll, excludedSpecFiles)
2199
+ const unstagedForeign = splitForeignLane(unstagedAll)
2200
+ const unstagedPathspec = ['--', '.', EXCLUDE_PATHSPEC, ...unstagedForeign.excludeSpecs]
2201
+ const unstaged = gitQuiet(mainCwd, ['diff', '--name-only', ...unstagedPathspec], { timeout: 30000 }) || ''
1838
2202
  if (unstaged) {
1839
2203
  try {
1840
2204
  // 用 Buffer 模式读取,避免二进制 patch 被 UTF-8 解码损坏(QUAL-01 收口,同上)
1841
- const patchBuf = git(mainCwd, ['diff', '--binary', '--', '.', EXCLUDE_PATHSPEC], { encoding: 'buffer', timeout: 30000 });
2205
+ const patchBuf = git(mainCwd, ['diff', '--binary', ...unstagedPathspec], { encoding: 'buffer', timeout: 30000 });
1842
2206
  if (patchBuf && patchBuf.length > 0) {
1843
2207
  const patchFile = join(worktreePath, '.sillyspec-baseline-unstaged.patch');
1844
2208
  try {
@@ -1859,6 +2223,8 @@ export class WorktreeManager {
1859
2223
  const untracked = gitQuiet(mainCwd, ['ls-files', '--others', '--exclude-standard'], { timeout: 30000 }) || '';
1860
2224
  const skippedDirs = [];
1861
2225
  const skippedArtifacts = [];
2226
+ // untracked 道 foreign 切分(一次全量非 .sillyspec 集,循环内仅 Map 查询)
2227
+ const untrackedForeign = splitForeignLane(untracked)
1862
2228
  if (untracked) {
1863
2229
  for (const f of untracked.split('\n').filter(Boolean)) {
1864
2230
  const norm = f.replace(/\\/g, '/')
@@ -1867,6 +2233,9 @@ export class WorktreeManager {
1867
2233
  // baseline checkpoint——289MB tar.gz 入分支(FF 破坏 + 二进制永久入 git)的实际代价兜底
1868
2234
  const artifactWhy = isCheckpointSkippableArtifact(join(mainCwd, f));
1869
2235
  if (artifactWhy) { skippedArtifacts.push(`${f}(${artifactWhy})`); continue }
2236
+ // 他者会话声明的在途文件不复制(与 skipped-dir/artifact 同型跳过;明细已在切分时
2237
+ // 计入隔离清单)——quotepath 引号路径先剥引号归一再比对
2238
+ if (untrackedForeign.foreignSet.has(normalizeGitPath(f))) continue;
1870
2239
  const r = copyUntrackedEntry(join(mainCwd, f), join(worktreePath, f));
1871
2240
  if (r.status === 'copied') files.push(f);
1872
2241
  else if (r.status === 'skipped-dir') skippedDirs.push(f);
@@ -1887,6 +2256,13 @@ export class WorktreeManager {
1887
2256
  const preview = excludedUnique.slice(0, 8).join(', ') + (excludedUnique.length > 8 ? ` …(共 ${excludedUnique.length} 个)` : '')
1888
2257
  console.log(`🧹 baseline overlay 已隔离 ${excludedUnique.length} 个 .sillyspec/ 未提交文件(跨变更 spec 文档不进本变更 baseline,留在主仓各自归属):${preview}`)
1889
2258
  }
2259
+ // 并行会话声明隔离可见性:foreign 文件去重截断一行(对齐上方 .sillyspec/ 隔离打印
2260
+ // 样式)——哪些在途文件被隔离、归属谁,create 时刻可查,不需事后考古
2261
+ const foreignUnique = [...new Set(foreignIsolated)]
2262
+ if (foreignUnique.length > 0) {
2263
+ const preview = foreignUnique.slice(0, 8).join(', ') + (foreignUnique.length > 8 ? ` …(共 ${foreignUnique.length} 个)` : '')
2264
+ console.log(`🧹 baseline overlay 已隔离 ${foreignUnique.length} 个并行会话声明的在途文件(worktree 取基线 HEAD 版本,不随本变更 apply):${preview}`)
2265
+ }
1890
2266
 
1891
2267
  if (files.length > 0) {
1892
2268
  console.log(`📁 baseline overlay: ${files.length} 个未提交文件已同步到 worktree`);
@@ -7,9 +7,9 @@
7
7
  1. **禁止绕过本文件规则和 SillySpec 流程**。所有变更走 sillyspec 流程,不裸改裸提交。
8
8
  2. **改代码前必须先说明依据**——依据的文档路径(design.md / 模块文档 / file-lifecycle.md)或现有代码依据,无依据不改。
9
9
  3. **新功能 / 大改动走完整流程**:`brainstorm → plan → execute → verify → archive`。
10
- 4. **小修复 / 小调整走 quick**:`sillyspec run quick`。
10
+ 4. **小修复 / 小调整走 quick**(= 无需要落盘的设计决策的改动,选道判据见第 6 条):`sillyspec run quick`。
11
11
  5. **执行顺序**:文档 → 读代码 → 写测试 → 写实现 → 跑测试 → 验收 → 更新文档。
12
- 6. **判规模选档**:≤3 文件、范围明确走 `quick`;多阶段 / 架构级走完整流程。
12
+ 6. **判规模选档**:看本次改动有无需要落盘的设计决策——有(新能力 / 行为契约变更 / 跨模块取舍 / 多阶段推进)走完整流程,无(范围明确的局部修补)走 `quick`。文件数不是选道判据,只作出口绊线——改动大或跨模块时 quick 收尾门禁会自动加查(分级提示补文件注记 / 测试增量 / 模块文档认领)。
13
13
  7. **代码先行不补流程(倒推 B 模式)**:代码若已先写好,**不回头补 brainstorm/plan 装样子**——用 `quick --done` 收尾 + 补 quicklog 条目,把已落盘改动如实登记进进度库。
14
14
  8. **实证核验再 `--done`**:触及 `src`/`test` 的改动,CLI 会在 `quick --done` 时**亲自实测** `.sillyspec/local.yaml` 的 `commands.test` / `commands.lint`(门禁:实测失败阻断 --done 回 pending;纯 doc/配置与未配置命令自动跳过;倒推 B 模式按 --files 声明边界兜底判定)。agent 侧预跑**可选**——想省「--done 被拦→修复→重跑」一轮时才预跑;纯 doc/配置改动无需跑。以落盘文件与测试结果为准,不信口头"已完成"。
15
15
  9. **中途停下不靠额外命令存进度**——进度已由上一次 `--done` 自动落盘;恢复时用 `sillyspec progress show` 查看进度,再用 `sillyspec run <stage>` 续跑,不直接 commit 半成品。
@@ -9,4 +9,5 @@
9
9
  3. **行为 vs 实现**:走公开 API 测行为;重构后功能不变,测试不应失败。
10
10
  4. **契约与回归**:覆盖本次改动会破坏的既有行为与相邻模块接口;改公共签名/文案/常量时同步修复受影响既有测试并跑全量,禁止删断言凑绿。
11
11
  5. **时间敏感分支**:超时/重试/并发分支别真等——超时参数做成可注入,测试传毫秒级极小值或注入可控时钟推进,再断言超时后的行为(走哪个分支/返回什么/资源是否回收)。
12
- 6. **隔离确定性**:独立 fixture、可重复跑,不依赖共享状态/执行顺序。
12
+ 6. **隔离确定性**:独立 fixture、可重复跑,不依赖共享状态/执行顺序。
13
+ 7. **覆盖对账**:每条 TaskCard acceptance 至少对应一个测试用例,或显式标注 non-testable(文档/部署类——证据列写一句理由);对应关系由 verify 探针 7 矩阵机械核对(判定 covered/partial 须给测试锚点)——写测试前先读卡的 acceptance 列表逐条对齐,勿只覆盖顺手的主路径。
@@ -3,7 +3,7 @@
3
3
  > 🔧 **先跑 CLI 机械探针(一条命令)**:`sillyspec verify-probes --change <change-name> [--init]`
4
4
  > 输出已包含四个纯机械探针的结果(可直接进验证报告):**探针 1** 未实现标记扫描(design 清单文件逐行 TODO/FIXME 命中 + 行号)、**探针 3** 测试文件递归查找(含 co-located tests/,逐 task 覆盖)、**探针 5** API 契约对账表(endpoints.json × 前端调用 diff)、**探针 6** 删除对账三态(git 事实 × design 声明)。`--init` 顺带生成 verify-result.md 九章节骨架(探针结果已预填,`<!--TODO-->` 占位替换即可)。
5
5
  >
6
- > 下列条目中,只有**半语义部分**需要你执行:探针 2(关键词提取)、探针 3.4 集成盲区标注、探针 3.5 断言有效性抽查、探针 4(决策追踪闭环判定)。CLI 已跑的部分勿重复手跑——对 CLI 输出的 warning 项做语义复核即可。
6
+ > 下列条目中,只有**半语义部分**需要你执行:探针 2(关键词提取)、探针 3.4 集成盲区标注、探针 3.5 断言有效性抽查、探针 4(决策追踪闭环判定)、探针 7(验收×测试覆盖矩阵判定填槽)。CLI 已跑的部分勿重复手跑——对 CLI 输出的 warning 项做语义复核即可。
7
7
 
8
8
  **探针 2:设计关键词覆盖探针(你执行——关键词提取是语义)**
9
9
  1. 读取 design.md,从中提取所有能力关键词(如"登录"、"导出"、"批量"、"删除"、"搜索"等动作词)
@@ -31,3 +31,6 @@ CLI 的 missing backend endpoint 是 advisory(不硬阻断归档)——但 c
31
31
 
32
32
  **探针 6 补充(CLI 已出三态判定,你做终审)**
33
33
  以 git 事实为准(真实 > 声明);CLI 的三态(合规/高风险/未声明)是机械判定,是否构成 FAIL blocker 由你诚实判定,务必如实记录——静默删除代码是 verify 的最大盲区。
34
+
35
+ **探针 7 补充(CLI 已预填矩阵归属与提示,你填判定与证据)**
36
+ 验收×测试覆盖矩阵:CLI 机械半边已预填——逐 task 解析 TaskCard acceptance(frontmatter)、双源结构归属测试文件(allowed_paths 测试模式 ∪ execute review changedFiles 中 test/ 路径)、关键词命中提示(命中≠判定,仅提示)。你逐行填判定槽四枚举(covered / partial / uncovered / non-testable——non-testable 是文档/部署类显式逃生门)与证据:covered/partial 附测试锚点(`.test.` 文件或 file:line),non-testable 附一句理由;判定或证据未填 fail-closed,阻断 verify `--done`。与探针 3 口径差异:探针 3 查模块目录有没有测试文件(存在性面),探针 7 查每条 acceptance 由哪些测试承接(承接面);两者并排冲突以 7 为准。