sillyspec 3.28.9 → 3.28.11
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/skills/sillyspec-execute/SKILL.md +223 -222
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
- package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/change-risk-profile.js +51 -3
- package/src/config-schema.js +51 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/design-facts.js +94 -18
- package/src/docs-check.js +14 -10
- package/src/docs-debt.js +7 -2
- package/src/docs-gate.js +42 -15
- package/src/endpoint-extractor.js +50 -2
- package/src/foreign-declared.js +25 -16
- package/src/hooks/worktree-guard.js +75 -2
- package/src/index.js +147 -2
- package/src/knowledge-classify.js +20 -1
- package/src/machine-interface.js +26 -0
- package/src/probe7-anchor-check.js +87 -0
- package/src/progress/stage-machine.js +34 -5
- package/src/progress.js +1 -1
- package/src/quicklog.js +240 -5
- package/src/run/command.js +25 -2
- package/src/run/complete-handlers.js +172 -1
- package/src/run/complete.js +143 -11
- package/src/run/gate-snapshot.js +372 -2
- package/src/run/gates.js +130 -12
- package/src/run/prompt.js +166 -7
- package/src/run/shared.js +81 -1
- package/src/scope-audit.js +101 -31
- package/src/stage-contract-spec.js +14 -0
- package/src/stage-contract.js +314 -8
- package/src/stage-review-checklist.js +3 -1
- package/src/stage-review.js +137 -2
- package/src/stages/archive.js +9 -5
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/execute.js +62 -8
- package/src/stages/knowledge.js +24 -3
- package/src/stages/plan-postcheck.js +74 -10
- package/src/stages/plan.js +8 -1
- package/src/stages/quick.js +5 -5
- package/src/stages/verify.js +3 -2
- package/src/sync-noise.js +57 -1
- package/src/sync.js +77 -4
- package/src/task-review.js +84 -29
- package/src/verify-facts-schema.js +78 -9
- package/src/verify-postcheck.js +439 -68
- package/src/verify-probes.js +1156 -45
- package/src/worktree-apply.js +323 -8
- package/src/worktree-deps.js +24 -1
- package/src/worktree.js +274 -10
- package/src/write-audit.js +55 -0
package/src/worktree.js
CHANGED
|
@@ -16,7 +16,10 @@ import { provisionDeps, checkDepsFreshness, detectEditableInstallEscape, sweepFo
|
|
|
16
16
|
import { resolveRuntimeRoot } from './run/shared.js';
|
|
17
17
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
18
18
|
import { openDatabase } from './db-engine.js';
|
|
19
|
-
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';
|
|
20
23
|
|
|
21
24
|
// meta.json 会被 hook 进程与其它 CLI 进程并发读取(worktree-guard / getMeta / create 幽灵判定),
|
|
22
25
|
// 必须原子写:半截 JSON 会让 getMeta 返回 null → 触发幽灵 worktree 强删(可能丢 gitignored 改动)。
|
|
@@ -44,6 +47,85 @@ function _chunkPathsPrivate(paths, maxChars = 8000) {
|
|
|
44
47
|
return batches
|
|
45
48
|
}
|
|
46
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
|
+
|
|
47
129
|
/**
|
|
48
130
|
* 解除 worktree 内全部 node_modules junction/symlink(根 + meta.depsModules 各子模块)。
|
|
49
131
|
*
|
|
@@ -722,7 +804,8 @@ export class WorktreeManager {
|
|
|
722
804
|
printSyncReport(syncDiagnostic, baseHash, name);
|
|
723
805
|
|
|
724
806
|
// 5.6 Dirty baseline overlay:将主工作区未提交变更同步到 worktree
|
|
725
|
-
|
|
807
|
+
// (传变更名启用并行会话声明隔离——他者在途文件不进本变更 baseline,D-001@v1)
|
|
808
|
+
const baselineResult = this._overlayBaseline(this.cwd, worktreePath, name);
|
|
726
809
|
const baselineFiles = baselineResult.files;
|
|
727
810
|
const baselineHash = baselineResult.baselineHash;
|
|
728
811
|
|
|
@@ -746,6 +829,18 @@ export class WorktreeManager {
|
|
|
746
829
|
deps = { depsStatus: 'failed', depsError: `provisionDeps crashed: ${e.message}` };
|
|
747
830
|
}
|
|
748
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
|
+
|
|
749
844
|
// 6. 写入 meta.json
|
|
750
845
|
const meta = {
|
|
751
846
|
name_zh: 'worktree 元数据',
|
|
@@ -767,6 +862,8 @@ export class WorktreeManager {
|
|
|
767
862
|
depsLockHash: deps.depsLockHash || null,
|
|
768
863
|
depsCheckedAt: deps.depsCheckedAt || null,
|
|
769
864
|
...(deps.depsError ? { depsError: deps.depsError } : {}),
|
|
865
|
+
// 5.9 实供清单(纯记录面,doctor/审计可读;空供给省略字段——存量 meta 缺省兼容)
|
|
866
|
+
...(suppliedFiles.length > 0 ? { supplyFiles: suppliedFiles } : {}),
|
|
770
867
|
...(adoptedBranch ? { adoptedBranch: true } : {}),
|
|
771
868
|
};
|
|
772
869
|
|
|
@@ -865,7 +962,7 @@ export class WorktreeManager {
|
|
|
865
962
|
baseHash = git(this.cwd, ['rev-parse', 'HEAD']);
|
|
866
963
|
}
|
|
867
964
|
|
|
868
|
-
const baselineResult = this._overlayBaseline(this.cwd, this.cwd);
|
|
965
|
+
const baselineResult = this._overlayBaseline(this.cwd, this.cwd, name);
|
|
869
966
|
const baselineFiles = baselineResult.files;
|
|
870
967
|
const baselineHash = baselineResult.baselineHash;
|
|
871
968
|
|
|
@@ -904,6 +1001,117 @@ export class WorktreeManager {
|
|
|
904
1001
|
return { branch: meta.branch, worktreePath, baseHash, mode: meta.mode };
|
|
905
1002
|
}
|
|
906
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
|
+
|
|
907
1115
|
/**
|
|
908
1116
|
* 构建 isolation 信息对象,用于写入 sillyspec.db(_writeIsolationToDB,isolation_status 列)
|
|
909
1117
|
* @param {string} changeName
|
|
@@ -1899,11 +2107,18 @@ export class WorktreeManager {
|
|
|
1899
2107
|
* baseline,apply 回 main 即随本变更交付,需人工隔离。spec 文档不参与 worktree 内
|
|
1900
2108
|
* 构建/测试(execute 的 spec 读写经 specDriftAnchor 锚回主仓),排除零功能损失;
|
|
1901
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 抛错。
|
|
1902
2116
|
* @param {string} mainCwd - 主工作区路径
|
|
1903
2117
|
* @param {string} worktreePath - worktree 路径
|
|
2118
|
+
* @param {string} [changeName] - 本变更名;缺省/空串不切分(存量两参直调路径零回归)
|
|
1904
2119
|
* @returns {Array<string>} overlay 的文件列表
|
|
1905
2120
|
*/
|
|
1906
|
-
_overlayBaseline(mainCwd, worktreePath) {
|
|
2121
|
+
_overlayBaseline(mainCwd, worktreePath, changeName = null) {
|
|
1907
2122
|
const files = [];
|
|
1908
2123
|
const errors = [];
|
|
1909
2124
|
// git pathspec 排除(execFileSync 数组传参不经 shell,字面安全):.sillyspec/ 整目录
|
|
@@ -1914,18 +2129,53 @@ export class WorktreeManager {
|
|
|
1914
2129
|
if (norm === '.sillyspec' || norm.startsWith('.sillyspec/')) excluded.push(f)
|
|
1915
2130
|
}
|
|
1916
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
|
+
}
|
|
1917
2163
|
|
|
1918
2164
|
try {
|
|
1919
2165
|
const excludedSpecFiles = [];
|
|
1920
|
-
// staged 变更(pathspec 排除 .sillyspec/——跨变更 spec 文档不进 baseline
|
|
1921
|
-
|
|
2166
|
+
// staged 变更(pathspec 排除 .sillyspec/——跨变更 spec 文档不进 baseline;
|
|
2167
|
+
// 他者声明文件切分后一并排除,--name-only 与 --binary 两组 diff 命令同步收窄,
|
|
2168
|
+
// files 清单只收 own——staged 输出即已剔除 foreign 的剩余部分)
|
|
1922
2169
|
const stagedAll = gitQuiet(mainCwd, ['diff', '--cached', '--name-only'], { timeout: 30000 }) || ''
|
|
1923
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 }) || ''
|
|
1924
2174
|
if (staged) {
|
|
1925
2175
|
try {
|
|
1926
2176
|
// 用 Buffer 模式读取,避免二进制 patch 被 UTF-8 解码损坏
|
|
1927
2177
|
// QUAL-01 收口:git-helper 新增 encoding:'buffer' 支持(二进制输出专用,跳过 trim)
|
|
1928
|
-
const patchBuf = git(mainCwd, ['diff', '--cached', '--binary',
|
|
2178
|
+
const patchBuf = git(mainCwd, ['diff', '--cached', '--binary', ...stagedPathspec], { encoding: 'buffer', timeout: 30000 });
|
|
1929
2179
|
if (patchBuf && patchBuf.length > 0) {
|
|
1930
2180
|
const patchFile = join(worktreePath, '.sillyspec-baseline-staged.patch');
|
|
1931
2181
|
try {
|
|
@@ -1943,14 +2193,16 @@ export class WorktreeManager {
|
|
|
1943
2193
|
files.push(...staged.split('\n').filter(Boolean));
|
|
1944
2194
|
}
|
|
1945
2195
|
|
|
1946
|
-
// unstaged 变更(同上 pathspec
|
|
1947
|
-
const unstaged = gitQuiet(mainCwd, ['diff', '--name-only', '--', '.', EXCLUDE_PATHSPEC], { timeout: 30000 }) || ''
|
|
2196
|
+
// unstaged 变更(同上 pathspec 排除 + 他者声明剔除;--binary 不带 --cached)
|
|
1948
2197
|
const unstagedAll = gitQuiet(mainCwd, ['diff', '--name-only'], { timeout: 30000 }) || ''
|
|
1949
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 }) || ''
|
|
1950
2202
|
if (unstaged) {
|
|
1951
2203
|
try {
|
|
1952
2204
|
// 用 Buffer 模式读取,避免二进制 patch 被 UTF-8 解码损坏(QUAL-01 收口,同上)
|
|
1953
|
-
const patchBuf = git(mainCwd, ['diff', '--binary',
|
|
2205
|
+
const patchBuf = git(mainCwd, ['diff', '--binary', ...unstagedPathspec], { encoding: 'buffer', timeout: 30000 });
|
|
1954
2206
|
if (patchBuf && patchBuf.length > 0) {
|
|
1955
2207
|
const patchFile = join(worktreePath, '.sillyspec-baseline-unstaged.patch');
|
|
1956
2208
|
try {
|
|
@@ -1971,6 +2223,8 @@ export class WorktreeManager {
|
|
|
1971
2223
|
const untracked = gitQuiet(mainCwd, ['ls-files', '--others', '--exclude-standard'], { timeout: 30000 }) || '';
|
|
1972
2224
|
const skippedDirs = [];
|
|
1973
2225
|
const skippedArtifacts = [];
|
|
2226
|
+
// untracked 道 foreign 切分(一次全量非 .sillyspec 集,循环内仅 Map 查询)
|
|
2227
|
+
const untrackedForeign = splitForeignLane(untracked)
|
|
1974
2228
|
if (untracked) {
|
|
1975
2229
|
for (const f of untracked.split('\n').filter(Boolean)) {
|
|
1976
2230
|
const norm = f.replace(/\\/g, '/')
|
|
@@ -1979,6 +2233,9 @@ export class WorktreeManager {
|
|
|
1979
2233
|
// baseline checkpoint——289MB tar.gz 入分支(FF 破坏 + 二进制永久入 git)的实际代价兜底
|
|
1980
2234
|
const artifactWhy = isCheckpointSkippableArtifact(join(mainCwd, f));
|
|
1981
2235
|
if (artifactWhy) { skippedArtifacts.push(`${f}(${artifactWhy})`); continue }
|
|
2236
|
+
// 他者会话声明的在途文件不复制(与 skipped-dir/artifact 同型跳过;明细已在切分时
|
|
2237
|
+
// 计入隔离清单)——quotepath 引号路径先剥引号归一再比对
|
|
2238
|
+
if (untrackedForeign.foreignSet.has(normalizeGitPath(f))) continue;
|
|
1982
2239
|
const r = copyUntrackedEntry(join(mainCwd, f), join(worktreePath, f));
|
|
1983
2240
|
if (r.status === 'copied') files.push(f);
|
|
1984
2241
|
else if (r.status === 'skipped-dir') skippedDirs.push(f);
|
|
@@ -1999,6 +2256,13 @@ export class WorktreeManager {
|
|
|
1999
2256
|
const preview = excludedUnique.slice(0, 8).join(', ') + (excludedUnique.length > 8 ? ` …(共 ${excludedUnique.length} 个)` : '')
|
|
2000
2257
|
console.log(`🧹 baseline overlay 已隔离 ${excludedUnique.length} 个 .sillyspec/ 未提交文件(跨变更 spec 文档不进本变更 baseline,留在主仓各自归属):${preview}`)
|
|
2001
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
|
+
}
|
|
2002
2266
|
|
|
2003
2267
|
if (files.length > 0) {
|
|
2004
2268
|
console.log(`📁 baseline overlay: ${files.length} 个未提交文件已同步到 worktree`);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* write-audit.js — 批量写入方留痕(2026-09-17 用户驾驭小结⑥②法证收口,ql 20260917-006)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:平台内容回放(platform resolve --take-platform / pull --spec)与 worktree apply 都是
|
|
5
|
+
* 「单命令内批量整文件重写、逐字节保留来源行尾」——sillyspec 自产文件一律 LF,而回放路径会把
|
|
6
|
+
* 他机编辑器写出的 CRLF 内容原样落盘。事后定位只能靠 mtime 考古(2026-09-17 实证:3 文件毫秒
|
|
7
|
+
* 级同 mtime + CRLF + 内容增量,「平台同步回写」被冤枉一整天后才还原真凶候选)。本模块在每个
|
|
8
|
+
* 批量写入口追加一条 JSONL 审计行(.sillyspec/.runtime/write-audit.jsonl):时间 / 触发命令 /
|
|
9
|
+
* 文件集 / CRLF 命中——下次「18:09 谁动了 design.md」查一行即中。
|
|
10
|
+
*
|
|
11
|
+
* 约束:fail-open(任何异常只丢审计不改写入行为);零依赖;单行 appendFileSync(并行会话
|
|
12
|
+
* 各自整行写入,Windows 下最坏交错按行丢失计,不损主行为)。
|
|
13
|
+
*/
|
|
14
|
+
import { appendFileSync, mkdirSync } from 'fs'
|
|
15
|
+
import { join, dirname } from 'path'
|
|
16
|
+
|
|
17
|
+
const CRLF_BUF = Buffer.from('\r\n')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 追加一条批量写入审计记录。
|
|
21
|
+
* @param {string} specDir .sillyspec 根(审计落 .runtime/write-audit.jsonl)
|
|
22
|
+
* @param {{ via: string, change?: string, files?: string[], crlfPaths?: string[], detail?: object }} record
|
|
23
|
+
* via=触发命令锚('platform-resolve-take-platform' | 'pull-spec-bundle' | 'worktree-apply')
|
|
24
|
+
* @returns {boolean} 是否落盘成功(fail-open:false 只损失审计)
|
|
25
|
+
*/
|
|
26
|
+
export function appendWriteAudit(specDir, record) {
|
|
27
|
+
try {
|
|
28
|
+
const p = join(specDir, '.runtime', 'write-audit.jsonl')
|
|
29
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
30
|
+
appendFileSync(p, JSON.stringify({ ts: new Date().toISOString(), ...record }) + '\n', 'utf8')
|
|
31
|
+
return true
|
|
32
|
+
} catch { return false }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* CRLF 命中探测(法证关键字段:本仓 sillyspec 自产文件一律 LF——落盘后呈 CRLF 的文件
|
|
37
|
+
* 几乎必来自「他机内容逐字节回放」或仓外工具写入)。
|
|
38
|
+
* @param {Array<[string, Buffer|string]>} entries [path, 写入内容] 对
|
|
39
|
+
* @returns {string[]} 含 \r\n 的路径(探测失败的条目不连坐)
|
|
40
|
+
*/
|
|
41
|
+
export function detectCrlfFiles(entries) {
|
|
42
|
+
const out = []
|
|
43
|
+
for (const pair of Array.isArray(entries) ? entries : []) {
|
|
44
|
+
if (!Array.isArray(pair)) continue
|
|
45
|
+
const [path, data] = pair
|
|
46
|
+
try {
|
|
47
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(String(data))
|
|
48
|
+
if (buf.includes(CRLF_BUF)) out.push(path)
|
|
49
|
+
} catch { /* 单条探测失败跳过 */ }
|
|
50
|
+
}
|
|
51
|
+
return out
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 文件清单截断帽(防大树/大面把审计行撑爆;截断时附 +N 标记由调用方自理) */
|
|
55
|
+
export const WRITE_AUDIT_FILE_CAP = 20
|