sillyspec 3.29.0 → 3.29.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.29.0",
3
+ "version": "3.29.1",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -156,7 +156,7 @@ export const LOCAL_YAML_SCHEMA = {
156
156
  {
157
157
  id: 'gate_snapshot',
158
158
  title: '门禁隔离快照 copy 面',
159
- note: '门禁快照(gate-snapshot)= HEAD worktree + 会话文件 overlay + 环境目录 junction——gitignored 生成物(api-types/generated 类)不进 HEAD 也不在会话集,快照内 lint/test 环境性假败(2026-09-16 驾驭小结④,friction5-hardening R4 / D-002@v1)。按本清单在快照构建期从主仓 junction 链接补齐(失败回退复制);快照内 overlay 已覆盖的同名路径跳过(本变更最新态优先)。⚠️ junction 是活链接:快照内再跑生成命令会写穿到主仓该目录。',
159
+ note: '门禁快照(gate-snapshot)= HEAD worktree + 会话文件 overlay + 环境目录 junction——gitignored 生成物(类型生成/代码生成产物类)不进 HEAD 也不在会话集,快照内 lint/test 环境性假败(2026-09-16 驾驭小结④,friction5-hardening R4 / D-002@v1)。按本清单在快照构建期从主仓 junction 链接补齐(失败回退复制);快照内 overlay 已覆盖的同名路径跳过(本变更最新态优先)。⚠️ junction 是活链接:快照内再跑生成命令会写穿到主仓该目录。',
160
160
  keys: [
161
161
  { path: 'gate_snapshot.copy', type: 'array', optional: true, status: 'live', readers: ['createGateSnapshot (src/run/gate-snapshot.js)'], desc: '门禁隔离快照的生成物/额外路径 copy 面(元素 string,相对仓根):HEAD 快照缺 gitignored 生成物导致 lint/test 环境性假败;声明后快照构建期从主仓 junction 链接(失败回退复制)。⚠️ junction 是活链接,快照内再跑生成命令会写穿到主仓该目录。未配置/空清单 = 全段空转零行为变化。', example: 'gate_snapshot:\n copy:\n - src/generated' },
162
162
  { path: 'gate_snapshot.commands', type: 'array', optional: true, status: 'live', readers: ['parseGateSnapshotCommands + runGateSnapshotCommands (src/run/gate-snapshot.js — createGateSnapshot 环境预检后 copy 面前)'], desc: '门禁隔离快照的命令面(元素 string):快照构建期在环境目录链接后、copy 面前于快照根逐条执行(cwd=快照根、300s/条超时帽、非零退出/超时只 warn 继续——fail-open 不作废快照),产出「本仓应然态」生成物,收口主仓生成物缺失/过期(fresh clone 未跑 postinstall / build-id 类产物)时快照内全量 lint/test 必挂的坑(D-002@v2,2026-09-17-feedback-hardening FR-01)。⚠️ 环境目录(node_modules/venv)是活链接,命令写它们会穿透主仓——只放生成物命令(npm run gen:build-id 类),不放 install/build 全家桶。未配置 = 全段空转零行为。', example: 'npm run gen:build-id' },
@@ -0,0 +1,195 @@
1
+ /**
2
+ * redlines.js —— 红线机检评估器(2026-09-20-redline-machine-check)
3
+ *
4
+ * 为什么需要(2026-09-19 双实现对撞实验核心教训):语义级设计红线(「已结束会话
5
+ * 不得假运行」「用量归属不得后写覆盖」)活在历史 design/knowledge 散文里,无机器
6
+ * 可查形态——三层独立评审 + 352 例测试拦不住两处红线失分,靠事后人工逐文件深读
7
+ * 才发现。本模块把红线转成机器可查断言:消费者仓 `.sillyspec/redlines.yaml` 条目
8
+ * = forbid/require 正则 × scope 文件 glob × severity × origin 文档锚。
9
+ *
10
+ * 定位与边界(decisions.md D-001~D-004):
11
+ * - v1 = 模式级断言(人写正则人负责;statement/origin 保留散文溯源);不做
12
+ * AST/语义级,不做散文自动提炼。
13
+ * - 机制与内容分离:清单消费者仓自持;本仓缺清单 = 不适用(零打扰)。
14
+ * - fail-open 全链:坏 yaml→不适用+注记;单条求值异常→跳过+warn;任何红线面
15
+ * 故障不炸 verify 主链(探针 11 接线侧 try/catch fail-soft,同探针 8/9/10)。
16
+ * - advisory:探针渲染 ❌/⚠️ 进 verify-result 骨架供 agent 裁定;不进 PASS
17
+ * 封顶(D-003:攒误报率数据后另案升硬门)。
18
+ *
19
+ * 纯函数纪律:root 注入,不读 env/时钟;fs 只经 resolveScopeFiles 的 walk。
20
+ * 零新增外部依赖(js-yaml 已有)。
21
+ */
22
+ import { readdirSync, statSync, readFileSync } from 'fs';
23
+ import { join, relative } from 'path';
24
+ import jsYaml from 'js-yaml';
25
+
26
+ /** scope walk 排除目录(依赖/版本库/工具自身产物——含 .sillyspec:清单/进度文件不得喂饱 require 造成假阴性,S2 审查 D1)。 */
27
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.sillyspec', '.runtime', 'dist', 'build', '.next', '__pycache__']);
28
+
29
+ /** 单文件扫描上限:2MB(大产物/锁文件跳过,warn 留痕)。 */
30
+ const MAX_FILE_BYTES = 2 * 1024 * 1024;
31
+
32
+ /** glob → 正则:`**` 跨层、`*` 单段(不跨 /);段全等不认长名(src-guide 不蹭 src)。 */
33
+ function globToRegExp(glob) {
34
+ const esc = String(glob).replace(/[.+^${}()|[\]\\]/g, '\\$&');
35
+ return new RegExp(
36
+ '^' + esc.replace(/\*\*/g, '⟦⟦DBL⟧⟧').replace(/\*/g, '⟦SGL⟧')
37
+ .replace(/⟦⟦DBL⟧⟧/g, '(?:.*)').replace(/⟦SGL⟧/g, '[^/]*') + '$',
38
+ );
39
+ }
40
+
41
+ /** 递归收集 root 下全部文件(排除 SKIP_DIRS;返回仓根相对 POSIX 路径)。 */
42
+ function walkFiles(root) {
43
+ const out = [];
44
+ const visit = (dir) => {
45
+ let entries;
46
+ try {
47
+ entries = readdirSync(dir, { withFileTypes: true });
48
+ } catch {
49
+ return;
50
+ }
51
+ for (const e of entries) {
52
+ const p = join(dir, e.name);
53
+ if (e.isDirectory()) {
54
+ if (SKIP_DIRS.has(e.name)) continue;
55
+ visit(p);
56
+ } else if (e.isFile()) {
57
+ out.push(relative(root, p).replace(/\\/g, '/'));
58
+ }
59
+ }
60
+ };
61
+ visit(root);
62
+ return out;
63
+ }
64
+
65
+ /**
66
+ * 解析 redlines.yaml 文本为条目数组(纯函数)。
67
+ * 无效条目四判据(跳过 + warnings 留痕):缺 id / 缺 scope / forbid 与 require
68
+ * 全空 / 任一正则编译失败。severity 缺省 'error'。
69
+ * @param {string} yamlText
70
+ * @returns {{ entries: Array<object>, warnings: string[] }}
71
+ */
72
+ export function parseRedlines(yamlText) {
73
+ const warnings = [];
74
+ const doc = jsYaml.load(yamlText);
75
+ const rawList = doc && Array.isArray(doc.redlines) ? doc.redlines : [];
76
+ const entries = [];
77
+ for (const raw of rawList) {
78
+ if (!raw || typeof raw !== 'object') {
79
+ warnings.push('跳过非对象条目');
80
+ continue;
81
+ }
82
+ const id = typeof raw.id === 'string' && raw.id.trim() !== '' ? raw.id.trim() : null;
83
+ if (id === null) { warnings.push('跳过缺 id 条目'); continue; }
84
+ const scope = Array.isArray(raw.scope) ? raw.scope.filter((s) => typeof s === 'string' && s !== '') : [];
85
+ if (scope.length === 0) { warnings.push(`条目 ${id} 缺 scope,跳过`); continue; }
86
+ const forbid = Array.isArray(raw.forbid) ? raw.forbid.filter((s) => typeof s === 'string' && s !== '') : [];
87
+ const require_ = Array.isArray(raw.require) ? raw.require.filter((s) => typeof s === 'string' && s !== '') : [];
88
+ if (forbid.length === 0 && require_.length === 0) {
89
+ warnings.push(`条目 ${id} 的 forbid 与 require 全空,跳过`);
90
+ continue;
91
+ }
92
+ const severity = raw.severity === 'warning' ? 'warning' : 'error';
93
+ const entry = {
94
+ id,
95
+ statement: typeof raw.statement === 'string' ? raw.statement : '',
96
+ scope,
97
+ forbid,
98
+ require: require_,
99
+ severity,
100
+ origin: typeof raw.origin === 'string' ? raw.origin : null,
101
+ _res: { forbid: [], require: [] },
102
+ };
103
+ let badRe = false;
104
+ for (const re of forbid) {
105
+ try { entry._res.forbid.push(new RegExp(re, 'm')); } catch (e) { warnings.push(`条目 ${id} forbid 正则编译失败(${re}),跳过该条`); badRe = true; }
106
+ }
107
+ for (const re of require_) {
108
+ try { entry._res.require.push(new RegExp(re, 'm')); } catch (e) { warnings.push(`条目 ${id} require 正则编译失败(${re}),跳过该条`); badRe = true; }
109
+ }
110
+ if (badRe) continue;
111
+ entries.push(entry);
112
+ }
113
+ return { entries, warnings };
114
+ }
115
+
116
+ /**
117
+ * scope glob 列表 → 命中文件集(root 下仓根相对 POSIX 路径,排序去重)。
118
+ * @param {string[]} scopeGlobs
119
+ * @param {string} root
120
+ * @returns {string[]}
121
+ */
122
+ export function resolveScopeFiles(scopeGlobs, root) {
123
+ const all = walkFiles(root);
124
+ const res = (Array.isArray(scopeGlobs) ? scopeGlobs : [])
125
+ .filter((g) => typeof g === 'string' && g !== '')
126
+ .map((g) => globToRegExp(g));
127
+ return [...new Set(all.filter((f) => res.some((r) => r.test(f))))].sort();
128
+ }
129
+
130
+ /**
131
+ * 逐条评估红线:forbid 命中收集 file:line + require 缺失 + severity 透传。
132
+ * 单条求值异常跳过 + warnings(fail-open,D-004)。
133
+ * @param {object} opts
134
+ * @param {Array<object>} opts.entries - parseRedlines 产物
135
+ * @param {string} opts.root - 求值根(worktree 或仓根,绝对路径)
136
+ * @returns {{ applicable: boolean, entryCount: number, findings: Array<object>, warnings: string[] }}
137
+ */
138
+ export function evaluateRedlines({ entries, root }) {
139
+ const warnings = [];
140
+ const findings = [];
141
+ const list = Array.isArray(entries) ? entries : [];
142
+ for (const entry of list) {
143
+ try {
144
+ const files = resolveScopeFiles(entry.scope, root);
145
+ const contents = new Map();
146
+ const readIfBig = (rel) => {
147
+ if (!contents.has(rel)) {
148
+ const abs = join(root, rel);
149
+ let text = '';
150
+ try {
151
+ if (statSync(abs).size > MAX_FILE_BYTES) {
152
+ warnings.push(`条目 ${entry.id}:${rel} 超 2MB 跳过`);
153
+ } else {
154
+ text = readFileSync(abs, 'utf8');
155
+ }
156
+ } catch (e) {
157
+ warnings.push(`条目 ${entry.id}:读 ${rel} 失败(${e && e.code ? e.code : 'error'}),跳过该文件`);
158
+ }
159
+ contents.set(rel, text);
160
+ }
161
+ return contents.get(rel);
162
+ };
163
+ for (const rel of files) {
164
+ const text = readIfBig(rel);
165
+ if (!text) continue;
166
+ const lines = text.split('\n');
167
+ for (const re of entry._res ? entry._res.forbid : []) {
168
+ for (let i = 0; i < lines.length; i++) {
169
+ const m = lines[i] && re.exec(lines[i]);
170
+ if (m) {
171
+ findings.push({
172
+ id: entry.id, severity: entry.severity, kind: 'forbid',
173
+ file: rel, line: i + 1, snippet: String(m[0]).slice(0, 160),
174
+ statement: entry.statement, origin: entry.origin,
175
+ });
176
+ }
177
+ }
178
+ }
179
+ }
180
+ for (const re of entry._res ? entry._res.require : []) {
181
+ const satisfied = [...contents.values()].some((text) => re.test(text));
182
+ if (!satisfied) {
183
+ findings.push({
184
+ id: entry.id, severity: 'warning', kind: 'require-missing',
185
+ statement: entry.statement, origin: entry.origin,
186
+ detail: `require 模式在 scope 全集(${files.length} 文件)无命中`,
187
+ });
188
+ }
189
+ }
190
+ } catch (e) {
191
+ warnings.push(`条目 ${entry.id} 求值异常跳过(${e && e.message ? e.message : e})`);
192
+ }
193
+ }
194
+ return { applicable: true, entryCount: list.length, findings, warnings };
195
+ }
@@ -588,6 +588,18 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
588
588
  }
589
589
  }
590
590
 
591
+ // verify 批量完成检测(P0-1 verify 尾巴瘦身,2026-09-20 对撞实验驱动):报告产物齐
592
+ // (verify-result.md 结论枚举已填 + verify-facts.json 在场)且语义锚定步(step 1-2)
593
+ // 已过 → 剩余 step 一次性标 completed,本次 --done 直接进阶段完成分支——收尾门禁
594
+ // (测试对账/验收矩阵/module-impact 死信/PASS 封顶)照常全跑:批量省的是 --done
595
+ // 肥上下文往返,不省任何门。镜像 execute 批量模式(乐观预标戳 + 失败一并回滚)。
596
+ if (stageName === 'verify' && changeName) {
597
+ const _vb = detectVerifyBatchFinish({ changeName, specBase, steps })
598
+ if (_vb.batched && _vb.aligned > 0) {
599
+ console.log(`\n🚀 verify 批量完成:报告结论已填 + facts 在场,一次性补完 ${_vb.aligned} 个剩余 step → 进入阶段完成分支(收尾门禁照常全跑)`)
600
+ }
601
+ }
602
+
591
603
  const nextPendingIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
592
604
 
593
605
  if (nextPendingIdx === -1) {
@@ -1267,8 +1279,47 @@ export async function autoCheckPlanFromReviews({ stageName, changeName, cwd, pla
1267
1279
  * 安全门:注册表零 checkbox / 未全勾 / 代码零变更(unchanged)均不批量——信任声明但用代码核验兜底。
1268
1280
  * @returns {Promise<{batched:boolean, aligned:number, reason?:string, blockedTasks?:string[]}>}
1269
1281
  */
1270
- async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, platformOpts, steps }) {
1271
- if (stageName !== 'execute' || !changeName) return { batched: false, aligned: 0 }
1282
+ /**
1283
+ * verify 批量完成检测(P0-1,2026-09-20):镜像 execute 的 detectExecuteBatchFinish。
1284
+ * 条件(全部满足才批量):verify-result.md 结论枚举已填(三态任一,非待填占位)+
1285
+ * verify-facts.json 在场(探针跑过)+ 锚定步(step 1-2)已完成。批量只标剩余 step
1286
+ * completed,收尾门禁由阶段完成分支照常执行(批量不绕门)。
1287
+ */
1288
+ export function detectVerifyBatchFinish({ changeName, specBase, steps }) {
1289
+ if (!changeName) return { batched: false, aligned: 0 }
1290
+ try {
1291
+ const changeDir = join(specBase, 'changes', changeName)
1292
+ const reportPath = join(changeDir, 'verify-result.md')
1293
+ if (!existsSync(reportPath)) return { batched: false, aligned: 0, reason: 'verify-result.md 缺席(报告未写不批量)' }
1294
+ const report = readFileSync(reportPath, 'utf8')
1295
+ if (!/结论枚举[::]\s*`?\s*(PASS WITH NOTES|PASS|FAIL)/.test(report)) {
1296
+ return { batched: false, aligned: 0, reason: '结论枚举未填(待填占位不批量)' }
1297
+ }
1298
+ if (!existsSync(join(changeDir, 'verify-facts.json'))) {
1299
+ return { batched: false, aligned: 0, reason: 'verify-facts.json 缺席(探针未跑不批量)' }
1300
+ }
1301
+ const firstPendingIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
1302
+ if (firstPendingIdx === -1) return { batched: false, aligned: 0, reason: '无剩余 step' }
1303
+ if (firstPendingIdx < 2) {
1304
+ return { batched: false, aligned: 0, reason: '锚定步未过(step 1-2 须逐步完成,语义锚定不批量)' }
1305
+ }
1306
+ const now = new Date().toLocaleString('zh-CN', { hour12: false })
1307
+ let aligned = 0
1308
+ for (const step of steps) {
1309
+ if (step.status === 'pending' || step.status === 'in-progress') {
1310
+ step.status = 'completed'
1311
+ step.completedAt = now
1312
+ step._batchAligned = true // 乐观预标戳:gate 失败 rollback 一并回滚(execute 同款)
1313
+ aligned++
1314
+ }
1315
+ }
1316
+ return { batched: true, aligned }
1317
+ } catch (e) {
1318
+ return { batched: false, aligned: 0, reason: `批量完成检测异常(fail-open 单步推进):${e && e.message ? e.message : e}` }
1319
+ }
1320
+ }
1321
+
1322
+ async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, platformOpts, steps }) { if (stageName !== 'execute' || !changeName) return { batched: false, aligned: 0 }
1272
1323
  try {
1273
1324
  const changeDir = join(specBase, 'changes', changeName)
1274
1325
  const { total: planTotal, checked: planChecked } = pm.readPlanCheckboxStatus(changeDir)
@@ -381,10 +381,39 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
381
381
  // local.yaml 由下方 cfg 段显式复制(单一来源,不经 overlay)。
382
382
  const overlayRoot = sourceRoot || cwd
383
383
  let overlaid = 0
384
+ // 双写一致性(2026-09-20 红线机检变更实证:主 agent 在主仓直写而 execute 已建 worktree 时,
385
+ // overlay 源=worktree 拿陈旧内容盖掉快照 HEAD 的新版——快照内 import 找不到新导出→门禁假红)。
386
+ // 三方取新:sourceRoot 与 cwd 同文件内容不同时,选「与快照 HEAD 基线不同」的那份;
387
+ // 两份都变了且互不相等(真分叉)→ 显式警告 + 取 cwd(主仓直写=本会话最新编辑位约定)。
388
+ // 行尾归一(坑 autocrlf-byte-misjudge):本机 autocrlf=true 时快照 checkout 是 CRLF、
389
+ // 工作区文件常是 LF——字节直比会误判「两边都改」。比较一律 CRLF→LF 归一后做。
390
+ const normalizeEol = (buf) => String(buf).replace(/\r\n/g, '\n')
391
+ const headFileContent = (rel) => {
392
+ try { return normalizeEol(readFileSync(join(snapshotRoot, rel))) } catch { return null }
393
+ }
384
394
  for (const f of files) {
385
395
  const isSillyspecRuntime = typeof f === 'string' && (f.startsWith('.sillyspec/.runtime/') || f.startsWith('.sillyspec/quicklog/') || f === '.sillyspec/local.yaml' || f === '.sillyspec/.sillyspec-platform.json')
386
396
  if (typeof f !== 'string' || f.includes('..') || f.startsWith('/') || isSillyspecRuntime) continue
387
- const src = join(overlayRoot, f)
397
+ let src = join(overlayRoot, f)
398
+ if (sourceRoot && sourceRoot !== cwd) {
399
+ const wtPath = join(sourceRoot, f), cwdPath = join(cwd, f)
400
+ if (existsSync(wtPath) && existsSync(cwdPath)) {
401
+ let wtText = null, cwdText = null
402
+ try { wtText = normalizeEol(readFileSync(wtPath)) } catch { /* 读失败不介入 */ }
403
+ try { cwdText = normalizeEol(readFileSync(cwdPath)) } catch { /* 读失败不介入 */ }
404
+ if (wtText !== null && cwdText !== null && wtText !== cwdText) {
405
+ const base = headFileContent(f)
406
+ const wtDiff = base === null || wtText !== base
407
+ const cwdDiff = base === null || cwdText !== base
408
+ if (cwdDiff && !wtDiff) {
409
+ src = cwdPath // 主仓直写新、worktree 停在基线 → 取主仓
410
+ } else if (!(wtDiff && !cwdDiff)) {
411
+ console.warn(`⚠️ 快照双写分叉:${f} 在主仓与 worktree 均有不同修改——取主仓(本会话最新编辑位),请尽快对齐两边`)
412
+ src = cwdPath
413
+ } // wtDiff && !cwdDiff → 保持 worktree(正常流零改变)
414
+ }
415
+ }
416
+ }
388
417
  if (!existsSync(src)) continue
389
418
  const dst = join(snapshotRoot, f)
390
419
  mkdirSync(dirname(dst), { recursive: true })
@@ -145,7 +145,7 @@ export function printQuickAuditReview(review) {
145
145
  if (review.foreignSpecChurn.length > 6) console.warn(` … 共 ${review.foreignSpecChurn.length} 个`)
146
146
  }
147
147
  if (review.eolOnlyFiles && review.eolOnlyFiles.length > 0) {
148
- console.warn(`⚠️ ${review.eolOnlyFiles.length} 个文件仅行尾重写(内容零变化,gen:types/编辑器 CRLF 假 M——已剔出本会话审计):${review.eolOnlyFiles.slice(0, 5).join('、')}${review.eolOnlyFiles.length > 5 ? ' 等' : ''}`)
148
+ console.warn(`⚠️ ${review.eolOnlyFiles.length} 个文件仅行尾重写(内容零变化,生成命令/编辑器 CRLF 假 M——已剔出本会话审计):${review.eolOnlyFiles.slice(0, 5).join('、')}${review.eolOnlyFiles.length > 5 ? ' 等' : ''}`)
149
149
  console.warn(` 根治:.gitattributes 声明 text=auto eol=lf(或按文件类型钉死),生成器重写不再制造假 M`)
150
150
  }
151
151
  if (review.docsCheckHint && review.docsCheckHint.invalid > 0) {
@@ -415,7 +415,17 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
415
415
  // 倒推 B 模式兜底:审计口径为空时回退声明边界(文件早于会话启动被基线吸收的场景)
416
416
  const files = audited.length > 0 ? audited : (Array.isArray(declaredFiles) ? declaredFiles : [])
417
417
  const fileSource = audited.length > 0 ? '审计' : (files.length > 0 ? '声明边界兜底(倒推 B:文件早于会话启动被基线吸收)' : '无')
418
- const codeFiles = files.filter(f => typeof f === 'string' && (f.startsWith('src/') || f.startsWith('test/')))
418
+ // 代码文件判定(2026-09-19 monorepo 子包实证修复:multi-agent-platform 回带收口 6 个
419
+ // sillyhub-daemon/src/**、frontend/src/** 文件被「纯 doc/配置」误判跳过实测):旧口径只认
420
+ // 仓根 src/、test/ 前缀——monorepo 子包(<pkg>/src/**、backend/app/**.py)全漏。
421
+ // 新口径双通道,方向取「宁可多跑不可漏跑」(门禁漏跑=静默放行,多跑只是费一次实测):
422
+ // ① 路径段匹配:任意层出现 src / test / tests / __tests__ 段(src-guide 等长名不误蹭——按段全等)
423
+ // ② 代码扩展名兜底:src/test 目录约定外的代码(backend/app/**.py、scripts/*.go 等)
424
+ const CODE_PATH_SEGMENT_RE = /(^|\/)(src|tests?|__tests__)(\/|$)/
425
+ const CODE_EXTENSION_RE = /\.(?:js|mjs|cjs|ts|tsx|jsx|py|pyw|go|rs|java|kt|kts|rb|php|cs|c|h|cpp|cc|hpp|swift|scala|groovy|vue|svelte)$/
426
+ const codeFiles = files.filter(
427
+ (f) => typeof f === 'string' && (CODE_PATH_SEGMENT_RE.test(f) || CODE_EXTENSION_RE.test(f)),
428
+ )
419
429
  if (files.length === 0) {
420
430
  return { action: 'skip', failed: [], reason: `无变更文件清单(${fileSource}口径均空——brownfield 无 guard 或空审计),跳过`, test: null, lint: null }
421
431
  }
@@ -176,7 +176,7 @@ export async function renderVerifyTestAttribution({ cwd, changeName, specBase, r
176
176
  const { filterStaleBaselineOverlap } = await import('./gates.js')
177
177
  const actionable = filterStaleBaselineOverlap(overlap, dirtySet, foreignMap)
178
178
  if (actionable.length > 0) {
179
- console.error(` ℹ️ 归因提示②:本变更有 ${actionable.length} 个文件与主仓最近 10 条提交重叠且旧内容仍在工作区(${actionable.slice(0, 5).join(', ')}${actionable.length > 5 ? ' 等' : ''})——若为生成产物(api-types 等),本变更可能在旧基线生成并覆盖了已合入内容;先在新基线重跑生成命令(如 gen:types)再复验。(重叠共 ${overlap.length} 个,其余未在工作区改动或属他者会话在途文件,已归提示①/无需处理)`)
179
+ console.error(` ℹ️ 归因提示②:本变更有 ${actionable.length} 个文件与主仓最近 10 条提交重叠且旧内容仍在工作区(${actionable.slice(0, 5).join(', ')}${actionable.length > 5 ? ' 等' : ''})——若为生成产物(类型生成/代码生成产物等),本变更可能在旧基线生成并覆盖了已合入内容;先在新基线重跑生成命令再复验。(重叠共 ${overlap.length} 个,其余未在工作区改动或属他者会话在途文件,已归提示①/无需处理)`)
180
180
  }
181
181
  }
182
182
  }
@@ -106,7 +106,7 @@ AC-002: 未修改数据库 schema
106
106
  AC-003: 未涉及鉴权/权限
107
107
  AC-004: 未扩大 allowed_paths
108
108
  AC-005: 未引入新外部依赖
109
- AC-006: 未修改核心模块(workflow/daemon/session/lifecycle)
109
+ AC-006: 未修改核心模块(模块索引中被多方依赖或承担关键流程的模块)
110
110
  AC-007: 已有项目约定可复用
111
111
  AC-008: 不影响向后兼容
112
112
  AC-009: 不涉及数据迁移
@@ -76,7 +76,10 @@ export const definition = {
76
76
  maxWaitRounds: 8,
77
77
  waitReason: '等待用户回答需求问题或澄清',
78
78
  waitOptions: ['回答见--answer', '信息够了,进入方案讨论'],
79
- prompt: `通过对话探索需求细节、分析原型(如有)、判断拆分/批量、并对歧义点做需求澄清 Grill。本步骤合并了原有的对话探索、原型分析、需求范围评估、需求澄清 Grill 四个环节——按下方 A→D 顺序按需执行。提问遵守「一次只问一个」(见 D 节追问策略):必须问的问题逐个问,能查代码/文档解决的不问;不要把多个歧义打包成一屏清单一次抛给用户(用户记不住、答不全,反而漏掉关键约束);也不要为凑数把单个问题拖成多轮。
79
+ prompt: `### ⚡ 薄跑判定(P0-3,2026-09-20 对撞实验驱动——需求已收敛来源走全仪探索是纯税)
80
+ **需求来源已收敛**(满足任一:--input 显式声明承接/已澄清/方案已定;fork 或关联会话已多轮澄清过本需求,你已能不问自答全部 P0 歧义;任务书/评审结论直接给定方案与边界)→ 本步可**薄跑**:A-D 节按已收敛事实直书结论(歧义零清单、范围直裁、决策若已有则引用源会话不重问),一次性完成不再多轮追问。薄跑不降质量底线:范围结论与决策落盘照常,仅省「明知故问」的交互轮。Grill(Step 7)与规范文件(Step 8)**不适用薄跑、永不省**。
81
+
82
+ 通过对话探索需求细节、分析原型(如有)、判断拆分/批量、并对歧义点做需求澄清 Grill。本步骤合并了原有的对话探索、原型分析、需求范围评估、需求澄清 Grill 四个环节——按下方 A→D 顺序按需执行。提问遵守「一次只问一个」(见 D 节追问策略):必须问的问题逐个问,能查代码/文档解决的不问;不要把多个歧义打包成一屏清单一次抛给用户(用户记不住、答不全,反而漏掉关键约束);也不要为凑数把单个问题拖成多轮。
80
83
 
81
84
  ### A. 原型/设计图分析(如用户提供)
82
85
  如果用户提供了截图、图片或 HTML 原型,先分析提取:
@@ -297,12 +300,12 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
297
300
 
298
301
  **路径存在性核验(NEW: 前缀铁律,2026-09-17 用户反馈⑥-①前移)**:清单条目在「生成规范文件」步 \`--done\` 门禁会逐一核验仓内存在性——**计划新建的文件必须在路径前加 \`NEW:\` 前缀**(冒号后不加空格,\`NEW:src/foo.js\` 合法、\`NEW: src/foo.js\` 属书写错误);未加前缀的新建路径会以 \`design_file_ref_invalid\`(幻觉路径/书写错误)在末步被拦,返工一轮。已有文件写仓根相对正斜杠路径,禁绝对路径/glob。
299
302
  **字段数据流标注**(避免新增字段到 execute 才发现没透传):当清单含「新增/修改对外字段、接口、DTO、响应体、事件 payload、配置键」时,对应行「说明」列必须交代 producer→consumer 数据流——产出方(producer)→ 每跳流转/归一化点(序列化/反序列化/字段映射/默认值兜底)→ 消费方(consumer)。漏标 = 字段在某跳 dormant(声明了却没透传),到 execute/verify 才暴露,属设计层缺口。
300
- - 示例:「修改 daemon.ts:新增 budget_tokens,producer=backend/api-types.ts → ws_hub envelope 透传 → daemon.ts normalizeUsage(snake→camel)→ consumer=runtime/lease.ts 写 lease」
303
+ - 示例:「修改 api/server.ts:新增 quota_remaining 字段,producer=服务端 types/api.ts → 消息 envelope 透传 → 客户端 normalize(snake→camel)→ consumer=runtime/quota.ts 写入」
301
304
  - 仅改内部实现、无对外字段变动时,说明列照常写「新增 xx 方法」即可,无需数据流。
302
305
 
303
306
  **跨仓变更写法**(本次变更涉及 local.yaml repos: 注册的其他仓时必读;纯主仓变更跳过):
304
- - 清单**按仓分段**:每个仓一个小节,标题格式固定 \`## <repo-key> 仓变更\`(如 \`## sub-grid-security 仓变更\`;可加编号前缀或(跨仓,X1-X4)这类括号备注后缀,其余后缀/写法解析不了)。plan-postcheck 文件覆盖对账按此段头解析仓归属,写成「前端仓 / backend」等其他标题 → 对账不上且会点名段头格式错误
305
- - 段内路径相对**该仓根**写(\`src/routes/x.js\`);❌ 禁止带仓库名前缀(\`sub-grid-security/src/routes/x.js\`)、❌ 禁止绝对路径/盘符路径(\`C:/repo/src/...\`)——后续 TaskCard 的 allowed_paths 与 review 对账(\`git -C <仓根> diff\` 产仓根相对路径)都用同一口径,前缀/绝对路径永不命中
307
+ - 清单**按仓分段**:每个仓一个小节,标题格式固定 \`## <repo-key> 仓变更\`(如 \`## user-service 仓变更\`;可加编号前缀或(跨仓,X1-X4)这类括号备注后缀,其余后缀/写法解析不了)。plan-postcheck 文件覆盖对账按此段头解析仓归属,写成「前端仓 / backend」等其他标题 → 对账不上且会点名段头格式错误
308
+ - 段内路径相对**该仓根**写(\`src/routes/x.js\`);❌ 禁止带仓库名前缀(\`<repo-key>/src/routes/x.js\`)、❌ 禁止绝对路径/盘符路径(\`C:/repo/src/...\`)——后续 TaskCard 的 allowed_paths 与 review 对账(\`git -C <仓根> diff\` 产仓根相对路径)都用同一口径,前缀/绝对路径永不命中
306
309
  - 主仓文件放在第一个段头之前(无段头区域 = main),或单独写 \`## main 仓变更\` 段
307
310
  - repo-key 必须是 local.yaml repos: 段已注册的键(未注册先跑 \`sillyspec local register-repo <key> <仓根路径>\`,main 隐式不用注册)
308
311
 
@@ -886,7 +886,7 @@ export function buildWavePrompt(wave, waveIndex, changeDir, worktreePath, option
886
886
  if (relevantContracts.length > 0) {
887
887
  contractInjection = `
888
888
  ### API Contract Matrix
889
- 本 Wave 存在前端/后端跨 task 契约:
889
+ 本 Wave 存在跨 task 契约(provider → consumer):
890
890
  ${relevantContracts.map(c => `- **${c.consumer}** 消费 **${c.provider}** 产出的 API`).join('\n')}
891
891
  `
892
892
  for (const taskName of waveTasks) {
@@ -1236,7 +1236,7 @@ ${workdirLines}
1236
1236
 
1237
1237
  ⚠️ **铁律:spec 流程产物只写主仓 {SPEC_ROOT},绝不写进 worktree 副本**——包括 module-impact.md / knowledge 条目 / 模块卡与 \`<module>.changelog.md\` sidecar。在 worktree 内发现 \`.sillyspec/\` 目录是 checkout 副本,写进去的任何内容都会随 worktree cleanup 整目录蒸发(2026-09-10 实证:模块文档写副本、归档被迫 checkout 补救)。子代理 prompt 中涉及此类产物时,必须原样带上主仓绝对路径。
1238
1238
 
1239
- ⚠️ **Python 后端导入链陷阱(venv editable install,2026-09-18 实证 gen:types 坑)**:worktree 内跑任何 \`import app\` / dump_openapi / gen:types 类命令时,若用的是主仓 venv(junction 链接或裸调主仓 python),其 editable install(\`_editable_impl_*.pth\`)指向**主仓**绝对路径——\`import app\` 会静默解析到主仓旧代码,产出旧 schema/旧行为且零报错(看似「后端没生效」)。**跑生成链前设 \`PYTHONPATH=<worktree>/backend\`**(PYTHONPATH 优先于 .pth),或用 worktree 自建 venv;\`sillyspec worktree doctor\` 的 editable-install-escape 检查可提前暴露。
1239
+ ⚠️ **Python 导入链陷阱(venv editable install,2026-09-18 实证)**:worktree 内跑任何 \`import <项目包>\` / dump_openapi / 代码生成类命令时,若用的是主仓 venv(junction 链接或裸调主仓 python),其 editable install(\`_editable_impl_*.pth\`)指向**主仓**绝对路径——项目包 import 会静默解析到主仓旧代码,产出旧 schema/旧行为且零报错(看似「改动没生效」)。**跑生成链前设 \`PYTHONPATH=<worktree>/<项目源码根>\`**(PYTHONPATH 优先于 .pth),或用 worktree 自建 venv;\`sillyspec worktree doctor\` 的 editable-install-escape 检查可提前暴露。
1240
1240
  `
1241
1241
  : ''
1242
1242
  }
@@ -92,7 +92,7 @@ const stepClassify = {
92
92
  判定为 full(满足任一即升为 full):
93
93
  - 预计 8 个以上 task
94
94
  - 跨 3 个以上模块
95
- - 涉及 CLI + 平台 + DB 联动
95
+ - 跨技术层联动(同一变更需多层协同改造,如前端 + 服务端 + 数据库)
96
96
  - 涉及 agent 调度 / worktree / isolation 逻辑
97
97
  - 涉及复杂状态恢复(checkpoint / resume)
98
98
  - 需要并行 sub-agent 执行
@@ -387,7 +387,7 @@ execute/verify 阶段会按实际代码变更更新此文档;archive 阶段会
387
387
  // ═══════════════════════════════════════════════════════════════
388
388
 
389
389
  /**
390
- * 构建紧凑 TaskCard 协调器步骤(单步,子代理按 batch 并行写卡片)
390
+ * 构建紧凑 TaskCard 协调器步骤(默认主 agent 直填;>8 任务且跨模块/上下文紧张时按 batch 派子代理)
391
391
  * 每个 task 生成 20~40 行紧凑可执行卡片
392
392
  * 允许把多个相关 task 合并为一个 batch,由单个子代理一次生成多张卡片,
393
393
  * 在保留 plan 任务拆分完整性的同时减少子代理调用数量。
@@ -416,7 +416,7 @@ allowed_paths: # ⚠️ 路径相对 repo 声明的
416
416
  target_files: # 可选。本 task 计划要改动的文件清单(文件级意图,供对账;与 allowed_paths 语义不同——那是权限范围)。骨架已预置 [] 占位行,无明确意图保留 [] 不动
417
417
  - src/foo.js # ✅ 精确仓根相对路径(口径同 allowed_paths);计划新建文件加 NEW: 前缀(NEW:src/bar.js)。❌ 禁 glob(src/**)/目录前缀(src/dir/)/绝对路径
418
418
  provides: # 可选。仅当本 task 给其他 task 提供接口/DTO/响应时填
419
- - contract: <DTO或响应类型名> # 如 DaemonRuntimeRead
419
+ - contract: <DTO或响应类型名> # 如 UserDTO
420
420
  fields: [field_a, field_b]
421
421
  expects_from: # 可选。仅当本 task 消费其他 task 的契约时填
422
422
  <provider-task-id>: # 如 task-05(占位符,不要照抄)
@@ -439,7 +439,7 @@ constraints:
439
439
  - 边界约束 2(如:不修改传入参数)
440
440
  related_tests: # 可选。当本 task 改动会导致既有测试断言失效时填(共享源文件/UI文案/常量/签名变更等,判据=测试是否失败非文件是否共享)
441
441
  - path: frontend/src/lib/errors.test.ts # 因本次改动断言失效的既有测试文件
442
- reason: 旧断言假设单例,改归属键后需同步
442
+ reason: 旧断言依赖本次要重构掉的实现细节,需同步
443
443
  ---
444
444
 
445
445
  {{include: taskcard-rules}}`
@@ -472,17 +472,21 @@ ${taskList}
472
472
  \`\`\`
473
473
  幂等,已存在的卡跳过不覆盖;骨架带 LF 行尾 + 闭合 frontmatter + 硬校验 9 字段 + depends_on 反填。**只有主 agent 跑这一次,子代理一律不再运行 taskcard CLI。**
474
474
  1. 确认 \`${changeDir}/tasks/\` 目录存在(上一步预生成会自动创建)
475
- 2. **按 batch 分派子代理(减少总子代理数量,而不是一个 task 一个子代理):**
475
+ 2. **默认主 agent 自己填卡(P0-2,2026-09-20 对撞实验驱动——6 个填卡子代理 6.7M token 做的是主 agent 可自完成的誊写,独立性价值为零):**
476
+ - task 总数 ≤8 或变更单仓单模块 → 主 agent 逐卡 Edit 填充(骨架已预生成,誊写近乎免费),**不派子代理**
477
+ - 仅当 task 总数 >8 **且**跨多模块/跨仓、或主会话上下文已明显吃紧时,才按 batch 分派子代理(此时并行省墙钟有真实收益)
478
+ 3. **(分派形态时)按 batch 分派子代理(减少总子代理数量,而不是一个 task 一个子代理):**
476
479
  - 把任务按「同一 Wave + 同一模块/相近能力 + 无跨 batch 强依赖」原则分组
477
480
  - **每个 batch 包含 2~4 个 task**;Wave 内任务数 ≤4 时整个 Wave 可作为一个 batch
478
481
  - 有提供/消费契约的 task 尽量放到同一 batch(子代理能同时看到 consumer 与 provider,避免契约字段漏配)
479
482
  - 跨 Wave 依赖的 task 不要放在同一 batch(子代理只需读 plan.md,但 batch 内 task 的 allowed_paths 不应互相阻塞)
480
- 3. 为每个 batch 启动一个独立子代理(Agent tool),可并行启动多个 batch
481
- 4. 每个子代理使用下方「批量 TaskCard 子代理 prompt」,一次生成该 batch 的全部 task-N.md
482
- 5. 等待所有 batch 子代理完成
483
- 6. 验证每个 task-N.md 文件已生成且非空
483
+ - batch 数量封顶 ≤3(13 任务≈3 batch 已足;再多是协调税)
484
+ 4. 为每个 batch 启动一个独立子代理(Agent tool),可并行启动多个 batch
485
+ 5. 每个子代理使用下方「批量 TaskCard 子代理 prompt」,一次生成该 batch 的全部 task-N.md
486
+ 6. 等待所有 batch 子代理完成
487
+ 7. 验证每个 task-N.md 文件已生成且非空
484
488
 
485
- > 设计意图:plan.md 里 task 数可以较多(能力拆分完整),但 TaskCard 生成阶段要合理合并,避免子代理数量随 task 数线性爆炸。一个子代理生成 2~4 张卡片与生成 1 张卡片的 token/时间成本接近,却能显著减少协调开销。
489
+ > 设计意图:plan.md 里 task 数可以较多(能力拆分完整),但 TaskCard 生成阶段的**默认形态是主 agent 直填**——卡是 design/plan 的誊写不是创作,主 agent 刚写完 plan 上下文最热;子代理只在规模/上下文压力真实存在时才有净收益(每个子代理都要重读 design+plan+源码,6 个子代理 6.7M token 的实证教训)。分派时合并 batch,避免子代理数量随 task 数线性爆炸。
486
490
 
487
491
  ### 批量 TaskCard 子代理 prompt
488
492
  \`\`\`
@@ -510,7 +514,7 @@ ${taskcardTemplate}
510
514
  ## 约束
511
515
  - 每个 task 20~40 行;禁止在 TaskCard 里写实现细节之外的冗长设计
512
516
  - 同 batch 内 task 的 allowed_paths 不要互相冲突
513
- - **跨仓 task 路径口径(涉及 local.yaml repos: 注册仓的变更必读)**:跨仓 task frontmatter 必须填 \`repo: <repo-key>\`(local.yaml repos: 注册键,缺省=main 省略不填);allowed_paths 相对**该仓根**写(\`src/routes/x.js\`),从 design.md「\`## <repo-key> 仓变更\`」段内路径原样搬。❌ 禁止带仓库名前缀(\`sub-grid-security/src/...\`)、❌ 禁止绝对路径/盘符(\`C:/repo/src/...\`)——review 对账按 \`git -C <仓根> diff\` 的仓根相对路径匹配 allowed_paths,带前缀/绝对路径永不命中 → task 改完却判「无归属」对账不上
517
+ - **跨仓 task 路径口径(涉及 local.yaml repos: 注册仓的变更必读)**:跨仓 task frontmatter 必须填 \`repo: <repo-key>\`(local.yaml repos: 注册键,缺省=main 省略不填);allowed_paths 相对**该仓根**写(\`src/routes/x.js\`),从 design.md「\`## <repo-key> 仓变更\`」段内路径原样搬。❌ 禁止带仓库名前缀(\`<repo-key>/src/...\`)、❌ 禁止绝对路径/盘符(\`C:/repo/src/...\`)——review 对账按 \`git -C <仓根> diff\` 的仓根相对路径匹配 allowed_paths,带前缀/绝对路径永不命中 → task 改完却判「无归属」对账不上
514
518
  - 若 task 之间有 provides/expects_from 契约,同 batch 生成时必须字段对齐
515
519
  - 只生成本 batch 声明的任务,不要多写或少写
516
520
  - 不要在 TaskCard 里泄露 plan.md 中未出现的实现假设
@@ -538,7 +542,7 @@ ${taskcardTemplate}
538
542
 
539
543
  return {
540
544
  id: 'generate_blueprints',
541
- name: '生成 TaskCard(子代理按 batch 并行)',
545
+ name: '生成 TaskCard(默认主代理直填,大规模才派 batch)',
542
546
  prompt,
543
547
  outputHint: 'TaskCard 生成结果',
544
548
  optional: false
@@ -289,7 +289,7 @@ modules:
289
289
  ### 示例
290
290
  \`\`\`yaml
291
291
  schema_version: 2
292
- project: multi-agent-platform
292
+ project: demo-platform
293
293
  source_commit: abc1234
294
294
  generated_at: 2026-06-02 22:00:00
295
295
  generator: sillyspec-scan
@@ -39,6 +39,9 @@ import { verifyApiParity, _readWorktreeMeta } from './contract-matrix.js'
39
39
  import { RECEIPT_SOURCE_CROSS_LAYER_RE as RECEIPT_CROSS_LAYER_RE, RECEIPT_SOURCE_UNIT_RE as RECEIPT_UNIT_RE, CLI_SMOKE_SOURCE_MARK as SMOKE_RECEIPT_SOURCE_MARK } from './change-risk-profile.js'
40
40
  import { splitOwnVsForeignDiffFiles } from './foreign-declared.js'
41
41
  import { resolveSpecDir, resolveRuntimeRoot, detectWorktreeSpecDrift } from './run/shared.js'
42
+ // 探针 11(2026-09-20-redline-machine-check):红线机检评估器——纯函数模块,依赖
43
+ // (fs/js-yaml)已在模块图内,静态导入零增量开销;评估器异常由探针 fail-soft 捕获。
44
+ import { parseRedlines, evaluateRedlines } from './redlines.js'
42
45
 
43
46
  // 探针1 未实现标记匹配(坑 probe1-literal-false-positive,2026-09-15 复盘实证:16 命中全
44
47
  // 字面误报——TODO_FLAG_TODO 业务常量、「XXX完成处置」中文占位模板)。口径分三级:
@@ -2229,6 +2232,17 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
2229
2232
  probe10.notes = [`探针 10 执行失败(fail-soft 跳过):${e && e.message ? e.message : e}`]
2230
2233
  }
2231
2234
 
2235
+ // ── 探针 11:红线一致性(advisory;2026-09-20-redline-machine-check——对撞实验核心
2236
+ // 教训转化:语义红线活在散文里无机器可查形态)。消费者仓自持 .sillyspec/redlines.yaml
2237
+ // (缺=不适用零打扰,D-002);forbid 命中 ❌/⚠️(severity 驱动)、require 缺失 ⚠️,渲染进
2238
+ // 骨架供裁定;不进 PASS 封顶(D-003:攒误报率后另案升硬门)。fail-soft 同 8/9/10。──
2239
+ let probe11 = { applicable: false, entryCount: 0, findings: [], warnings: [] }
2240
+ try {
2241
+ probe11 = runRedlineConsistencyProbe({ specBase, cwd, wtRoot, changeName })
2242
+ } catch (e) {
2243
+ probe11.warnings = [`探针 11 执行失败(fail-soft 跳过):${e && e.message ? e.message : e}`]
2244
+ }
2245
+
2232
2246
  // ── 接口面 + 消费端归类(task-04 / FR-05 / D-005~D-007):design.md 接口段 tolerant
2233
2247
  // 解析 + 清单消费端归类——骨架「## 接口验证覆盖矩阵」段预填与 facts 落盘
2234
2248
  // (backfillFactsFromMdAndTests)共用同一解析器(单一产物源,不各读各的)。fail-soft:
@@ -2352,6 +2366,8 @@ export function renderVerifyProbesReport(result) {
2352
2366
  // 探针 10 紧随探针 9;旧 result 无 probe10 键(存量调用方/合成 result)→ applicable=false
2353
2367
  // 渲染「不适用」行 + notes,零回归(探针 8/9 兜底口径同款)。
2354
2368
  L.push(...(renderProbe10Lines(result.probe10 || { applicable: false, checkedFiles: 0, unclearedFiles: [], notes: [] })))
2369
+ // 探针 11 紧随 10;旧 result 无 probe11 键 → 不适用兜底(8/9/10 同款零回归口径)。
2370
+ L.push(...(renderProbe11Lines(result.probe11 || { applicable: false, entryCount: 0, findings: [], warnings: [] })))
2355
2371
  return L.join('\n')
2356
2372
  }
2357
2373
 
@@ -2463,6 +2479,67 @@ function renderProbe10Lines(p10) {
2463
2479
  return L
2464
2480
  }
2465
2481
 
2482
+ // ── 探针 11:红线一致性(2026-09-20-redline-machine-check,advisory)──────────────
2483
+
2484
+ const PROBE11_HEADING = '#### 探针 11:红线一致性(advisory)'
2485
+
2486
+ /**
2487
+ * 红线一致性探针(advisory / fail-open 全链,D-003/D-004):
2488
+ * 读 <specBase>/redlines.yaml(消费者仓自持,缺=不适用零打扰),调 redlines.js
2489
+ * 评估器(纯函数,root=wtRoot||cwd 定位 scope 文件)。坏 yaml → 不适用 + 注记;
2490
+ * 不进 PASS 封顶(D-003:攒误报率后另案升硬门)。
2491
+ */
2492
+ export function runRedlineConsistencyProbe({ specBase, cwd, wtRoot = null }) {
2493
+ const yamlPath = join(specBase, 'redlines.yaml')
2494
+ if (!existsSync(yamlPath)) {
2495
+ return { applicable: false, entryCount: 0, findings: [], warnings: [] }
2496
+ }
2497
+ let yamlText
2498
+ try {
2499
+ yamlText = readFileSync(yamlPath, 'utf8')
2500
+ } catch (e) {
2501
+ return { applicable: false, entryCount: 0, findings: [], warnings: [`redlines.yaml 读取失败(不适用):${e && e.code ? e.code : 'error'}`] }
2502
+ }
2503
+ let parsed
2504
+ try {
2505
+ parsed = parseRedlines(yamlText)
2506
+ } catch (e) {
2507
+ return { applicable: false, entryCount: 0, findings: [], warnings: [`redlines.yaml 解析失败(不适用):${e && e.message ? e.message : e}`] }
2508
+ }
2509
+ const root = wtRoot || cwd
2510
+ const result = evaluateRedlines({ entries: parsed.entries, root })
2511
+ return {
2512
+ applicable: true,
2513
+ entryCount: result.entryCount,
2514
+ findings: result.findings,
2515
+ warnings: [...(parsed.warnings || []), ...(result.warnings || [])],
2516
+ }
2517
+ }
2518
+
2519
+ /** renderProbe11Lines:不适用 / 命中逐条(severity 驱动 ❌/⚠️ + statement + origin)/ ✅ 计数。 */
2520
+ function renderProbe11Lines(p11) {
2521
+ const L = [PROBE11_HEADING]
2522
+ if (!p11 || !p11.applicable) {
2523
+ L.push('- 不适用(仓未配置 .sillyspec/redlines.yaml——红线机检零打扰,D-002)')
2524
+ for (const w of (p11 && p11.warnings) || []) L.push(`- ℹ️ ${w}`)
2525
+ return L
2526
+ }
2527
+ L.push('<!-- 口径注记:模式级断言(正则×scope,人写人负责)非语义审计——命中是「定向复核提示」;advisory 不阻断(D-003:误报率数据攒够后另案升硬门)。命中处置:真违规则修代码或升红线版本;正则过宽则收窄条目。 -->')
2528
+ const findings = p11.findings || []
2529
+ if (findings.length === 0) {
2530
+ L.push(`- ✅ 红线全过(${p11.entryCount ?? 0} 条断言,0 命中 0 缺失)`)
2531
+ } else {
2532
+ for (const f of findings) {
2533
+ const marker = f.kind === 'forbid' && f.severity === 'error' ? '❌' : '⚠️'
2534
+ const where = f.file ? `\`${f.file}:${f.line}\`` : 'scope 全集'
2535
+ L.push(`- ${marker} ${f.id}(${f.kind === 'forbid' ? '禁式命中' : '要求缺失'})@ ${where}${f.snippet ? `:\`${mdEscapeCell(f.snippet, 100)}\`` : f.detail ? `:${f.detail}` : ''}`)
2536
+ if (f.statement) L.push(` - 红线:${f.statement}${f.origin ? `(溯源:${f.origin})` : ''}`)
2537
+ }
2538
+ }
2539
+ for (const w of p11.warnings || []) L.push(`- ℹ️ ${w}`)
2540
+ return L
2541
+ }
2542
+
2466
2543
  // ── 接口验证覆盖矩阵段渲染(task-04 / FR-05 / D-005~D-007)──
2467
2544
  // 骨架新写路径(generateVerifyResultSkeleton)与缺段补齐(ensureApiCoverageMatrixSection)
2468
2545
  // 共用单一实现——renderProbe7Lines 先例。不动 probe7 既有矩阵(独立 ## 章节并行存在,口径
@@ -1519,7 +1519,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
1519
1519
  if (derivedRisk.length > 0) {
1520
1520
  result.warnings = (result.warnings || []).concat([
1521
1521
  `${derivedRisk.length} 个变更文件在 worktree 基线后主仓已有新提交(并行变更可能已合入):${derivedRisk.slice(0, 5).join(', ')}${derivedRisk.length > 5 ? ' 等' : ''}` +
1522
- `——若其中含生成产物(api-types/generated 等),本次 apply 可能以旧基线内容覆盖已合入的新内容;apply 后在新基线重跑生成命令(如 gen:types)再验证`
1522
+ `——若其中含生成产物(类型生成/代码生成产物等),本次 apply 可能以旧基线内容覆盖已合入的新内容;apply 后在新基线重跑生成命令再验证`
1523
1523
  ]);
1524
1524
  }
1525
1525
  }
package/src/worktree.js CHANGED
@@ -1751,7 +1751,7 @@ export class WorktreeManager {
1751
1751
  const escapes = detectEditableInstallEscape(wtPath);
1752
1752
  if (escapes.length > 0) {
1753
1753
  issues.push({ type: 'editable-install-escape', name, fixable: false,
1754
- detail: `worktree venv 含 ${escapes.length} 个 editable install 指向 worktree 外(${escapes.slice(0, 5).map(e => `${e.pkg}→${e.target}`).join(', ')}${escapes.length > 5 ? ' 等' : ''})——gen:types/后端命令会静默加载 worktree 外旧代码。轻量出口:跑生成链前设 PYTHONPATH=<worktree>/backend(PYTHONPATH 优先于 .pth);彻底修复:worktree 内重装(uv sync / uv pip install -e .)` });
1754
+ detail: `worktree venv 含 ${escapes.length} 个 editable install 指向 worktree 外(${escapes.slice(0, 5).map(e => `${e.pkg}→${e.target}`).join(', ')}${escapes.length > 5 ? ' 等' : ''})——生成链/后端命令会静默加载 worktree 外旧代码。轻量出口:跑生成链前设 PYTHONPATH=<worktree>/<项目源码根>(PYTHONPATH 优先于 .pth);彻底修复:worktree 内重装(uv sync / uv pip install -e .)` });
1755
1755
  }
1756
1756
  } catch { /* 探测失败不阻断 doctor */ }
1757
1757
  }