sillyspec 3.28.8 → 3.28.9

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 (61) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +222 -224
  2. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  3. package/.claude/skills/sillyspec-quick/SKILL.md +118 -117
  4. package/AGENTS.md +3 -2
  5. package/README.md +35 -11
  6. package/SKILL.md +24 -10
  7. 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
  8. package/package.json +1 -1
  9. package/src/change-risk-profile.js +20 -0
  10. package/src/config-schema.js +32 -0
  11. package/src/db.js +11 -3
  12. package/src/decision-distill.js +15 -3
  13. package/src/design-facts.js +33 -3
  14. package/src/docs-gate.js +105 -6
  15. package/src/doctor-diagnostics.js +224 -2
  16. package/src/friction-ledger.js +171 -0
  17. package/src/hooks/worktree-guard.js +13 -1
  18. package/src/index.js +188 -17
  19. package/src/knowledge-classify.js +363 -0
  20. package/src/knowledge-hits.js +74 -0
  21. package/src/knowledge-stats.js +180 -0
  22. package/src/progress/change-registry.js +183 -8
  23. package/src/progress/shared.js +3 -2
  24. package/src/progress.js +100 -8
  25. package/src/quick-gate-profile.js +251 -0
  26. package/src/quicklog.js +155 -14
  27. package/src/review-dispatch.js +2 -1
  28. package/src/run/command.js +107 -12
  29. package/src/run/complete-handlers.js +511 -21
  30. package/src/run/complete.js +54 -16
  31. package/src/run/concurrent-detect.js +98 -0
  32. package/src/run/gates.js +50 -9
  33. package/src/run/prompt.js +134 -5
  34. package/src/run/quick-audit.js +111 -13
  35. package/src/run/shared.js +76 -10
  36. package/src/run/stage.js +6 -0
  37. package/src/scan-diff.js +61 -30
  38. package/src/scan-postcheck.js +74 -0
  39. package/src/scan-refresh.js +477 -0
  40. package/src/scan-staleness.js +25 -13
  41. package/src/scope-audit.js +365 -35
  42. package/src/stage-contract.js +185 -2
  43. package/src/stage-review.js +23 -7
  44. package/src/stages/brainstorm.js +6 -0
  45. package/src/stages/execute.js +85 -3
  46. package/src/stages/knowledge.js +52 -3
  47. package/src/stages/quick.js +1 -1
  48. package/src/stages/verify.js +1 -0
  49. package/src/sync-noise.js +114 -6
  50. package/src/sync.js +135 -7
  51. package/src/task-review.js +292 -21
  52. package/src/verify-postcheck.js +20 -0
  53. package/src/verify-probes.js +340 -4
  54. package/src/worktree-apply.js +538 -44
  55. package/src/worktree-deps.js +100 -2
  56. package/src/worktree.js +117 -5
  57. package/templates/agents-instruction.md +2 -2
  58. package/templates/prompts/testcase-design.md +2 -1
  59. package/templates/prompts/verify-probes.md +4 -1
  60. package/.patch16-tmp.mjs +0 -169
  61. package/sa-rename-probe/src/new.js +0 -4
@@ -13,8 +13,8 @@
13
13
  * node 校验与 java 校验各说各话。新生态补一行表项即可;表外项目落 generic → n/a 不阻断。
14
14
  */
15
15
 
16
- import { existsSync, readFileSync, realpathSync, lstatSync, readdirSync, unlinkSync } from 'fs';
17
- import { join, isAbsolute, relative, resolve as resolvePath, sep as pathSep } from 'path';
16
+ import { existsSync, readFileSync, realpathSync, lstatSync, readdirSync, readlinkSync, unlinkSync } from 'fs';
17
+ import { join, dirname, isAbsolute, relative, resolve as resolvePath, sep as pathSep } from 'path';
18
18
  import { fileURLToPath } from 'url';
19
19
  import { execFileSync } from 'child_process';
20
20
  import { createHash } from 'crypto';
@@ -387,6 +387,86 @@ function isSafeModulePath(p) {
387
387
  return !segs.includes('..');
388
388
  }
389
389
 
