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.
- package/.claude/full.json +1 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -224
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +118 -117
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- 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
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/change-risk-profile.js +20 -0
- package/src/config-schema.js +62 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/db.js +11 -3
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +89 -20
- package/src/docs-gate.js +105 -6
- package/src/doctor-diagnostics.js +224 -2
- package/src/friction-ledger.js +171 -0
- package/src/hooks/worktree-guard.js +13 -1
- package/src/index.js +206 -18
- package/src/knowledge-classify.js +382 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/probe7-anchor-check.js +69 -0
- package/src/progress/change-registry.js +183 -8
- package/src/progress/shared.js +3 -2
- package/src/progress.js +100 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +164 -18
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +131 -13
- package/src/run/complete-handlers.js +511 -21
- package/src/run/complete.js +123 -23
- package/src/run/concurrent-detect.js +98 -0
- package/src/run/gate-snapshot.js +110 -2
- package/src/run/gates.js +141 -16
- package/src/run/prompt.js +221 -11
- package/src/run/quick-audit.js +111 -13
- package/src/run/shared.js +147 -11
- package/src/run/stage.js +6 -0
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +456 -56
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +23 -7
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/execute.js +85 -3
- package/src/stages/knowledge.js +76 -6
- package/src/stages/plan-postcheck.js +32 -5
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +170 -6
- package/src/sync.js +141 -9
- package/src/task-review.js +366 -46
- package/src/verify-postcheck.js +212 -55
- package/src/verify-probes.js +727 -16
- package/src/worktree-apply.js +685 -48
- package/src/worktree-deps.js +124 -3
- package/src/worktree.js +391 -15
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/.patch16-tmp.mjs +0 -169
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
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',
|
|
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
|
|
10
|
+
4. **小修复 / 小调整走 quick**(= 无需要落盘的设计决策的改动,选道判据见第 6 条):`sillyspec run quick`。
|
|
11
11
|
5. **执行顺序**:文档 → 读代码 → 写测试 → 写实现 → 跑测试 → 验收 → 更新文档。
|
|
12
|
-
6.
|
|
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
|
|
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 为准。
|