sillyspec 3.28.8 → 3.28.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/full.json +1 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -224
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +118 -117
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/change-risk-profile.js +20 -0
- package/src/config-schema.js +62 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/db.js +11 -3
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +89 -20
- package/src/docs-gate.js +105 -6
- package/src/doctor-diagnostics.js +224 -2
- package/src/friction-ledger.js +171 -0
- package/src/hooks/worktree-guard.js +13 -1
- package/src/index.js +206 -18
- package/src/knowledge-classify.js +382 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/probe7-anchor-check.js +69 -0
- package/src/progress/change-registry.js +183 -8
- package/src/progress/shared.js +3 -2
- package/src/progress.js +100 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +164 -18
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +131 -13
- package/src/run/complete-handlers.js +511 -21
- package/src/run/complete.js +123 -23
- package/src/run/concurrent-detect.js +98 -0
- package/src/run/gate-snapshot.js +110 -2
- package/src/run/gates.js +141 -16
- package/src/run/prompt.js +221 -11
- package/src/run/quick-audit.js +111 -13
- package/src/run/shared.js +147 -11
- package/src/run/stage.js +6 -0
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +456 -56
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +23 -7
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/execute.js +85 -3
- package/src/stages/knowledge.js +76 -6
- package/src/stages/plan-postcheck.js +32 -5
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +170 -6
- package/src/sync.js +141 -9
- package/src/task-review.js +366 -46
- package/src/verify-postcheck.js +212 -55
- package/src/verify-probes.js +727 -16
- package/src/worktree-apply.js +685 -48
- package/src/worktree-deps.js +124 -3
- package/src/worktree.js +391 -15
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/.patch16-tmp.mjs +0 -169
- package/sa-rename-probe/src/new.js +0 -4
package/src/scope-audit.js
CHANGED
|
@@ -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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
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
|
-
//
|
|
423
|
-
// declared
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ——
|
|
566
|
-
//
|
|
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
|
|
571
|
-
if (
|
|
572
|
-
|
|
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
|
|
697
|
-
//
|
|
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}
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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 (
|
|
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(` 他者会话声明(不进上表,归属对应会话审计):`)
|