sillyspec 3.28.8 → 3.28.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.claude/full.json +1 -0
  2. package/.claude/skills/sillyspec-execute/SKILL.md +222 -224
  3. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  4. package/.claude/skills/sillyspec-quick/SKILL.md +118 -117
  5. package/AGENTS.md +3 -2
  6. package/README.md +35 -11
  7. package/SKILL.md +24 -10
  8. package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
  9. package/package.json +1 -1
  10. package/src/change-list.js +73 -20
  11. package/src/change-risk-profile.js +20 -0
  12. package/src/config-schema.js +62 -0
  13. package/src/contract-matrix.js +84 -5
  14. package/src/cross-repo-reconcile.js +134 -0
  15. package/src/db.js +11 -3
  16. package/src/decision-distill.js +15 -3
  17. package/src/design-facts.js +89 -20
  18. package/src/docs-gate.js +105 -6
  19. package/src/doctor-diagnostics.js +224 -2
  20. package/src/friction-ledger.js +171 -0
  21. package/src/hooks/worktree-guard.js +13 -1
  22. package/src/index.js +206 -18
  23. package/src/knowledge-classify.js +382 -0
  24. package/src/knowledge-hits.js +74 -0
  25. package/src/knowledge-stats.js +180 -0
  26. package/src/probe7-anchor-check.js +69 -0
  27. package/src/progress/change-registry.js +183 -8
  28. package/src/progress/shared.js +3 -2
  29. package/src/progress.js +100 -8
  30. package/src/quick-gate-profile.js +251 -0
  31. package/src/quicklog.js +164 -18
  32. package/src/review-dispatch.js +2 -1
  33. package/src/run/command.js +131 -13
  34. package/src/run/complete-handlers.js +511 -21
  35. package/src/run/complete.js +123 -23
  36. package/src/run/concurrent-detect.js +98 -0
  37. package/src/run/gate-snapshot.js +110 -2
  38. package/src/run/gates.js +141 -16
  39. package/src/run/prompt.js +221 -11
  40. package/src/run/quick-audit.js +111 -13
  41. package/src/run/shared.js +147 -11
  42. package/src/run/stage.js +6 -0
  43. package/src/scan-diff.js +61 -30
  44. package/src/scan-postcheck.js +74 -0
  45. package/src/scan-refresh.js +477 -0
  46. package/src/scan-staleness.js +25 -13
  47. package/src/scope-audit.js +456 -56
  48. package/src/stage-contract.js +185 -2
  49. package/src/stage-review.js +23 -7
  50. package/src/stages/brainstorm.js +6 -0
  51. package/src/stages/cmd-existence.js +9 -3
  52. package/src/stages/execute.js +85 -3
  53. package/src/stages/knowledge.js +76 -6
  54. package/src/stages/plan-postcheck.js +32 -5
  55. package/src/stages/quick.js +1 -1
  56. package/src/stages/verify.js +1 -0
  57. package/src/sync-noise.js +170 -6
  58. package/src/sync.js +141 -9
  59. package/src/task-review.js +366 -46
  60. package/src/verify-postcheck.js +212 -55
  61. package/src/verify-probes.js +727 -16
  62. package/src/worktree-apply.js +685 -48
  63. package/src/worktree-deps.js +124 -3
  64. package/src/worktree.js +391 -15
  65. package/templates/agents-instruction.md +2 -2
  66. package/templates/prompts/testcase-design.md +2 -1
  67. package/templates/prompts/verify-probes.md +4 -1
  68. package/.patch16-tmp.mjs +0 -169
  69. package/sa-rename-probe/src/new.js +0 -4
@@ -15,15 +15,25 @@
15
15
  * 行数三档(D-002):tracked → numstat;untracked 新文件 → wc-l 记全 + 行;binary(numstat
16
16
  * 两列 '-')→ additions/deletions=null。三态(full-flow):planned / unplanned / untouched。
17
17
  *
18
+ * quick 画像出口(2026-09-14-quick-exit-tiered-gates task-03,FR-04 / D-008):quick 模式结果
19
+ * 增量携带 gateProfile(实时态透传 auditQuickCompletion 挂的 review.gateProfile,不重复计算;
20
+ * 冻结重放态透传 quicklog/patches 记录冻结值,旧记录按 rows 清单重算),表格出口按存在性
21
+ * 追加 [gate] 画像段、--json 出口随结果对象序列化——画像的独立可重放出口与阈值校准数据源。
22
+ * 既有三态对账表/归属表/--file 出口与返回字段语义零变化(gateProfile 为增量字段)。
23
+ *
18
24
  * 全 advisory(D-006):不抛错阻断、不写门禁状态;任何异常 catch 后并入 degradedReason。
19
25
  * 纯读:不落盘、不改 guard/进度库。Windows 路径 \\→/ 归一,ESM-only,零新依赖。
20
26
  */
21
27
  import { existsSync, readFileSync, readdirSync } from 'node:fs'
22
28
  import { createHash } from 'node:crypto'
23
29
  import { join, dirname } from 'node:path'
24
- import { locateQuickSessionGuard, auditQuickCompletion, resolveRuntimeRoot, collectOtherQuickSessionDeclarations, ancestorSpecDirs } from './run/shared.js'
30
+ import { locateQuickSessionGuard, auditQuickCompletion, resolveRuntimeRoot, collectOtherQuickSessionDeclarations, ancestorSpecDirs, isQuickMetadata } from './run/shared.js'
31
+ import { computeGateProfile, resolveGateThresholds } from './quick-gate-profile.js'
25
32
  import { safeGit } from './git-helper.js'
26
33
  import { parseFileChangeListDetailed, pathMatches } from './change-list.js'
34
+ import { classifyToolScaffold } from './worktree-apply.js'
35
+ import { parseRepoRegistry } from './stages/plan-postcheck.js'
36
+ import { _readWorktreeMeta } from './contract-matrix.js'
27
37
 
28
38
  /** quick 会话 id 形态(与 run/command.js :652 QUICK_SID_RE 同款——crypto.randomUUID 前 8 hex) */
29
39
  const QUICK_SID_RE = /^quick-[0-9a-f]{8}$/