390
+ /**
391
+ * resolved 路径是否严格位于 root 目录之下(不含 root 自身)。win32 大小写不敏感比较由
392
+ * path.win32.relative 自带(与 detectEditableInstallEscape 的越界判定同口径)。
393
+ */
394
+ function isUnderDir(child, root) {
395
+ if (!child || !root) return false;
396
+ const rel = relative(resolvePath(root), resolvePath(child));
397
+ return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
398
+ }
399
+
400
+ /**
401
+ * ql-20260915-003 修复①(坑 junction-foreign-ownership,2026-09-15 用户实证:主仓
402
+ * frontend/node_modules 内部发现 junction 指向 session-export worktree 的 .pnpm——跨会话
403
+ * node_modules 污染,apply 后被迫 --force 重装)。
404
+ *
405
+ * 归属判定:链接方向 wt/node_modules → main 是 SillySpec 的正确设计(worktree 共享主仓
406
+ * 依赖);反向——main node_modules 内出现指向 .sillyspec/.runtime/worktrees/ 之下目标的
407
+ * junction/symlink——是 install 兜底/包管理器穿透写坏留下的污染,任何合法流程都不依赖它,
408
+ * 一律视为外来(foreign)。pnpm 自建的包链接(node_modules/<pkg> → 同 node_modules 内
409
+ * .pnpm/...)与全局 store 链接目标都不在 worktrees 区之下,零误伤。
410
+ *
411
+ * 扫描口径:main 的 node_modules(根 + 各子模块 node_modules)**顶层目录项**——单层
412
+ * readdir+lstat,不递归 .pnpm 内部(顶层已覆盖包链接;递归大仓 node_modules 的性能与
413
+ * 误删面都不可接受)。子模块清单:submodules 直传相对路径列表,或给 specBase 自动读
414
+ * local.yaml modules 块。
415
+ *
416
+ * 删除安全:junction/symlink 删除不跟随目标(win32 cmd /c rmdir / POSIX unlinkSync——与
417
+ * worktree.js unlinkNodeModulesLinks :82 同款安全删法);单个清理失败 fail-open 记入
418
+ * failed 不阻断(清扫是治理动作,不拦主流程)。
419
+ *
420
+ * @param {string} mainDir 主仓根目录(node_modules 所在)
421
+ * @param {{ worktreesRoot?: string, specBase?: string|null, submodules?: string[]|null, dryRun?: boolean, onlyUnder?: string|null }} opts
422
+ * - worktreesRoot 外来边界:resolved target 落在其下才算外来(缺省直接空手而归)
423
+ * - submodules 为 null 且给了 specBase 时,自动读 local.yaml modules 块补子模块清单
424
+ * - dryRun=true 只报清单零写盘
425
+ * - onlyUnder 进一步收窄:只认 target 落在该目录之下的(install 兜底前只清指向本 worktree 的)
426
+ * @returns {{ foreign: Array<{link: string, target: string}>, removed: Array<{link: string, target: string}>, failed: Array<{link: string, error: string}> }}
427
+ * dryRun 时 foreign=将清清单、removed/failed 恒空;非 dryRun 时 removed=实际已清、failed=清理失败
428
+ */
429
+ export function sweepForeignNodeModulesJunctions(mainDir, { worktreesRoot, specBase = null, submodules = null, dryRun = false, onlyUnder = null } = {}) {
430
+ const result = { foreign: [], removed: [], failed: [] };
431
+ if (!mainDir || !worktreesRoot || !existsSync(mainDir)) return result;
432
+ let mods = submodules;
433
+ if (mods === null && specBase) mods = extractModulePaths(readLocalYaml(specBase, null));
434
+ const nmDirs = [join(mainDir, 'node_modules')];
435
+ for (const sp of Array.isArray(mods) ? mods : []) {
436
+ if (!isSafeModulePath(sp)) continue;
437
+ nmDirs.push(join(mainDir, sp, 'node_modules'));
438
+ }
439
+ for (const nmDir of nmDirs) {
440
+ let names;
441
+ try { names = readdirSync(nmDir); } catch { continue; } // 目录不存在/不可读 → 该层跳过
442
+ for (const nm of names) {
443
+ const linkPath = join(nmDir, nm);
444
+ let st;
445
+ try { st = lstatSync(linkPath); } catch { continue; }
446
+ if (!st.isSymbolicLink()) continue; // junction(reparse point)与 symlink 的 lstat 均报 isSymbolicLink
447
+ let target;
448
+ try { target = readlinkSync(linkPath); } catch { continue; }
449
+ // 相对 target(pnpm 自建包链接常态)相对链接所在目录解析
450
+ const resolved = resolvePath(dirname(linkPath), target);
451
+ if (!isUnderDir(resolved, worktreesRoot)) continue;
452
+ if (onlyUnder && !isUnderDir(resolved, onlyUnder)) continue;
453
+ result.foreign.push({ link: linkPath, target: resolved });
454
+ if (dryRun) continue;
455
+ try {
456
+ if (process.platform === 'win32') {
457
+ execFileSync('cmd.exe', ['/c', 'rmdir', linkPath], { stdio: ['pipe', 'pipe', 'pipe'] });
458
+ } else {
459
+ unlinkSync(linkPath);
460
+ }
461
+ result.removed.push({ link: linkPath, target: resolved });
462
+ } catch (e) {
463
+ result.failed.push({ link: linkPath, error: e.message });
464
+ }
465
+ }
466
+ }
467
+ return result;
468
+ }
469
+
390
470
  /**
391
471
  * 对单个子模块目录 tryLink main 的 node_modules → wt 的 node_modules(modules 子模块专用)。
392
472
  * 仅走 link 快路径(不 install——子模块 install 慢且易失败;lockfile 不一致时交给用户 pnpm install)。
@@ -464,6 +544,24 @@ export function provisionDeps(worktreePath, mainCwd, opts = {}) {
464
544
  }
465
545
  if (!linked) {
466
546
  // 兜底:install
547
+ // ql-20260915-003 修复①(坑 junction-foreign-ownership)防再犯:install 在 worktree cwd
548
+ // 执行,若 main node_modules 内残留指向**本次 worktree** 的外来 junction,包管理器会经
549
+ // 反向链接穿透写坏主仓 node_modules(2026-09-15 实证的污染源头)——先清再装。
550
+ // 只清指向本 worktree 的(onlyUnder),全量治理交给 create/cleanup 挂点
551
+ // (worktree.js _sweepMainForeignJunctionLinks)。
552
+ if (mainCwd && existsSync(join(mainCwd, 'node_modules'))) {
553
+ try {
554
+ const preSweep = sweepForeignNodeModulesJunctions(mainCwd, {
555
+ worktreesRoot: resolvePath(mainCwd, '.sillyspec', '.runtime', 'worktrees'),
556
+ submodules: extractModulePaths(yamlText),
557
+ onlyUnder: worktreePath,
558
+ });
559
+ if (preSweep.removed.length > 0) {
560
+ console.warn(`⚠️ install 前清理:主仓 node_modules 内检测到指向本 worktree 的外来 junction(坑 junction-foreign-ownership),已移除 ${preSweep.removed.length} 个`);
561
+ for (const r of preSweep.removed) console.warn(` ${r.link} → ${r.target}`);
562
+ }
563
+ } catch { /* 清扫自身异常 fail-open,交 install 自然暴露问题 */ }
564
+ }
467
565
  const installResult = tryInstall(installCmd, worktreePath, timeout);
468
566
  result = installResult.ok
469
567
  ? { depsStatus: 'installed', depsMethod: 'install', depsSource: 'install', depsLockHash: wtHash }
package/src/worktree.js CHANGED
@@ -12,7 +12,8 @@ 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
19
  import { git, gitQuiet } from './git-helper.js';
@@ -478,6 +479,44 @@ export class WorktreeManager {
478
479
  return resolve(this.worktreeBase, changeName);
479
480
  }
480
481
 
482
+ /**
483
+ * ql-20260915-003 修复①(坑 junction-foreign-ownership,2026-09-15 用户实证:主仓
484
+ * frontend/node_modules 内 junction 指向历史 worktree 的 .pnpm——跨会话 node_modules
485
+ * 污染):清扫主仓 node_modules(根 + local.yaml modules 子模块)内指向 worktrees 区的
486
+ * 外来 junction/symlink。正确链接方向只有 wt/node_modules → main;main 侧指向 worktrees
487
+ * 区的反向链接一律是 install 兜底/包管理器穿透写坏的污染,发现即清 + warn。
488
+ *
489
+ * 挂点:create 依赖供给前(5.7b,防新供给经污染链接写坏)与 cleanup(把本 worktree 可能
490
+ * 遗留的反向链接一并清)。清扫失败 fail-open 不阻断主流程(治理动作非卡点)。
491
+ *
492
+ * @param {string} label 调用场景(warn/details 文案用)
493
+ * @param {string[]|null} [details] 可选,cleanup details 兼容
494
+ * @returns {{ foreign: Array, removed: Array, failed: Array }} sweep 结果(异常时空集)
495
+ */
496
+ _sweepMainForeignJunctionLinks(label, details = null) {
497
+ try {
498
+ const mainRoot = this._resolveMainRepoRoot();
499
+ const sweep = sweepForeignNodeModulesJunctions(mainRoot, {
500
+ worktreesRoot: this.worktreeBase,
501
+ specBase: join(mainRoot, '.sillyspec'),
502
+ });
503
+ if (sweep.removed.length > 0) {
504
+ console.warn(`⚠️ 检测到主仓 node_modules 内指向历史 worktree 的外来 junction,已移除 ${sweep.removed.length} 个(坑 junction-foreign-ownership,${label})`);
505
+ for (const r of sweep.removed) console.warn(` ${r.link} → ${r.target}`);
506
+ if (details) details.push(`foreign junctions swept (${label}): ${sweep.removed.length}`);
507
+ }
508
+ if (sweep.failed.length > 0) {
509
+ console.warn(`⚠️ 主仓 node_modules 外来 junction 清理未全成(${sweep.failed.length} 个失败,可重跑 sillyspec worktree doctor 或手工 rmdir): ${sweep.failed.map(f => f.link).join('、')}`);
510
+ if (details) details.push(`foreign junction sweep partial fail (${label}): ${sweep.failed.map(f => f.link).join('; ')}`);
511
+ }
512
+ return sweep;
513
+ } catch (e) {
514
+ // fail-open:清扫自身异常不阻断 create/cleanup 主流程
515
+ console.warn(`⚠️ 主仓 node_modules 外来 junction 清扫跳过(${e && e.message ? e.message : e})`);
516
+ return { foreign: [], removed: [], failed: [] };
517
+ }
518
+ }
519
+
481
520
  /**
482
521
  * 读取 worktree 元数据
483
522
  * @param {string} changeName
@@ -696,6 +735,10 @@ export class WorktreeManager {
696
735
 
697
736
  // 5.8 依赖供给(change 2026-06-28-worktree-deps-provision)
698
737
  // baseline overlay 后让 worktree 立即可构建/测试;失败不阻断 create,只记 meta。
738
+ // 5.7b ql-20260915-003 修复①(坑 junction-foreign-ownership):依赖供给前清扫主仓
739
+ // node_modules 内指向历史 worktree 的外来 junction(反向链接污染,install 兜底穿透
740
+ // 写坏的源头)——发现即清 + warn,防新供给再经污染链接写坏主仓依赖。
741
+ this._sweepMainForeignJunctionLinks('worktree create');
699
742
  let deps = {};
700
743
  try {
701
744
  deps = provisionDeps(worktreePath, this.cwd, { specBase: join(this.cwd, '.sillyspec') }) || {};
@@ -1046,16 +1089,76 @@ export class WorktreeManager {
1046
1089
  }
1047
1090
  }
1048
1091
 
1092
+ /**
1093
+ * ql-20260915-003 修复②(坑 cleanup-meta-fallback,2026-09-15 用户实证:cleanup 报
1094
+ * 「mode: null 跳过清理」但 worktree 目录实际创建过——meta 在 apply 后丢失 + cwd/specRoot
1095
+ * 漂移让 getWorktreePath 解析到别处时,meta/worktreePath/metaDir 三缺被误判「什么都不
1096
+ * 存在」早退):meta 缺失时的 worktree 残留兜底探测。两路探针(异常一律 fail-open 视为
1097
+ * 未命中):
1098
+ * (a) `git worktree list --porcelain` 中分支 refs/heads/sillyspec/<name> 的注册路径——
1099
+ * git 注册是权威口径,不受本进程 worktreeBase 解析漂移影响;目录在不在都算命中
1100
+ * (注册残留本身就该 prune,目录在则连目录一起清)
1101
+ * (b) resolveRuntimeRoot 口径(cwd/.sillyspec 为 localSpecBase)下 worktrees/<name>
1102
+ * 目录存在性——平台 runtimeRoot / cwd 漂移时的第二解析口径
1103
+ * 全不命中 → hit:null + candidates(三路探针描述,供 CLI 诊断式跳过输出)。
1104
+ * @param {string} name changeName(已过 validateChangeName)
1105
+ * @returns {{ hit: { path: string, via: string }|null, candidates: string[] }}
1106
+ */
1107
+ _probeWorktreeRemnants(name) {
1108
+ const stdPath = this.getWorktreePath(name);
1109
+ const candidates = [`meta/目录探针(getWorktreePath 口径): ${stdPath} 不存在`];
1110
+ try {
1111
+ const out = gitQuiet(this.cwd, ['worktree', 'list', '--porcelain'], { timeout: 30000 });
1112
+ const wantBranch = `branch refs/heads/${BRANCH_PREFIX}${name}`;
1113
+ let curPath = null;
1114
+ let regHit = null;
1115
+ for (const line of String(out || '').split('\n')) {
1116
+ if (line.startsWith('worktree ')) curPath = line.slice('worktree '.length).trim();
1117
+ else if (curPath && line.trim() === wantBranch) { regHit = curPath; break; }
1118
+ }
1119
+ if (regHit) {
1120
+ candidates.push(`git 分支探针: 分支 ${BRANCH_PREFIX}${name} 注册于 ${regHit} —— 命中`);
1121
+ return { hit: { path: regHit, via: `git worktree list 分支 ${BRANCH_PREFIX}${name} 注册路径` }, candidates };
1122
+ }
1123
+ candidates.push(`git 分支探针: git worktree list 无分支 ${BRANCH_PREFIX}${name} 注册`);
1124
+ } catch {
1125
+ candidates.push(`git 分支探针: git worktree list 失败(fail-open 视为未命中)`);
1126
+ }
1127
+ try {
1128
+ const alt = join(resolveRuntimeRoot(null, join(this.cwd, '.sillyspec')), 'worktrees', name);
1129
+ if (existsSync(alt)) {
1130
+ candidates.push(`runtimeRoot 口径目录探针: ${alt} 存在 —— 命中`);
1131
+ return { hit: { path: alt, via: `resolveRuntimeRoot 口径目录 ${alt}` }, candidates };
1132
+ }
1133
+ candidates.push(`runtimeRoot 口径目录探针: ${alt} 不存在`);
1134
+ } catch {
1135
+ candidates.push('runtimeRoot 口径目录探针: 解析失败(fail-open 视为未命中)');
1136
+ }
1137
+ return { hit: null, candidates };
1138
+ }
1139
+
1049
1140
  cleanup(changeName, { force = false, maxRetries = 3 } = {}) {
1050
1141
  const name = validateChangeName(changeName);
1051
1142
  const meta = this.getMeta(name);
1052
- const worktreePath = this.getWorktreePath(name);
1143
+ let worktreePath = this.getWorktreePath(name);
1053
1144
  const metaDir = join(this.worktreeBase, name);
1054
1145
  const details = [];
1055
1146
 
1056
- // 幂等:什么都不存在 → 直接跳过
1147
+ // 幂等:什么都不存在 → 直接跳过。
1148
+ // ql-20260915-003 修复②(坑 cleanup-meta-fallback):meta 缺失时不轻信「三缺 = 什么都不
1149
+ // 存在」——先跑 _probeWorktreeRemnants 兜底(git 注册路径 + resolveRuntimeRoot 口径目录),
1150
+ // 任一命中 → 不走早退,按命中路径继续清理(mode 推断 'worktree',details 记兜底依据);
1151
+ // 全不命中才跳过,跳过结果带 probePaths 探针清单(CLI 诊断式输出 + doctor 指引)。
1057
1152
  if (!meta && !existsSync(worktreePath) && !existsSync(metaDir)) {
1058
- return { result: 'skipped', mode: null, details };
1153
+ const probes = this._probeWorktreeRemnants(name);
1154
+ if (probes.hit) {
1155
+ worktreePath = probes.hit.path;
1156
+ const note = `meta 缺失,目录/分支探测兜底(命中:${probes.hit.via})`;
1157
+ details.push(note);
1158
+ console.warn(`⚠️ ${note}`);
1159
+ } else {
1160
+ return { result: 'skipped', mode: null, details, probePaths: probes.candidates };
1161
+ }
1059
1162
  }
1060
1163
 
1061
1164
  const mode = meta?.mode || 'worktree';
@@ -1076,7 +1179,11 @@ export class WorktreeManager {
1076
1179
  if (!force) {
1077
1180
  const check = this.hasUnappliedChanges(name);
1078
1181
  if (check.hasChanges) {
1079
- console.error(`🚫 worktree cleanup 拒绝:${check.changedFiles.length} 个交付变更未落地主工作区 HEAD,清理会丢失代码。`);
1182
+ // ql-20260915-003 修复② 配套:no-meta fail-closed 拦截时 changedFiles 为空,裸报
1183
+ // 「0 个交付变更」让人误解——0 文件时改报无法判定的原因(保守保留语义不变)。
1184
+ console.error(check.changedFiles.length > 0
1185
+ ? `🚫 worktree cleanup 拒绝:${check.changedFiles.length} 个交付变更未落地主工作区 HEAD,清理会丢失代码。`
1186
+ : `🚫 worktree cleanup 拒绝:无法判定交付是否已全部落地(${check.reason || 'meta 缺失,保守保留'}),保守不清理。`);
1080
1187
  for (const f of check.changedFiles) console.error(` ${f}`);
1081
1188
  console.error(' 请先落地(sillyspec worktree apply <name>)或 commit 到分支,或显式 --force 强制清理。');
1082
1189
  return { result: 'blocked', mode, details: [...details, 'blocked: uncommitted deliverable changes'], residual: [] };
@@ -1098,6 +1205,11 @@ export class WorktreeManager {
1098
1205
  unlinkNodeModulesLinks(worktreePath, meta, details)
1099
1206
  }
1100
1207
 
1208
+ // ql-20260915-003 修复①(坑 junction-foreign-ownership):cleanup 时同款清扫主仓
1209
+ // node_modules——把本 worktree(及其他历史 worktree)可能遗留的反向链接一并清,
1210
+ // 否则污染 junction 永久滞留主仓,直到下次人工 --force 重装依赖才发现。
1211
+ this._sweepMainForeignJunctionLinks('worktree cleanup', details);
1212
+
1101
1213
  // 1. git worktree remove(带 retry)—— in-place 跳过:无 git worktree 注册,且 worktreePath 即主工作区
1102
1214
  let gitRemoveOk = false;
1103
1215
  if (!isInPlace && existsSync(worktreePath)) {
@@ -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 为准。
package/.patch16-tmp.mjs DELETED
@@ -1,169 +0,0 @@
1
- import { readFileSync, writeFileSync, rmSync } from 'node:fs'
2
-
3
- // ── ① task-review.js:四 writer provenance 戳 + emptyDiff 报错直显 ──
4
- {
5
- const f = 'src/task-review.js'
6
- let t = readFileSync(f, 'utf8')
7
-
8
- // helper(放 REVIEW_SCHEMA_VERSION 常量后)
9
- const a0 = "export const REVIEW_SCHEMA_VERSION = 1"
10
- const b0 = `export const REVIEW_SCHEMA_VERSION = 1
11
-
12
- /**
13
- * review.json 写入溯源戳(坑 review-json-mystery-overwrite,2026-09-12 驾驭第十七批①,
14
- * 用户实证 task-04 review 被覆写成空壳、gate 报零改动伪造重写恢复——四个 writer 均有
15
- * 覆盖守卫,无堆栈钉不死覆写者):每次写入附 writtenBy(写入通道 + PID)+ writtenAt,
16
- * emptyDiff 伪造 error 直显两字段——下次复现自曝身份,事后可归因。
17
- */
18
- export function reviewProvenanceStamp(channel) {
19
- return { writtenBy: \`\${channel}#pid\${process.pid}\`, writtenAt: new Date().toISOString() }
20
- }`
21
- if (!t.includes(a0)) { console.error('a0 missing'); process.exit(1) }
22
- t = t.replace(a0, b0)
23
-
24
- // writer 1/2:两处 draft(reviewerNotes 行后追加 stamp 到对象)——draft 对象构造尾
25
- const a1 = ` if (draftRepo) draft.repo = draftRepo
26
- // fail-open(对齐函数头契约 + execute-run-dir-fail-loud 测试锁定的「写失败返回统计不抛」):
27
- // 无归属草稿写入失败(如 tasks/ 被普通文件占位 ENOTDIR)降级 skipped,不崩整个草稿生成
28
- try {
29
- mkdirSync(reviewDir, { recursive: true })
30
- writeFileSync(reviewPath, JSON.stringify(draft, null, 2) + '\\n')`
31
- const b1 = ` if (draftRepo) draft.repo = draftRepo
32
- Object.assign(draft, reviewProvenanceStamp('generateTaskReviewDrafts:no-attribution'))
33
- // fail-open(对齐函数头契约 + execute-run-dir-fail-loud 测试锁定的「写失败返回统计不抛」):
34
- // 无归属草稿写入失败(如 tasks/ 被普通文件占位 ENOTDIR)降级 skipped,不崩整个草稿生成
35
- try {
36
- mkdirSync(reviewDir, { recursive: true })
37
- writeFileSync(reviewPath, JSON.stringify(draft, null, 2) + '\\n')`
38
- if (!t.includes(a1)) { console.error('a1 missing'); process.exit(1) }
39
- t = t.replace(a1, b1)
40
-
41
- const a2 = ` if (draftRepo) draft.repo = draftRepo
42
- mkdirSync(reviewDir, { recursive: true })
43
- writeFileSync(reviewPath, JSON.stringify(draft, null, 2) + '\\n')
44
- generated++
45
- }`
46
- const b2 = ` if (draftRepo) draft.repo = draftRepo
47
- Object.assign(draft, reviewProvenanceStamp('generateTaskReviewDrafts'))
48
- mkdirSync(reviewDir, { recursive: true })
49
- writeFileSync(reviewPath, JSON.stringify(draft, null, 2) + '\\n')
50
- generated++
51
- }`
52
- if (!t.includes(a2)) { console.error('a2 missing'); process.exit(1) }
53
- t = t.replace(a2, b2)
54
-
55
- // writer 3:adopt(merged 写回前)
56
- const a3 = ` try {
57
- if (mechanicsChanged || verdictTouched || evidenceFilled) {
58
- writeFileSync(reviewPath, JSON.stringify(merged, null, 2) + '\\n')`
59
- const b3 = ` try {
60
- if (mechanicsChanged || verdictTouched || evidenceFilled) {
61
- Object.assign(merged, reviewProvenanceStamp('adoptTaskReviewMechanics'))
62
- writeFileSync(reviewPath, JSON.stringify(merged, null, 2) + '\\n')`
63
- if (!t.includes(a3)) { console.error('a3 missing'); process.exit(1) }
64
- t = t.replace(a3, b3)
65
-
66
- // writer 4:writeTaskReview(schema 自检前)
67
- const a4 = ` const schemaResult = validateReviewSchema(review)
68
- if (!schemaResult.ok) {
69
- return { ok: false, errors: schemaResult.errors, warnings }
70
- }
71
- try {
72
- mkdirSync(reviewDir, { recursive: true })
73
- writeFileSync(reviewPath, JSON.stringify(review, null, 2) + '\\n')`
74
- const b4 = ` Object.assign(review, reviewProvenanceStamp('writeTaskReview' + (force ? ':force' : '')))
75
- const schemaResult = validateReviewSchema(review)
76
- if (!schemaResult.ok) {
77
- return { ok: false, errors: schemaResult.errors, warnings }
78
- }
79
- try {
80
- mkdirSync(reviewDir, { recursive: true })
81
- writeFileSync(reviewPath, JSON.stringify(review, null, 2) + '\\n')`
82
- if (!t.includes(a4)) { console.error('a4 missing'); process.exit(1) }
83
- t = t.replace(a4, b4)
84
-
85
- // emptyDiff 报错直显 provenance(validateTaskReviews 两处 emptyDiff error)
86
- const a5 = ` errors.push(\`\${taskId}: base..head(\${review.base.slice(0, 8)}..\${review.head.slice(0, 8)})无任何代码变更 — 评审了一个零改动的任务,review 疑似伪造\${scopeNote}\`)`
87
- const b5 = ` const prov = (review.writtenBy || review.writtenAt) ? \`[写入溯源:\${review.writtenBy || '?'} @ \${review.writtenAt || '?'}]\` : '[无写入溯源—— older writer 产物]'
88
- errors.push(\`\${taskId}: base..head(\${review.base.slice(0, 8)}..\${review.head.slice(0, 8)})无任何代码变更 — 评审了一个零改动的任务,review 疑似伪造\${scopeNote}\${prov}\`)`
89
- if (!t.includes(a5)) { console.error('a5 missing'); process.exit(1) }
90
- t = t.replace(a5, b5)
91
-
92
- writeFileSync(f, t)
93
- console.log('provenance ok')
94
- }
95
-
96
- // ── ② gate-snapshot.js:symlink-store 布局探测 → null 回退 ──
97
- {
98
- const f = 'src/run/gate-snapshot.js'
99
- let t = readFileSync(f, 'utf8')
100
- const a = `export function createGateSnapshot({ cwd, files, sourceRoot = null }) {
101
- let snapshotRoot = null
102
- try {
103
- // 前置:主仓须是 git 仓且有 HEAD(无 git 环境回退主仓现行为)
104
- git(cwd, ['rev-parse', 'HEAD'])`
105
- const b = `export function createGateSnapshot({ cwd, files, sourceRoot = null }) {
106
- let snapshotRoot = null
107
- try {
108
- // 前置:主仓须是 git 仓且有 HEAD(无 git 环境回退主仓现行为)
109
- git(cwd, ['rev-parse', 'HEAD'])
110
-
111
- // symlink-store 布局探测(坑 gate-snapshot-pnpm-store-break,2026-09-12 驾驭第十七批②,
112
- // 用户第三次踩「lint 沙箱临时目录跑 pnpm 必假败」):pnpm/bun/lerna 的 node_modules 内部
113
- // 是指向 store 的相对/绝对符号链接网——junction 进临时目录后跨根解析失效,实测必假败。
114
- // 与其让沙箱报无关错误逼 advisory,不如布局命中即作废快照回退主仓(宁可主仓口径)。
115
- const layoutHit = detectSymlinkStoreLayout(cwd)
116
- if (layoutHit) {
117
- console.warn(\`⚠️ 门禁快照对 \${layoutHit} 布局不可靠(node_modules 符号链接 store 经 junction 跨根失效,实测必假败)——跳过快照回退主仓实测;主仓失败再做污染归属鉴定\`)
118
- return null
119
- }`
120
- if (!t.includes(a)) { console.error('layout anchor missing'); process.exit(1) }
121
- t = t.replace(a, b)
122
-
123
- const helper = `
124
- /** symlink-store 包管理布局探测:命中即 junction 快照不可靠。返回布局名或 null。 */
125
- export function detectSymlinkStoreLayout(cwd) {
126
- try {
127
- if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
128
- if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) return 'bun'
129
- if (existsSync(join(cwd, 'lerna.json'))) return 'lerna'
130
- try {
131
- const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'))
132
- const pm = pkg && pkg.packageManager ? String(pkg.packageManager) : ''
133
- if (/pnpm@/.test(pm)) return 'pnpm(packageManager)'
134
- if (/yarn@/.test(pm)) return 'yarn(packageManager)'
135
- if (/bun@/.test(pm)) return 'bun(packageManager)'
136
- } catch { /* 无 package.json/损坏 → 仅锁文件判据 */ }
137
- return null
138
- } catch { return null }
139
- }
140
- `
141
- const anchor = '/**\n * 环境目录名集(根与子包通用) */'
142
- if (!t.includes(anchor)) { console.error('helper2 anchor missing'); process.exit(1) }
143
- t = t.replace(anchor, helper + '\n' + anchor)
144
- writeFileSync(f, t)
145
- console.log('layout ok')
146
- }
147
-
148
- // ── ③ verify-probes.js:probe1 剥 NEW: 前缀 ──
149
- {
150
- const f = 'src/verify-probes.js'
151
- let t = readFileSync(f, 'utf8')
152
- const a = ` let abs = join(cwd, e.path)
153
- if (!existsSync(abs)) {
154
- const wtAbs = wtRoot ? join(wtRoot, e.path) : null`
155
- const b = ` // NEW: 待建前缀剥离(坑 probe1-new-prefix-miss,2026-09-12 驾驭第十七批③,用户实证
156
- // 「NEW: 前缀文件已合入主仓」时 existsSync(join(cwd,'NEW:src/x')) 恒 miss 落 skippedFiles
157
- // 留 ⚠️ 噪声):design 清单的 NEW: 是「新建意图」标记,文件 apply 后已是无前缀实体——
158
- // 探针按目标文件扫(与 pathMatches 比对侧剥除同源)。
159
- const probePath = String(e.path).replace(/^NEW:\\s*/, '')
160
- let abs = join(cwd, probePath)
161
- if (!existsSync(abs)) {
162
- const wtAbs = wtRoot ? join(wtRoot, probePath) : null`
163
- if (!t.includes(a)) { console.error('probe anchor missing'); process.exit(1) }
164
- t = t.replace(a, b)
165
- writeFileSync(f, t)
166
- console.log('probe ok')
167
- }
168
-
169
- rmSync('.patch16-tmp.mjs', { force: true })
@@ -1,4 +0,0 @@
1
- a
2
- b
3
- c
4
- d