@@ -204,10 +214,11 @@ function resolveChangeDir(sb, changeName) {
204
214
  function resolveDiffRoot(sb, changeName, form, cwd) {
205
215
  if (form === 'worktree') {
206
216
  try {
207
- const meta = JSON.parse(readFileSync(join(sb, '.runtime', 'worktrees', changeName, 'meta.json'), 'utf8'))
208
- if (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
209
- return meta.worktreePath
210
- }
217
+ // 双根读取(坑 platform-dual-root-form-misjudge):平台模式 sb=specRoot 时项目侧
218
+ // .sillyspec 的 meta 读不到 → worktree 根错落 cwd。_readWorktreeMeta 双候选(contract-matrix
219
+ // 既有样板,gitDir 已含 worktreePath/mode 判定)。
220
+ const found = _readWorktreeMeta(sb || null, cwd, changeName)
221
+ if (found) return found.gitDir
211
222
  } catch { /* 读失败 → cwd 兜底(advisory) */ }
212
223
  }
213
224
  return cwd
@@ -266,18 +277,20 @@ function filterPatchForFiles(patchText, filesSet) {
266
277
  }
267
278
 
268
279
  /**
269
- /** patch 内容 sha256(A-F01 防篡改锚——写入方存 json.patchSha256,读取方校验) */
270
- function sha256Text(text) {
271
- return createHash('sha256').update(text, 'utf8').digest('hex')
280
+ * patch sha256——换行归一化口径(quick-ec5fc714):git autocrlf 会在提交/检出时把 patch
281
+ * 转 CRLF(平台仓实测 mtime 晚落盘一天的假篡改告警)——双侧统一 LF 归一后取 hash。
282
+ */
283
+ function sha256PatchNormalized(text) {
284
+ return createHash('sha256').update(String(text).replace(/\r\n/g, '\n'), 'utf8').digest('hex')
272
285
  }
273
286
 
274
287
  /**
275
- * 冻结 patch 完整性校验(A-F01):json 记录的 patchSha256 与 patch 实文比对。
288
+ * 冻结 patch 完整性校验(A-F01):json 记录的 patchSha256 与 patch 实文比对(LF 归一口径)。
276
289
  * @returns {boolean|null} true=一致;false=不匹配(可能被篡改);null=未记录 hash(旧记录)
277
290
  */
278
291
  function verifyPatchIntegrity(recordedSha, patchText) {
279
292
  if (!recordedSha || typeof recordedSha !== 'string') return null
280
- return sha256Text(patchText) === recordedSha
293
+ return sha256PatchNormalized(patchText) === recordedSha
281
294
  }
282
295
 
283
296
  /**
@@ -344,7 +357,8 @@ function slicePatchForFile(patchText, filePath) {
344
357
 
345
358
  /**
346
359
  * quick 记录态反查(guard 清理后):扫祖先链各 specBase 的 quicklog/patches/*.json,
347
- * 按 json 内冗余的 sessionId 匹配。返回记录 + patch 路径(--file 切片源)。
360
+ * 按 json 内冗余的 sessionId 匹配。返回记录 + patch 路径(--file 切片源)+ specBase
361
+ * (命中目录——画像重放的 module-map/local.yaml 加载锚,task-03)。
348
362
  */
349
363
  function findQuickPatchRecord(cwd, sessionId) {
350
364
  for (const sb of ancestorSpecDirs(cwd)) {
@@ -355,7 +369,7 @@ function findQuickPatchRecord(cwd, sessionId) {
355
369
  try {
356
370
  const j = JSON.parse(readFileSync(join(dir, f), 'utf8'))
357
371
  if (j && j.sessionId === sessionId && Array.isArray(j.rows)) {
358
- return { record: j, patchPath: join(dir, f.replace(/\.json$/, '.patch')), qlId: j.qlId || f.replace(/\.json$/, '') }
372
+ return { record: j, patchPath: join(dir, f.replace(/\.json$/, '.patch')), qlId: j.qlId || f.replace(/\.json$/, ''), specBase: sb }
359
373
  }
360
374
  } catch { /* 单文件损坏跳过 */ }
361
375
  }
@@ -363,6 +377,183 @@ function findQuickPatchRecord(cwd, sessionId) {
363
377
  return null
364
378
  }
365
379
 
380
+ /** QUICKLOG 条目号形态(ql-20260914-001-abcd:日期-序号-短 hash,短 hash 4 hex) */
381
+ const QL_ID_RE = /^ql-\d{8}-\d{3}-[0-9a-f]{4}$/
382
+
383
+ /**
384
+ * 注册仓 key 清单(scope-audit-cross-repo-blindness 改进点 2):读 <specBase>/local.yaml 的
385
+ * repos 段(parseRepoRegistry 同源,D-003 不另造解析)——design 清单跨仓子段 / `cross-repo:`
386
+ * 前缀只认注册 key(fail-closed,防普通路径误判)。local.yaml 缺失/坏 → 空清单(全部按主仓
387
+ * 解析,行为同改进前)。
388
+ * @param {string} specBase .sillyspec 根
389
+ * @returns {string[]} repoKey 清单
390
+ */
391
+ function loadRegisteredRepoKeys(specBase) {
392
+ try {
393
+ const p = join(specBase || '', 'local.yaml')
394
+ if (!specBase || !existsSync(p)) return []
395
+ return [...parseRepoRegistry(readFileSync(p, 'utf8')).keys()]
396
+ } catch { /* 读取/解析失败 → 空清单(主仓口径兜底) */ return [] }
397
+ }
398
+
399
+ /**
400
+ * ql-xxx → quick-xxx 反查(用户「为什么不直接用 ql-xxx 查」根治,ql-20260910-014 设计:
401
+ * 映射只在本机 guard,会话结束即清;patches 记录按 qlId 命名且 json 冗余 sessionId——
402
+ * 平台可抓的持久映射)。按文件名前缀匹配(qlId=文件名),避免逐文件解析全目录。
403
+ */
404
+ function findQuickSessionByQlId(cwd, qlId) {
405
+ const norm = String(qlId || '').trim()
406
+ if (!QL_ID_RE.test(norm)) return null
407
+ for (const sb of ancestorSpecDirs(cwd)) {
408
+ try {
409
+ const jsonPath = join(sb, 'quicklog', 'patches', `${norm}.json`)
410
+ if (!existsSync(jsonPath)) continue
411
+ const j = JSON.parse(readFileSync(jsonPath, 'utf8'))
412
+ if (j && typeof j.sessionId === 'string' && QUICK_SID_RE.test(j.sessionId)) {
413
+ return { sessionId: j.sessionId, record: j, patchPath: join(sb, 'quicklog', 'patches', `${norm}.patch`), qlId: norm, specBase: sb }
414
+ }
415
+ } catch { /* 单文件损坏跳过 */ }
416
+ }
417
+ return null
418
+ }
419
+
420
+ /**
421
+ * quick 记录态回放结果构建(共用:guard 清理后 sessionId 反查 + ql-xxx 反查两入口)。
422
+ * 画像重放(FR-04 / D-008 / task-03)+ 「声明即归属」过滤(quick-ec5fc714)。
423
+ */
424
+ async function buildQuickRecordResult(rec, requestedName) {
425
+ let gateProfile = null
426
+ try {
427
+ if (rec.record.gateProfile && typeof rec.record.gateProfile === 'object') {
428
+ gateProfile = rec.record.gateProfile
429
+ } else {
430
+ const rowsPaths = rec.record.rows
431
+ .map(r => (r && typeof r.path === 'string' ? r.path : ''))
432
+ .filter(Boolean)
433
+ if (rowsPaths.length > 0) {
434
+ const thresholds = await resolveReplayGateThresholds(rec.specBase)
435
+ gateProfile = computeGateProfile(
436
+ rowsPaths,
437
+ await loadGateModuleIndex(rec.specBase, rowsPaths),
438
+ thresholds ? { thresholds } : {},
439
+ )
440
+ }
441
+ }
442
+ } catch { gateProfile = null }
443
+ const filteredRows = rec.record.rows.filter(r => r && !isQuickMetadata(r.path || '', []))
444
+ const filterDropped = rec.record.rows.length - filteredRows.length
445
+ const viaQl = requestedName !== rec.record.sessionId && QL_ID_RE.test(requestedName)
446
+ return {
447
+ mode: 'quick', ok: true, degradedReason: null,
448
+ baseAnchor: rec.record.baseAnchor || `quick-window:${rec.record.sessionId || requestedName}`,
449
+ totals: rec.record.totals && typeof rec.record.totals === 'object' && filterDropped === 0
450
+ ? rec.record.totals
451
+ : { files: filteredRows.length, ...sumTotals(filteredRows) },
452
+ rows: filteredRows,
453
+ excluded: rec.record.excluded && Array.isArray(rec.record.excluded.foreignDeclared)
454
+ ? rec.record.excluded
455
+ : { foreignDeclared: [] },
456
+ frozenPatchPath: rec.patchPath,
457
+ patchSha256: rec.record.patchSha256 || null,
458
+ patchStatus: rec.record.patchStatus || null,
459
+ gateProfile,
460
+ note: `quick 会话已收尾——记录态(quicklog/patches/${rec.qlId},${rec.record.savedAt ? String(rec.record.savedAt).replace('T', ' ').slice(0, 19) + ' 落盘' : '--done 时点冻结'})${viaQl ? `;经 ql-Id 反查(会话 ${rec.record.sessionId})` : ''}${filterDropped > 0 ? `;回放已剔除 ${filterDropped} 个 quick 元数据/并行变更目录文件(范围视图收紧)` : ''}`,
461
+ }
462
+ }
463
+
464
+ /**
465
+ * quick 审计链 module-map 的项目推导(2026-09-14-quick-exit-tiered-gates task-03):
466
+ * loadQuickModuleIndex(run/shared.js,未 export)的加载路径是
467
+ * <specBase>/docs/<projectName>/modules/_module-map.yaml——scope-audit 侧无 progress 上下文
468
+ * 拿 projectName,按候选 map 推导:
469
+ * - 单候选 → 直取(绝大多数仓单项目);
470
+ * - 多候选 + 样本文件(实时态=guard.allowedFiles 声明面 / 重放态=记录 rows 冻结集)→ 按
471
+ * 「样本归属得分唯一最高」消歧(computeGateProfile.unmappedFiles 同款归属口径,纯函数
472
+ * 零 IO 重用——本仓 dashboard+sillyspec 双 map 实证需要);
473
+ * - 零候选 / 无样本 / 得分平分 → null(画像走 degraded 档,fail-open 不猜,兼容策略第 1 条)。
474
+ * @param {string} specBase
475
+ * @returns {string[]} 带 _module-map.yaml 的项目名清单
476
+ */
477
+ function listModuleMapProjects(specBase) {
478
+ if (!specBase) return []
479
+ try {
480
+ const docsDir = join(specBase, 'docs')
481
+ return readdirSync(docsDir).filter(n => existsSync(join(docsDir, n, 'modules', '_module-map.yaml')))
482
+ } catch {
483
+ return []
484
+ }
485
+ }
486
+
487
+ /** 单项目 _module-map.yaml 读取(parseModuleMapSimple canonical 解析,与 loadQuickModuleIndex 同款);不可得/解析空 → null。 */
488
+ async function readModuleMap(specBase, project) {
489
+ try {
490
+ const { parseModuleMapSimple } = await import('./modules.js')
491
+ const idx = parseModuleMapSimple(readFileSync(join(specBase, 'docs', project, 'modules', '_module-map.yaml'), 'utf8'))
492
+ return idx && Object.keys(idx).length > 0 ? idx : null
493
+ } catch {
494
+ return null
495
+ }
496
+ }
497
+
498
+ /**
499
+ * 项目名推导(口径见 listModuleMapProjects 头注释):唯一得分(样本非文档文件中归属最少
500
+ * unmapped 的 map)才采信,平分/无样本/多候选全失败 → null。
501
+ * @param {string} specBase
502
+ * @param {string[]} [sampleFiles] 归属得分样本(声明面 / 记录 rows)
503
+ * @returns {Promise<string|null>}
504
+ */
505
+ async function pickModuleMapProject(specBase, sampleFiles = []) {
506
+ const names = listModuleMapProjects(specBase)
507
+ if (names.length === 1) return names[0]
508
+ if (names.length === 0 || !Array.isArray(sampleFiles)) return null
509
+ const files = sampleFiles.map(f => String(f || '')).filter(Boolean)
510
+ if (files.length === 0) return null
511
+ let best = null
512
+ let bestUnmapped = Infinity
513
+ let tie = false
514
+ for (const n of names) {
515
+ const idx = await readModuleMap(specBase, n)
516
+ if (!idx) continue
517
+ const unmapped = computeGateProfile(files, idx).unmappedFiles.length
518
+ if (unmapped < bestUnmapped) { best = n; bestUnmapped = unmapped; tie = false }
519
+ else if (unmapped === bestUnmapped) tie = true
520
+ }
521
+ return tie ? null : best
522
+ }
523
+
524
+ /**
525
+ * 画像重放的 moduleIndex 加载(task-03,FR-04):与 run/shared.js loadQuickModuleIndex 同链路
526
+ * ——同路径 + parseModuleMapSimple(canonical)解析,项目按记录 rows 样本消歧;不可得 → null
527
+ * (computeGateProfile 走 degraded 档)。动态 import modules.js 隔离依赖(shared.js 同款)。
528
+ * @param {string} specBase quicklog/patches 记录命中的 spec 根
529
+ * @param {string[]} [sampleFiles] 项目消歧样本(记录 rows)
530
+ * @returns {Promise<object|null>}
531
+ */
532
+ async function loadGateModuleIndex(specBase, sampleFiles = []) {
533
+ const project = await pickModuleMapProject(specBase, sampleFiles)
534
+ return project ? readModuleMap(specBase, project) : null
535
+ }
536
+
537
+ /**
538
+ * 画像重放的阈值合并(task-03,D-009):与 auditQuickCompletion 画像挂载段同链路——
539
+ * <specBase>/local.yaml(= 实时态 readLocalYamlRaw(sessionRoot) 读的同一文件)+ js-yaml +
540
+ * resolveGateThresholds;缺失/坏 YAML → null(computeGateProfile 内回 THRESHOLDS 默认)。
541
+ * @param {string} specBase
542
+ * @returns {Promise<object|null>}
543
+ */
544
+ async function resolveReplayGateThresholds(specBase) {
545
+ try {
546
+ const p = join(specBase, 'local.yaml')
547
+ if (!existsSync(p)) return null
548
+ const mod = await import('js-yaml')
549
+ const yamlLoad = mod.load || mod.default?.load
550
+ const cfg = yamlLoad(readFileSync(p, 'utf8'))
551
+ return cfg && typeof cfg === 'object' ? resolveGateThresholds(cfg) : null
552
+ } catch {
553
+ return null
554
+ }
555
+ }
556
+
366
557
  /** 汇总非 null 行数(binary 与降级 null 不计入,不出伪数据) */
367
558
  function sumTotals(rows) {
368
559
  let additions = 0
@@ -391,6 +582,16 @@ async function computeQuickAudit({ cwd, platformOpts, sessionId, located, collec
391
582
  try {
392
583
  const mergedGuard = {
393
584
  ...guard,
585
+ // 画像输入对齐(2026-09-14-quick-exit-tiered-gates task-03,FR-04):specBase/projectName
586
+ // 透传——complete-handlers :1050 同款 merge(「--done 收尾同源实时采集」的既有约定)补齐。
587
+ // guard.json 落盘不含这两键,缺省会让 auditQuickCompletion 挂的 review.gateProfile 恒走
588
+ // degraded 档(moduleSpan=null),与 --done 时点 review.gateProfile 字段不一致。projectName
589
+ // 无 progress 上下文,按 module-map 候选推导(声明会话以 allowedFiles 为归属得分样本;
590
+ // 零候选/无样本/平分 → null → degraded,fail-open)。specBase/projectName 在 audit 内只喂
591
+ // quicklog 目录解析(与 cwd 推导同一路径)、docSyncHint 模块归属与 gate 画像(computeQuickAudit
592
+ // 不消费 docSyncHint),不触 status/reasons/归属切分语义。
593
+ specBase: guardSpecBase,
594
+ projectName: await pickModuleMapProject(guardSpecBase, guard.allowedFiles),
394
595
  otherSessionsDeclared: collectOtherQuickSessionDeclarations(platformOpts, guardSpecBase, sessionId),
395
596
  }
396
597
  audit = await auditQuickCompletion(sessionRoot, mergedGuard, {})
@@ -419,14 +620,39 @@ async function computeQuickAudit({ cwd, platformOpts, sessionId, located, collec
419
620
  return { ...base, ok: false, degradedReason: `quick 窗口审计失败: ${hardFailure}` }
420
621
  }
421
622
 
422
- // 归属切分(消费 auditQuickCompletion 产物,不重算):attributedFiles(含 sameFileHits)→
423
- // declared;undeclaredFiles − softTestFiles → undeclared;softTestFiles → soft。
623
+ // 归属切分(quick-ec5fc714 平台实测修正):scope-audit 是**范围视图**不是门禁——按
624
+ // 「声明即归属」硬切:declared=声明∩窗口;soft=软归属;其余(含 quick 元数据/其他变更
625
+ // 目录/linkedChanges 文件——audit 侧刻意保留它们供审计行落盘不静默挤走,范围视图则不该
626
+ // 把别人的活算进来)归为 undeclared 仅 note 交代。窗口文件全在 undeclared(如声明文件
627
+ // 已提交无 diff)时,undeclared 升格进 rows(范围视图只展示改了的东西)——本 quick
628
+ // 实测:声明两 pi-rpc 文件已提交 + 他者变更目录 19 文件未声明,行数贡献恰来自后者。
424
629
  const attributed = Array.isArray(audit.attributedFiles) ? audit.attributedFiles.map(toPosix) : []
425
630
  const undeclared = Array.isArray(audit.undeclaredFiles) ? audit.undeclaredFiles.map(toPosix) : []
426
631
  const softSet = new Set((Array.isArray(audit.softTestFiles) ? audit.softTestFiles : []).map(toPosix))
427
632
  const attrSet = new Set(attributed)
428
-
429
- const rowFiles = [...new Set([...attributed, ...undeclared])]
633
+ const linkedChangesArr = Array.isArray(guard.linkedChanges) ? guard.linkedChanges.map(c => String(c).replace(/\\/g, '/')) : []
634
+ const isLinkedLeftover = (f) => linkedChangesArr.some(lc => f.startsWith(`.sillyspec/changes/${lc}/`) || f.startsWith(`.sillyspec/changes/archive/${lc}/`))
635
+
636
+ let rowFiles
637
+ let dropNote = null
638
+ if (attributed.length > 0 || softSet.size > 0) {
639
+ // 有归属文件时:undeclared 全部剔出范围视图——undeclared 即「不属于本 quick」的文件
640
+ // (他者变更目录/quicklog 产物/无关未提交),audit 侧留它们供审计行追溯,范围视图不该
641
+ // 展示(平台仓实测:窗口 13 个文件里 11 个是他者变更目录,唯二归属文件已提交)。
642
+ rowFiles = [...new Set([...attributed, ...softSet])]
643
+ if (undeclared.length > 0) {
644
+ dropNote = `${undeclared.length} 个未声明文件不计入范围视图(他者变更目录/quicklog 产物/漏声明——audit 行有追溯)`
645
+ }
646
+ } else {
647
+ // 窗口全 undeclared(如声明文件已提交无 diff):范围视图仍剔 quick 元数据/变更目录
648
+ // ——audit 侧留它们供审计行追溯,范围视图不展示他者活(platform quick-ffb92f60 实测:
649
+ // 19 个 undeclared 中 17 个是他者变更目录/quicklog 产物,唯二声明文件已提交)。真未知的
650
+ // 源码/业务文件保留进 rows(他者窗口改动或漏声明,note 交代)。
651
+ rowFiles = [...new Set(undeclared.filter(f => !isQuickMetadata(f, linkedChangesArr) && !isLinkedLeftover(f)))]
652
+ if (rowFiles.length < undeclared.length) {
653
+ dropNote = `${undeclared.length - rowFiles.length} 个 quick 元数据/他者变更目录文件不计入范围视图`
654
+ }
655
+ }
430
656
  const stats = collectNumstatByPath(sessionRoot, rowFiles, { baseRef: 'HEAD' })
431
657
 
432
658
  const rows = rowFiles.map(f => {
@@ -446,7 +672,7 @@ async function computeQuickAudit({ cwd, platformOpts, sessionId, located, collec
446
672
 
447
673
  // 「quick 已提交」降级(design 总体方案):窗口空(changedFiles 空)且 QUICKLOG 已有该会话
448
674
  // 条目 → 记录态提示读 QUICKLOG,不以空表冒充实时。
449
- let note = null
675
+ let note = dropNote
450
676
  if (rowFiles.length === 0) {
451
677
  const qlId = guard.quicklogId || null
452
678
  if (quicklogHasEntry(guardSpecBase, qlId)) {
@@ -475,6 +701,11 @@ async function computeQuickAudit({ cwd, platformOpts, sessionId, located, collec
475
701
  },
476
702
  frozenPatch,
477
703
  note,
704
+ // 画像透传(FR-04 / task-03 gateProfile-json):auditQuickCompletion 挂的 review.gateProfile
705
+ // 原样随 review 携带进结果对象——不重复计算(与 --done 时点同源);fail-open 时为 null
706
+ // (消费方按存在性读取)。complete-handlers --done 落 quicklog/patches/<qlId>.json 时随
707
+ // 快照对象冻结,供冻结重放路径透传。
708
+ gateProfile: audit.gateProfile && typeof audit.gateProfile === 'object' ? audit.gateProfile : null,
478
709
  }
479
710
  }
480
711
 
@@ -493,7 +724,8 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
493
724
  // 审计 tag)全无 = 尚未进入 execute——实际侧不存在(B2 会吞整个工作区脏文件误归属本变更,
494
725
  // 2026-09-11-cross-change-decision-guard 实证 28 个「计划外」全是并行在途)。出计划清单
495
726
  // 视图:rows=design 清单 untouched(待实现),工作区改动归属各自会话不进表。
496
- const hasWorktreeMeta = existsSync(join(sb, '.runtime', 'worktrees', changeName, 'meta.json'))
727
+ // 双根(坑 platform-dual-root-form-misjudge):平台模式 sb=specRoot 时项目侧 meta 单路径读不到
728
+ const hasWorktreeMeta = !!_readWorktreeMeta(sb || null, cwd, changeName)
497
729
  const hasBranch = !!safeGit(cwd, ['rev-parse', '--verify', '--quiet', `sillyspec/${changeName}^{commit}`], { timeout: 15 * 1000 }).value
498
730
  const hasAuditTag = !!safeGit(cwd, ['rev-parse', '--verify', '--quiet', `sillyspec-audit/sillyspec/${changeName}^{commit}`], { timeout: 15 * 1000 }).value
499
731
  // 审查 C-F02:预执行判定必须排除归档(归档=流程走完,58/77 归档变更三信号全缺被误判
@@ -529,11 +761,12 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
529
761
  try {
530
762
  prePlanned = parseFileChangeListDetailed(
531
763
  join(changeDirInfo ? changeDirInfo.dir : join(sb, 'changes', changeName), 'design.md'),
532
- { keepSillyspecDocs: true })
764
+ { keepSillyspecDocs: true, repoKeys: loadRegisteredRepoKeys(sb) })
533
765
  } catch { /* 清单解析失败 → 空清单 + 说明 */ }
534
766
  const preRows = prePlanned.map(e => ({
535
767
  path: e.path, planned: e.operation || null, additions: 0, deletions: 0,
536
768
  kind: 'modified', verdict: 'untouched',
769
+ ...(e.repo ? { crossRepo: e.repo } : {}),
537
770
  }))
538
771
  return {
539
772
  ...emptyBase,
@@ -551,8 +784,9 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
551
784
  let planDegraded = null
552
785
  try {
553
786
  // keepSillyspecDocs=true:与 actual 侧 filterDeliverableFiles 保留 .sillyspec/docs/** 交付物
554
- // 的口径对齐(dogfood 模块文档=交付物)
555
- plannedEntries = parseFileChangeListDetailed(designMdPath, { keepSillyspecDocs: true })
787
+ // 的口径对齐(dogfood 模块文档=交付物);repoKeys=注册仓清单——跨仓子段/`cross-repo:` 前缀
788
+ // 条目带 repo 字段(改进点 2,标注「跨仓(本表不含)」的数据源)
789
+ plannedEntries = parseFileChangeListDetailed(designMdPath, { keepSillyspecDocs: true, repoKeys: loadRegisteredRepoKeys(sb) })
556
790
  } catch (e) {
557
791
  planDegraded = `design.md 清单解析异常: ${e && e.message ? String(e.message).split('\n')[0] : e}`
558
792
  }
@@ -562,16 +796,68 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
562
796
  : `design.md 不存在(${toPosix(designMdPath)})——降级实际侧 only 视图(不出三态列)`
563
797
  }
564
798
 
565
- // —— 实际侧(verify-postcheck.js task-02 契约:files + baseAnchor;动态 import 隔离
566
- // 并行时序——task-02 未落地时降级说明而非炸)——
799
+ // —— 实际侧采集 ——
800
+ // 形态 A(worktree 存活,quick-ec5fc714):**自采纯 worktree 口径**——锚(meta baselineCommit
801
+ // 优先)..worktree 工作区 = 本变更真实边界(子代理未提交改动 + 分支已提交)。不沿用
802
+ // resolveVerifyChangedFiles(其语义是「verify 证据核验面」:主仓∪worktree 并集——对证据核验
803
+ // 合理,对本变更范围全是噪音:主仓侧 20 并行文件混入,且它们对 worktree 锚 diff miss 后落
804
+ // wc-l 档记成整文件行数,两放大器叠加出 +11840/22 文件的巨型表)。
805
+ // 形态 B(post-apply):继续走 resolveReconcileActualFiles 三源(本变更活已回主仓,语义正确)。
567
806
  let actual = null
568
807
  let actualFailure = null
569
808
  try {
570
- const mod = await import('./verify-postcheck.js')
571
- if (typeof mod.resolveReconcileActualFiles !== 'function') {
572
- throw new Error('resolveReconcileActualFiles 未导出(依赖契约未就绪)')
809
+ const found = _readWorktreeMeta(sb || null, cwd, changeName)
810
+ if (found) {
811
+ const wtRoot = found.gitDir
812
+ const anchor = found.meta.baselineCommit || found.meta.actualBaseHash || found.meta.baseHash || null
813
+ if (anchor) {
814
+ const diffOut = safeGit(wtRoot, ['diff', '--name-only', anchor], { timeout: 30 * 1000 })
815
+ if (!diffOut.error && typeof diffOut.value === 'string') {
816
+ // 过滤口径与 resolveVerifyChangedFiles 的 wtFiles 同款(保留 docs 交付物,排运行时/进度面)
817
+ const wtFilter = (p) => p && p !== '.sillyspec'
818
+ && !p.startsWith('.sillyspec/changes/')
819
+ && !p.startsWith('.sillyspec/.runtime/')
820
+ && !p.startsWith('.sillyspec/quicklog/')
821
+ const files = diffOut.value.split('\n')
822
+ .map(p => toPosix(p.replace(/^"|"$/g, '').trim()))
823
+ .filter(wtFilter)
824
+ // untracked 新文件不在 diff(git diff 只看 tracked)——status --porcelain 并入
825
+ // (与旧链路 includeWorkingTree 的 wtFiles 半边同覆盖,行数侧走 wc-l 档)
826
+ const st = safeGit(wtRoot, ['status', '--porcelain', '--untracked-files=all'], { timeout: 30 * 1000, trim: false })
827
+ if (!st.error && typeof st.value === 'string') {
828
+ for (const line of st.value.split('\n')) {
829
+ if (!line.startsWith('??')) continue
830
+ const p = toPosix(line.slice(3).trim().replace(/^"|"$/g, '').split(' -> ').pop() || '')
831
+ if (wtFilter(p) && !files.includes(p)) files.push(p)
832
+ }
833
+ }
834
+ actual = { ok: true, form: 'worktree', files, sources: ['worktree:diff-anchor..worktree', 'worktree:status-untracked'], foreignExcluded: 0, foreignExcludedFiles: [], degradedReason: null, baseAnchor: anchor }
835
+ } else {
836
+ actual = { ok: false, form: 'worktree', files: [], sources: [], foreignExcluded: 0, foreignExcludedFiles: [], degradedReason: 'worktree 锚定 diff 失败', baseAnchor: null }
837
+ }
838
+ } else {
839
+ // 无锚 meta(baseHash 等字段缺失):退 HEAD 未提交窗口口径——文件集=status 全量
840
+ // (tracked 改动 + untracked),行数侧由主链路 HEAD 兜底(quick-8aa52289 契约不变)
841
+ const st = safeGit(wtRoot, ['status', '--porcelain', '--untracked-files=all'], { timeout: 30 * 1000, trim: false })
842
+ const files = []
843
+ if (!st.error && typeof st.value === 'string') {
844
+ for (const line of st.value.split('\n')) {
845
+ const p = toPosix(line.slice(3).trim().replace(/^"|"$/g, '').split(' -> ').pop() || '')
846
+ if (p && p !== '.sillyspec'
847
+ && !p.startsWith('.sillyspec/changes/')
848
+ && !p.startsWith('.sillyspec/.runtime/')
849
+ && !p.startsWith('.sillyspec/quicklog/')) files.push(p)
850
+ }
851
+ }
852
+ actual = { ok: true, form: 'worktree', files, sources: ['worktree:status-porcelain(HEAD-window)'], foreignExcluded: 0, foreignExcludedFiles: [], degradedReason: null, baseAnchor: null }
853
+ }
854
+ } else {
855
+ const mod = await import('./verify-postcheck.js')
856
+ if (typeof mod.resolveReconcileActualFiles !== 'function') {
857
+ throw new Error('resolveReconcileActualFiles 未导出(依赖契约未就绪)')
858
+ }
859
+ actual = mod.resolveReconcileActualFiles({ cwd, specBase: sb, runtimeRoot, changeName })
573
860
  }
574
- actual = mod.resolveReconcileActualFiles({ cwd, specBase: sb, runtimeRoot, changeName })
575
861
  } catch (e) {
576
862
  actualFailure = e && e.message ? String(e.message).split('\n')[0] : String(e)
577
863
  }
@@ -596,6 +882,29 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
596
882
  return { ...base, degradedReason: [reason, planDegraded].filter(Boolean).join(';') }
597
883
  }
598
884
 
885
+ // —— execute --done 采集·apply-pathspec 归属收窄(quick-ec5fc714):apply 时点在 .runtime
886
+ // 落过本变更精确文件清单(worktree-apply.js 产 apply-pathspec-<change>.txt)——execute 采集
887
+ // 形态(freshActual+collectPatch,仅 printExecuteScopeAudit 组合)下按它收窄 actual.files,
888
+ // 快照/patch 冻**归属面**而非开放区间(平台仓实测:186KB 快照中 23/44 段为并行会话 docs
889
+ // 同步等噪音)。查询面不收窄(快照说什么是什么);pathspec 缺失退开放区间(in-place 等)。
890
+ let narrowNote = null
891
+ if (freshActual && collectPatch) {
892
+ try {
893
+ const pathspecFile = join(runtimeRoot, `apply-pathspec-${changeName}.txt`)
894
+ const owned = readFileSync(pathspecFile, 'utf8').split('\n').map(l => toPosix(l.trim())).filter(Boolean)
895
+ if (owned.length > 0) {
896
+ const ownedSet = new Set(owned)
897
+ const before = actual.files.length
898
+ actual = { ...actual, files: actual.files.filter(f => ownedSet.has(toPosix(f))) }
899
+ // 收窄掉的并行噪音可见性保留(审计不静默挤走)
900
+ const dropped = before - actual.files.length
901
+ if (dropped > 0) {
902
+ narrowNote = `快照已按 apply-pathspec 收窄至本变更归属文件(剔除开放区间并行文件 ${dropped} 个——它们归属各自会话/变更)`
903
+ }
904
+ }
905
+ } catch { /* pathspec 缺失 → 开放区间(旧行为) */ }
906
+ }
907
+
599
908
  // —— 已收尾变更·冻结快照优先(真实 > 冻结记录 > 实时开放区间):归档变更的范围应封闭在
600
909
  // apply 时点,不随后续主仓改动漂移(quick-f5acdeeb);quick-bd84b852 放宽——**post-apply
601
910
  // (分支已删 = execute 已 apply 收尾)的活跃变更同样收尾**,此后实时区间只会被并行演进泡大
@@ -669,6 +978,7 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
669
978
  const degraded = []
670
979
  if (planDegraded) degraded.push(planDegraded)
671
980
  const notes = []
981
+ if (narrowNote) notes.push(narrowNote)
672
982
  // 已收尾但快照缺失(上方分支置位):实时开放区间兜底,明示漂移语义——快照机制上线
673
983
  // (2026-09-10)前收尾的旧变更冻结记录无法重建,如实告知替代渠道。
674
984
  if (noSnapshotDrift) {
@@ -693,10 +1003,20 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
693
1003
  notes.push('baseAnchor 缺失——行数按 HEAD 未提交窗口采集(不含已提交改动,quick 模式同口径;numstat 采集失败时行列为 —)')
694
1004
  }
695
1005
 
696
- // 他者声明排除面可见(R-04 同精神):resolveReconcileActualFiles 只回计数不回名单,退栈的
697
- // 计划内文件会误显「计划未动」——note 点破归属重叠可能性,让 ⚠️ 行可解释。
1006
+ // 他者声明排除面可见(R-04 同精神)+ 退栈名单消费(scope-audit-cross-repo-blindness 改进点 3):
1007
+ // planned 文件被退栈排除但已写盘 → 该行标 suspectedForeignDone「疑似他者已实现·已退栈」,
1008
+ // 与真未动区分(名单来自 resolveReconcileActualFiles 新增的 foreignExcludedFiles)。
1009
+ const foreignExcludedSet = new Set((Array.isArray(actual.foreignExcludedFiles) ? actual.foreignExcludedFiles : []).map(toPosix))
698
1010
  if (actual.foreignExcluded > 0) {
699
- notes.push(`实际侧另有 ${actual.foreignExcluded} 个文件按他者会话声明退栈未进本表——「计划未动」行先怀疑归属重叠(声明即归属,git diff 核实)`)
1011
+ notes.push(`实际侧另有 ${actual.foreignExcluded} 个文件按他者会话声明退栈未进本表——「计划未动」行标注「疑似他者已实现·已退栈」者即此类(声明即归属,git diff 核实)`)
1012
+ }
1013
+
1014
+ // 计划侧跨仓段汇总(改进点 2):跨仓条目不进本仓对账(actual 只采主仓 git),行级标
1015
+ // crossRepo + note 交代去对应仓对账。
1016
+ const crossRepoKeys = [...new Set(plannedEntries.map(e => e.repo).filter(Boolean))]
1017
+ if (crossRepoKeys.length > 0) {
1018
+ const crossCount = plannedEntries.filter(e => e.repo).length
1019
+ notes.push(`计划侧含 ${crossCount} 个跨仓文件(repo:${crossRepoKeys.join('、')})——⊘ 行不在本仓对账面(实际侧只采主仓 git),请到对应仓跑 scope-audit 对账`)
700
1020
  }
701
1021
 
702
1022
  // —— 三态判定 ——
@@ -716,6 +1036,12 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
716
1036
  } else {
717
1037
  row.planned = null
718
1038
  row.verdict = 'unplanned'
1039
+ // 工具/平台脚手架(改进点 1 软桶):CLI/平台自装文件不占「计划外」——硬排档
1040
+ // (.worktrees//.sillyspec-platform*/knowledge/local.yaml)已被 filterDeliverableFiles
1041
+ // 滤出不进 actual;此处打标的是可能正当交付的软档(.claude/skills/、CLAUDE.md、
1042
+ // attachments/),范围视图归入「工具/平台设施」桶。
1043
+ const facility = classifyToolScaffold(path)
1044
+ if (facility) row.facility = facility
719
1045
  }
720
1046
  }
721
1047
  rows.push(row)
@@ -723,7 +1049,16 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
723
1049
  // 计划未动:清单文件无实际改动 → 补行(行数 0/0);计划侧降级时不产(plannedEntries 为空天然不跑)
724
1050
  for (const e of plannedEntries) {
725
1051
  if (matchedPlanned.has(e.path)) continue
726
- rows.push({ path: e.path, planned: e.operation || null, additions: 0, deletions: 0, kind: 'modified', verdict: 'untouched' })
1052
+ const row = { path: e.path, planned: e.operation || null, additions: 0, deletions: 0, kind: 'modified', verdict: 'untouched' }
1053
+ if (e.repo) {
1054
+ // 跨仓条目(改进点 2):不是「未动」是「本表不含」
1055
+ row.crossRepo = e.repo
1056
+ } else {
1057
+ // 退栈排除 + 盘面已存在 → 他者会话已写盘(改进点 3);NEW: 前缀剥除后比对(pathMatches 同款口径)
1058
+ const norm = toPosix(e.path).replace(/^NEW:/, '')
1059
+ if (foreignExcludedSet.has(norm) && existsSync(join(numstatRoot, norm))) row.suspectedForeignDone = true
1060
+ }
1061
+ rows.push(row)
727
1062
  }
728
1063
 
729
1064
  // 冻结 patch(collectPatch,execute --done 落盘方消费):与行数同根同锚(numstatRoot +
@@ -764,9 +1099,18 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
764
1099
  * rows: Array<{ path: string, planned?: string|null, declared?: boolean,
765
1100
  * additions: number|null, deletions: number|null,
766
1101
  * kind: 'binary'|'new'|'modified'|'deleted',
767
- * verdict?: 'planned'|'unplanned'|'untouched', attribution?: 'declared'|'soft'|'undeclared' }>,
1102
+ * verdict?: 'planned'|'unplanned'|'untouched', attribution?: 'declared'|'soft'|'undeclared',
1103
+ * crossRepo?: string, suspectedForeignDone?: boolean, facility?: 'tool'|'platform' }>,
1104
+ * (crossRepo/suspectedForeignDone/facility 为 scope-audit-cross-repo-blindness 改进点 1/2/3
1105
+ * 增量字段:跨仓条目标 repoKey / 退栈排除但已写盘的 planned 文件 / 计划外行里的工具平台
1106
+ * 脚手架桶——均为按存在性读取,缺省无字段零回归)
768
1107
  * excluded: { foreignDeclared: Array<{ file: string, sessions: string[] }> },
769
- * note?: string|null }>}
1108
+ * note?: string|null,
1109
+ * gateProfile?: object|null }>}
1110
+ * gateProfile(2026-09-14-quick-exit-tiered-gates task-03 增量字段,消费方按存在性读取):
1111
+ * 仅 quick 模式携带——实时态透传 auditQuickCompletion 挂的 review.gateProfile(不重复计算);
1112
+ * 冻结重放态透传 quicklog/patches 记录冻结值(旧记录无 → rows 文件清单现算重放)。full-flow
1113
+ * 全流程变更恒无该字段(零回归契约)。
770
1114
  */
771
1115
  export async function computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts, collectPatch, freshActual } = {}) {
772
1116
  const empty = {
@@ -778,6 +1122,21 @@ export async function computeChangeScopeAudit({ cwd, specBase, changeName, platf
778
1122
  if (!cwd || !changeName) {
779
1123
  return { ...empty, degradedReason: '参数缺失:cwd 与 changeName 必填' }
780
1124
  }
1125
+ // ql-xxx 反查(「为什么不直接用 ql-xxx 查」根治):patches 记录按 qlId 命名且 json 冗余
1126
+ // sessionId——ql-xxx → quick-xxx 映射在 patches 里持久可查(guard 已清也能出记录态)。
1127
+ // 提升到 quick 判定外层:ql-xxx 不匹配 QUICK_SID_RE,在内部分支永远进不去。
1128
+ if (QL_ID_RE.test(changeName)) {
1129
+ const byQl = findQuickSessionByQlId(cwd, changeName)
1130
+ if (byQl) {
1131
+ const rec = { record: byQl.record, patchPath: byQl.patchPath, qlId: byQl.qlId, specBase: byQl.specBase }
1132
+ return buildQuickRecordResult(rec, changeName)
1133
+ }
1134
+ return {
1135
+ ...empty,
1136
+ mode: 'quick',
1137
+ degradedReason: `quick 条目 ${changeName} 在 quicklog/patches/ 无记录——会话未产生改动、早于记录机制,或 patches 目录已被清理`,
1138
+ }
1139
+ }
781
1140
  // quick 模式判定(pattern + guard 双条件,与 run/command.js :140/:652 口径一致)
782
1141
  if (QUICK_SID_RE.test(changeName)) {
783
1142
  const located = locateQuickSessionGuard(cwd, changeName)
@@ -788,21 +1147,7 @@ export async function computeChangeScopeAudit({ cwd, specBase, changeName, platf
788
1147
  // 命中出冻结记录(行数为 --done 时点真值),patch 可供 --file 切片
789
1148
  const rec = findQuickPatchRecord(cwd, changeName)
790
1149
  if (rec) {
791
- return {
792
- mode: 'quick', ok: true, degradedReason: null,
793
- baseAnchor: rec.record.baseAnchor || `quick-window:${changeName}`,
794
- totals: rec.record.totals && typeof rec.record.totals === 'object'
795
- ? rec.record.totals
796
- : { files: rec.record.rows.length, ...sumTotals(rec.record.rows) },
797
- rows: rec.record.rows,
798
- excluded: rec.record.excluded && Array.isArray(rec.record.excluded.foreignDeclared)
799
- ? rec.record.excluded
800
- : { foreignDeclared: [] },
801
- frozenPatchPath: rec.patchPath,
802
- patchSha256: rec.record.patchSha256 || null,
803
- patchStatus: rec.record.patchStatus || null,
804
- note: `quick 会话已收尾——记录态(quicklog/patches/${rec.qlId},${rec.record.savedAt ? String(rec.record.savedAt).replace('T', ' ').slice(0, 19) + ' 落盘' : '--done 时点冻结'})`,
805
- }
1150
+ return buildQuickRecordResult(rec, changeName)
806
1151
  }
807
1152
  return {
808
1153
  ...empty,
@@ -891,7 +1236,7 @@ export async function getFileDiff({ cwd, specBase, changeName, platformOpts, fil
891
1236
  const located = locateQuickSessionGuard(cwd, changeName)
892
1237
  if (located) root = dirname(located.specBase)
893
1238
  } else {
894
- const form = existsSync(join(sb, '.runtime', 'worktrees', changeName, 'meta.json')) ? 'worktree' : 'post-apply'
1239
+ const form = _readWorktreeMeta(sb || null, cwd, changeName) ? 'worktree' : 'post-apply'
895
1240
  root = resolveDiffRoot(sb, changeName, form, cwd)
896
1241
  }
897
1242
 
@@ -914,12 +1259,16 @@ export async function getFileDiff({ cwd, specBase, changeName, platformOpts, fil
914
1259
  }
915
1260
  }
916
1261
 
917
- /** verdict / attribution → 展示标记(人类可读面;BIN 为 binary 行数占位,— 为降级占位) */
1262
+ /** verdict / attribution → 展示标记(人类可读面;BIN 为 binary 行数占位,— 为降级占位)。
1263
+ * 行级标注优先(scope-audit-cross-repo-blindness 改进点 1/2/3):跨仓 > 疑似他者已实现 > 设施桶 */
918
1264
  const VERDICT_LABEL = {
919
1265
  planned: '✓ 计划内',
920
1266
  unplanned: '⚠️ 计划外',
921
1267
  untouched: '⚠️ 计划未动',
922
1268
  }
1269
+ const CROSS_REPO_LABEL = '⊘ 跨仓(本表不含)'
1270
+ const SUSPECTED_FOREIGN_LABEL = '⚠️ 计划未动(疑似他者已实现·已退栈)'
1271
+ const FACILITY_LABEL = '⚠️ 计划外(工具/平台设施)'
923
1272
  const ATTRIBUTION_LABEL = {
924
1273
  declared: '✓ 已声明',
925
1274
  soft: '🔍 软归属',
@@ -936,6 +1285,8 @@ function fmtCount(n) {
936
1285
  *
937
1286
  * 防御式:result 任意字段缺失/畸形不抛(advisory 展示层)。opts.maxRows 截断 + 指引行
938
1287
  * (R-03:大表注入场景由调用方传 60,命令直跑缺省全表)。
1288
+ * [gate] 画像段(task-03,gate-table-section):result.gateProfile 存在时(quick 模式)追加
1289
+ * 级别/跨度/模块/风险命中/缺失检查项段;字段缺失/为 null(full-flow / 重放不可得)→ 零输出零回归。
939
1290
  *
940
1291
  * @param {object} result computeChangeScopeAudit 返回值
941
1292
  * @param {{ maxRows?: number }} [opts] 可选截断(缺省不截)
@@ -980,7 +1331,10 @@ export function renderScopeAuditTable(result, opts = {}) {
980
1331
  const stat = row.kind === 'binary'
981
1332
  ? { a: 'BIN', d: 'BIN' }
982
1333
  : { a: fmtCount(row.additions) ?? '—', d: fmtCount(row.deletions) ?? '—' }
983
- const label = VERDICT_LABEL[row.verdict] || ATTRIBUTION_LABEL[row.attribution] || ''
1334
+ const label = row.crossRepo ? CROSS_REPO_LABEL
1335
+ : row.suspectedForeignDone ? SUSPECTED_FOREIGN_LABEL
1336
+ : row.facility ? FACILITY_LABEL
1337
+ : VERDICT_LABEL[row.verdict] || ATTRIBUTION_LABEL[row.attribution] || ''
984
1338
  lines.push(` ${row.path || '(未知路径)'}${' '.repeat(Math.max(1, 40 - String(row.path || '').length))}${label}${' '.repeat(Math.max(1, 12 - label.length))}${String(stat.a).padStart(5)} ${String(stat.d).padStart(5)} ${row.kind || ''}`)
985
1339
  }
986
1340
  if (rows.length > shown.length) {
@@ -989,18 +1343,64 @@ export function renderScopeAuditTable(result, opts = {}) {
989
1343
 
990
1344
  lines.push(` 合计:${totals.files ?? rows.length} 文件 +${totals.additions ?? 0} / -${totals.deletions ?? 0}`)
991
1345
 
992
- // ⚠️ 出口指引(full-flow 三态面):计划外补声明、计划未动确认遗漏
993
- const unplanned = rows.filter(x => x && x.verdict === 'unplanned').length
994
- const untouched = rows.filter(x => x && x.verdict === 'untouched').length
995
- if (unplanned > 0 || untouched > 0) {
1346
+ // ⚠️ 出口指引(full-flow 三态面):计划外补声明、计划未动确认遗漏。
1347
+ // 行级标注三分(scope-audit-cross-repo-blindness 改进点 1/2/3):跨仓行/疑似他者已实现行/
1348
+ // 工具设施桶各有去向说明,不混入笼统的「计划外/计划未动」计数逼用户排查噪音。
1349
+ const unplanned = rows.filter(x => x && x.verdict === 'unplanned' && !x.facility).length
1350
+ const facility = rows.filter(x => x && x.verdict === 'unplanned' && x.facility).length
1351
+ const untouchedReal = rows.filter(x => x && x.verdict === 'untouched' && !x.suspectedForeignDone && !x.crossRepo).length
1352
+ const suspectedForeign = rows.filter(x => x && x.suspectedForeignDone).length
1353
+ const crossRepo = rows.filter(x => x && x.crossRepo).length
1354
+ if (unplanned > 0 || untouchedReal > 0) {
996
1355
  const parts = []
997
1356
  if (unplanned > 0) parts.push(`计划外 ${unplanned} 文件`)
998
- if (untouched > 0) parts.push(`计划未动 ${untouched} 文件`)
1357
+ if (untouchedReal > 0) parts.push(`计划未动 ${untouchedReal} 文件`)
999
1358
  lines.push(` ⚠️ ${parts.join('、')}——计划外请补 design.md 声明或 --output 注明原因;计划未动请确认是否遗漏`)
1000
1359
  }
1360
+ if (suspectedForeign > 0) {
1361
+ lines.push(` ℹ️ 疑似他者已实现 ${suspectedForeign} 文件(已写盘但被退栈——他者会话声明归属,本表不计未动;git diff 核实归属)`)
1362
+ }
1363
+ if (facility > 0) {
1364
+ lines.push(` ℹ️ 工具/平台设施 ${facility} 文件(CLI/平台自装脚手架,非本变更改动,不占计划外)`)
1365
+ }
1366
+ if (crossRepo > 0) {
1367
+ lines.push(` ℹ️ 跨仓 ${crossRepo} 文件(⊘ 行——本表不含,请到对应仓跑 scope-audit 对账)`)
1368
+ }
1001
1369
  const undeclared = rows.filter(x => x && x.attribution === 'undeclared').length
1002
1370
  if (undeclared > 0) lines.push(` ⚠️ 未声明 ${undeclared} 文件——超出 allowedFiles 声明面,补 --files 声明或注明原因`)
1003
1371
 
1372
+ // ── [gate] 画像段(2026-09-14-quick-exit-tiered-gates task-03,FR-04 / D-008 gate-table-section):
1373
+ // quick 会话分级门禁画像的表格出口。按 gateProfile 存在性渲染——full-flow 变更 / 画像为 null
1374
+ // → 零输出零回归;检查项随级别分层(L1:每文件注记+测试增量;L2:模块文档认领+风险命中),
1375
+ // 与 run/quick-audit.js [gate] 打印块同一 review.gateProfile 数据源、同 tier 语义(advisory 展示,
1376
+ // 不构成门禁)。防御式:字段缺失/畸形不抛(degraded 无 span、modules 空集等均有占位)。
1377
+ const gate = r.gateProfile && typeof r.gateProfile === 'object' ? r.gateProfile : null
1378
+ if (gate) {
1379
+ const gateModules = Array.isArray(gate.modules) ? gate.modules.filter(m => m && typeof m === 'object') : []
1380
+ const spanTxt = gate.degraded
1381
+ ? '无 module-map(降级档:span 不参与判级)'
1382
+ : `跨 ${gate.moduleSpan ?? '—'} 模块${gateModules.length > 0 ? `(${gateModules.map(m => String(m.id)).join(' · ')})` : ''}`
1383
+ lines.push(` 🚦 [gate] ${gate.level ?? '—'} 规模门(${spanTxt} · ${gate.fileCount ?? '—'} 文件:${gate.codeFileCount ?? '—'} 代码 / ${gate.testFileCount ?? '—'} 测试)——advisory 画像不阻断`)
1384
+ if (Array.isArray(gate.unmappedFiles) && gate.unmappedFiles.length > 0) {
1385
+ lines.push(` ⚠️ ${gate.unmappedFiles.length} 个文件未命中 module-map(${gate.unmappedFiles.slice(0, 5).join(', ')}${gate.unmappedFiles.length > 5 ? ' 等' : ''})`)
1386
+ }
1387
+ const checks = gate.checks && typeof gate.checks === 'object' ? gate.checks : {}
1388
+ if (gate.level === 'L1') {
1389
+ if (checks.perFileNotes === false) lines.push(' - 每文件注记缺失——补 --file-notes "path1::注 || path2::注"(覆盖变更文件全集)')
1390
+ if (checks.testDelta === 'missing') lines.push(` - 测试增量缺失——${gate.codeFileCount} 个代码文件改动无测试文件`)
1391
+ } else if (gate.level === 'L2') {
1392
+ if (checks.docClaim === 'missing') lines.push(' - 模块文档认领缺失——触及模块的卡片文件不在改动集(补模块卡/changelog 或 --no-docs 豁免)')
1393
+ if (checks.docClaim === 'exempt-no-docs') lines.push(' - 模块文档认领:--no-docs 显式豁免')
1394
+ }
1395
+ if (Array.isArray(gate.riskHits) && gate.riskHits.length > 0) {
1396
+ lines.push(` - 风险命中 ${gate.riskHits.length} 处(运行时证据要求——风险路径改动需附运行验证说明):`)
1397
+ for (const h of gate.riskHits.slice(0, 5)) {
1398
+ if (h && typeof h === 'object') lines.push(` · [${h.pattern}] ${h.file}`)
1399
+ }
1400
+ if (gate.riskHits.length > 5) lines.push(` … 共 ${gate.riskHits.length} 处`)
1401
+ }
1402
+ }
1403
+
1004
1404
  // 他者会话声明排除面(R-04:不进 rows,单列可见)
1005
1405
  if (foreignDeclared.length > 0) {
1006
1406
  lines.push(` 他者会话声明(不进上表,归属对应会话审计):`)