sillyspec 3.28.8 → 3.28.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-risk-profile.js +20 -0
- package/src/config-schema.js +32 -0
- package/src/db.js +11 -3
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +33 -3
- 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 +188 -17
- package/src/knowledge-classify.js +363 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -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 +155 -14
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +107 -12
- package/src/run/complete-handlers.js +511 -21
- package/src/run/complete.js +54 -16
- package/src/run/concurrent-detect.js +98 -0
- package/src/run/gates.js +50 -9
- package/src/run/prompt.js +134 -5
- package/src/run/quick-audit.js +111 -13
- package/src/run/shared.js +76 -10
- 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 +365 -35
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +23 -7
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/execute.js +85 -3
- package/src/stages/knowledge.js +52 -3
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +114 -6
- package/src/sync.js +135 -7
- package/src/task-review.js +292 -21
- package/src/verify-postcheck.js +20 -0
- package/src/verify-probes.js +340 -4
- package/src/worktree-apply.js +538 -44
- package/src/worktree-deps.js +100 -2
- package/src/worktree.js +117 -5
- 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,13 +15,20 @@
|
|
|
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'
|
|
27
34
|
|
|
@@ -266,18 +273,20 @@ function filterPatchForFiles(patchText, filesSet) {
|
|
|
266
273
|
}
|
|
267
274
|
|
|
268
275
|
/**
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
276
|
+
* patch sha256——换行归一化口径(quick-ec5fc714):git autocrlf 会在提交/检出时把 patch
|
|
277
|
+
* 转 CRLF(平台仓实测 mtime 晚落盘一天的假篡改告警)——双侧统一 LF 归一后取 hash。
|
|
278
|
+
*/
|
|
279
|
+
function sha256PatchNormalized(text) {
|
|
280
|
+
return createHash('sha256').update(String(text).replace(/\r\n/g, '\n'), 'utf8').digest('hex')
|
|
272
281
|
}
|
|
273
282
|
|
|
274
283
|
/**
|
|
275
|
-
* 冻结 patch 完整性校验(A-F01):json 记录的 patchSha256 与 patch
|
|
284
|
+
* 冻结 patch 完整性校验(A-F01):json 记录的 patchSha256 与 patch 实文比对(LF 归一口径)。
|
|
276
285
|
* @returns {boolean|null} true=一致;false=不匹配(可能被篡改);null=未记录 hash(旧记录)
|
|
277
286
|
*/
|
|
278
287
|
function verifyPatchIntegrity(recordedSha, patchText) {
|
|
279
288
|
if (!recordedSha || typeof recordedSha !== 'string') return null
|
|
280
|
-
return
|
|
289
|
+
return sha256PatchNormalized(patchText) === recordedSha
|
|
281
290
|
}
|
|
282
291
|
|
|
283
292
|
/**
|
|
@@ -344,7 +353,8 @@ function slicePatchForFile(patchText, filePath) {
|
|
|
344
353
|
|
|
345
354
|
/**
|
|
346
355
|
* quick 记录态反查(guard 清理后):扫祖先链各 specBase 的 quicklog/patches/*.json,
|
|
347
|
-
* 按 json 内冗余的 sessionId 匹配。返回记录 + patch 路径(--file
|
|
356
|
+
* 按 json 内冗余的 sessionId 匹配。返回记录 + patch 路径(--file 切片源)+ specBase
|
|
357
|
+
* (命中目录——画像重放的 module-map/local.yaml 加载锚,task-03)。
|
|
348
358
|
*/
|
|
349
359
|
function findQuickPatchRecord(cwd, sessionId) {
|
|
350
360
|
for (const sb of ancestorSpecDirs(cwd)) {
|
|
@@ -355,7 +365,7 @@ function findQuickPatchRecord(cwd, sessionId) {
|
|
|
355
365
|
try {
|
|
356
366
|
const j = JSON.parse(readFileSync(join(dir, f), 'utf8'))
|
|
357
367
|
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$/, '') }
|
|
368
|
+
return { record: j, patchPath: join(dir, f.replace(/\.json$/, '.patch')), qlId: j.qlId || f.replace(/\.json$/, ''), specBase: sb }
|
|
359
369
|
}
|
|
360
370
|
} catch { /* 单文件损坏跳过 */ }
|
|
361
371
|
}
|
|
@@ -363,6 +373,167 @@ function findQuickPatchRecord(cwd, sessionId) {
|
|
|
363
373
|
return null
|
|
364
374
|
}
|
|
365
375
|
|
|
376
|
+
/** QUICKLOG 条目号形态(ql-20260914-001-abcd:日期-序号-短 hash,短 hash 4 hex) */
|
|
377
|
+
const QL_ID_RE = /^ql-\d{8}-\d{3}-[0-9a-f]{4}$/
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* ql-xxx → quick-xxx 反查(用户「为什么不直接用 ql-xxx 查」根治,ql-20260910-014 设计:
|
|
381
|
+
* 映射只在本机 guard,会话结束即清;patches 记录按 qlId 命名且 json 冗余 sessionId——
|
|
382
|
+
* 平台可抓的持久映射)。按文件名前缀匹配(qlId=文件名),避免逐文件解析全目录。
|
|
383
|
+
*/
|
|
384
|
+
function findQuickSessionByQlId(cwd, qlId) {
|
|
385
|
+
const norm = String(qlId || '').trim()
|
|
386
|
+
if (!QL_ID_RE.test(norm)) return null
|
|
387
|
+
for (const sb of ancestorSpecDirs(cwd)) {
|
|
388
|
+
try {
|
|
389
|
+
const jsonPath = join(sb, 'quicklog', 'patches', `${norm}.json`)
|
|
390
|
+
if (!existsSync(jsonPath)) continue
|
|
391
|
+
const j = JSON.parse(readFileSync(jsonPath, 'utf8'))
|
|
392
|
+
if (j && typeof j.sessionId === 'string' && QUICK_SID_RE.test(j.sessionId)) {
|
|
393
|
+
return { sessionId: j.sessionId, record: j, patchPath: join(sb, 'quicklog', 'patches', `${norm}.patch`), qlId: norm, specBase: sb }
|
|
394
|
+
}
|
|
395
|
+
} catch { /* 单文件损坏跳过 */ }
|
|
396
|
+
}
|
|
397
|
+
return null
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* quick 记录态回放结果构建(共用:guard 清理后 sessionId 反查 + ql-xxx 反查两入口)。
|
|
402
|
+
* 画像重放(FR-04 / D-008 / task-03)+ 「声明即归属」过滤(quick-ec5fc714)。
|
|
403
|
+
*/
|
|
404
|
+
async function buildQuickRecordResult(rec, requestedName) {
|
|
405
|
+
let gateProfile = null
|
|
406
|
+
try {
|
|
407
|
+
if (rec.record.gateProfile && typeof rec.record.gateProfile === 'object') {
|
|
408
|
+
gateProfile = rec.record.gateProfile
|
|
409
|
+
} else {
|
|
410
|
+
const rowsPaths = rec.record.rows
|
|
411
|
+
.map(r => (r && typeof r.path === 'string' ? r.path : ''))
|
|
412
|
+
.filter(Boolean)
|
|
413
|
+
if (rowsPaths.length > 0) {
|
|
414
|
+
const thresholds = await resolveReplayGateThresholds(rec.specBase)
|
|
415
|
+
gateProfile = computeGateProfile(
|
|
416
|
+
rowsPaths,
|
|
417
|
+
await loadGateModuleIndex(rec.specBase, rowsPaths),
|
|
418
|
+
thresholds ? { thresholds } : {},
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} catch { gateProfile = null }
|
|
423
|
+
const filteredRows = rec.record.rows.filter(r => r && !isQuickMetadata(r.path || '', []))
|
|
424
|
+
const filterDropped = rec.record.rows.length - filteredRows.length
|
|
425
|
+
const viaQl = requestedName !== rec.record.sessionId && QL_ID_RE.test(requestedName)
|
|
426
|
+
return {
|
|
427
|
+
mode: 'quick', ok: true, degradedReason: null,
|
|
428
|
+
baseAnchor: rec.record.baseAnchor || `quick-window:${rec.record.sessionId || requestedName}`,
|
|
429
|
+
totals: rec.record.totals && typeof rec.record.totals === 'object' && filterDropped === 0
|
|
430
|
+
? rec.record.totals
|
|
431
|
+
: { files: filteredRows.length, ...sumTotals(filteredRows) },
|
|
432
|
+
rows: filteredRows,
|
|
433
|
+
excluded: rec.record.excluded && Array.isArray(rec.record.excluded.foreignDeclared)
|
|
434
|
+
? rec.record.excluded
|
|
435
|
+
: { foreignDeclared: [] },
|
|
436
|
+
frozenPatchPath: rec.patchPath,
|
|
437
|
+
patchSha256: rec.record.patchSha256 || null,
|
|
438
|
+
patchStatus: rec.record.patchStatus || null,
|
|
439
|
+
gateProfile,
|
|
440
|
+
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 元数据/并行变更目录文件(范围视图收紧)` : ''}`,
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* quick 审计链 module-map 的项目推导(2026-09-14-quick-exit-tiered-gates task-03):
|
|
446
|
+
* loadQuickModuleIndex(run/shared.js,未 export)的加载路径是
|
|
447
|
+
* <specBase>/docs/<projectName>/modules/_module-map.yaml——scope-audit 侧无 progress 上下文
|
|
448
|
+
* 拿 projectName,按候选 map 推导:
|
|
449
|
+
* - 单候选 → 直取(绝大多数仓单项目);
|
|
450
|
+
* - 多候选 + 样本文件(实时态=guard.allowedFiles 声明面 / 重放态=记录 rows 冻结集)→ 按
|
|
451
|
+
* 「样本归属得分唯一最高」消歧(computeGateProfile.unmappedFiles 同款归属口径,纯函数
|
|
452
|
+
* 零 IO 重用——本仓 dashboard+sillyspec 双 map 实证需要);
|
|
453
|
+
* - 零候选 / 无样本 / 得分平分 → null(画像走 degraded 档,fail-open 不猜,兼容策略第 1 条)。
|
|
454
|
+
* @param {string} specBase
|
|
455
|
+
* @returns {string[]} 带 _module-map.yaml 的项目名清单
|
|
456
|
+
*/
|
|
457
|
+
function listModuleMapProjects(specBase) {
|
|
458
|
+
if (!specBase) return []
|
|
459
|
+
try {
|
|
460
|
+
const docsDir = join(specBase, 'docs')
|
|
461
|
+
return readdirSync(docsDir).filter(n => existsSync(join(docsDir, n, 'modules', '_module-map.yaml')))
|
|
462
|
+
} catch {
|
|
463
|
+
return []
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** 单项目 _module-map.yaml 读取(parseModuleMapSimple canonical 解析,与 loadQuickModuleIndex 同款);不可得/解析空 → null。 */
|
|
468
|
+
async function readModuleMap(specBase, project) {
|
|
469
|
+
try {
|
|
470
|
+
const { parseModuleMapSimple } = await import('./modules.js')
|
|
471
|
+
const idx = parseModuleMapSimple(readFileSync(join(specBase, 'docs', project, 'modules', '_module-map.yaml'), 'utf8'))
|
|
472
|
+
return idx && Object.keys(idx).length > 0 ? idx : null
|
|
473
|
+
} catch {
|
|
474
|
+
return null
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* 项目名推导(口径见 listModuleMapProjects 头注释):唯一得分(样本非文档文件中归属最少
|
|
480
|
+
* unmapped 的 map)才采信,平分/无样本/多候选全失败 → null。
|
|
481
|
+
* @param {string} specBase
|
|
482
|
+
* @param {string[]} [sampleFiles] 归属得分样本(声明面 / 记录 rows)
|
|
483
|
+
* @returns {Promise<string|null>}
|
|
484
|
+
*/
|
|
485
|
+
async function pickModuleMapProject(specBase, sampleFiles = []) {
|
|
486
|
+
const names = listModuleMapProjects(specBase)
|
|
487
|
+
if (names.length === 1) return names[0]
|
|
488
|
+
if (names.length === 0 || !Array.isArray(sampleFiles)) return null
|
|
489
|
+
const files = sampleFiles.map(f => String(f || '')).filter(Boolean)
|
|
490
|
+
if (files.length === 0) return null
|
|
491
|
+
let best = null
|
|
492
|
+
let bestUnmapped = Infinity
|
|
493
|
+
let tie = false
|
|
494
|
+
for (const n of names) {
|
|
495
|
+
const idx = await readModuleMap(specBase, n)
|
|
496
|
+
if (!idx) continue
|
|
497
|
+
const unmapped = computeGateProfile(files, idx).unmappedFiles.length
|
|
498
|
+
if (unmapped < bestUnmapped) { best = n; bestUnmapped = unmapped; tie = false }
|
|
499
|
+
else if (unmapped === bestUnmapped) tie = true
|
|
500
|
+
}
|
|
501
|
+
return tie ? null : best
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* 画像重放的 moduleIndex 加载(task-03,FR-04):与 run/shared.js loadQuickModuleIndex 同链路
|
|
506
|
+
* ——同路径 + parseModuleMapSimple(canonical)解析,项目按记录 rows 样本消歧;不可得 → null
|
|
507
|
+
* (computeGateProfile 走 degraded 档)。动态 import modules.js 隔离依赖(shared.js 同款)。
|
|
508
|
+
* @param {string} specBase quicklog/patches 记录命中的 spec 根
|
|
509
|
+
* @param {string[]} [sampleFiles] 项目消歧样本(记录 rows)
|
|
510
|
+
* @returns {Promise<object|null>}
|
|
511
|
+
*/
|
|
512
|
+
async function loadGateModuleIndex(specBase, sampleFiles = []) {
|
|
513
|
+
const project = await pickModuleMapProject(specBase, sampleFiles)
|
|
514
|
+
return project ? readModuleMap(specBase, project) : null
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* 画像重放的阈值合并(task-03,D-009):与 auditQuickCompletion 画像挂载段同链路——
|
|
519
|
+
* <specBase>/local.yaml(= 实时态 readLocalYamlRaw(sessionRoot) 读的同一文件)+ js-yaml +
|
|
520
|
+
* resolveGateThresholds;缺失/坏 YAML → null(computeGateProfile 内回 THRESHOLDS 默认)。
|
|
521
|
+
* @param {string} specBase
|
|
522
|
+
* @returns {Promise<object|null>}
|
|
523
|
+
*/
|
|
524
|
+
async function resolveReplayGateThresholds(specBase) {
|
|
525
|
+
try {
|
|
526
|
+
const p = join(specBase, 'local.yaml')
|
|
527
|
+
if (!existsSync(p)) return null
|
|
528
|
+
const mod = await import('js-yaml')
|
|
529
|
+
const yamlLoad = mod.load || mod.default?.load
|
|
530
|
+
const cfg = yamlLoad(readFileSync(p, 'utf8'))
|
|
531
|
+
return cfg && typeof cfg === 'object' ? resolveGateThresholds(cfg) : null
|
|
532
|
+
} catch {
|
|
533
|
+
return null
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
366
537
|
/** 汇总非 null 行数(binary 与降级 null 不计入,不出伪数据) */
|
|
367
538
|
function sumTotals(rows) {
|
|
368
539
|
let additions = 0
|
|
@@ -391,6 +562,16 @@ async function computeQuickAudit({ cwd, platformOpts, sessionId, located, collec
|
|
|
391
562
|
try {
|
|
392
563
|
const mergedGuard = {
|
|
393
564
|
...guard,
|
|
565
|
+
// 画像输入对齐(2026-09-14-quick-exit-tiered-gates task-03,FR-04):specBase/projectName
|
|
566
|
+
// 透传——complete-handlers :1050 同款 merge(「--done 收尾同源实时采集」的既有约定)补齐。
|
|
567
|
+
// guard.json 落盘不含这两键,缺省会让 auditQuickCompletion 挂的 review.gateProfile 恒走
|
|
568
|
+
// degraded 档(moduleSpan=null),与 --done 时点 review.gateProfile 字段不一致。projectName
|
|
569
|
+
// 无 progress 上下文,按 module-map 候选推导(声明会话以 allowedFiles 为归属得分样本;
|
|
570
|
+
// 零候选/无样本/平分 → null → degraded,fail-open)。specBase/projectName 在 audit 内只喂
|
|
571
|
+
// quicklog 目录解析(与 cwd 推导同一路径)、docSyncHint 模块归属与 gate 画像(computeQuickAudit
|
|
572
|
+
// 不消费 docSyncHint),不触 status/reasons/归属切分语义。
|
|
573
|
+
specBase: guardSpecBase,
|
|
574
|
+
projectName: await pickModuleMapProject(guardSpecBase, guard.allowedFiles),
|
|
394
575
|
otherSessionsDeclared: collectOtherQuickSessionDeclarations(platformOpts, guardSpecBase, sessionId),
|
|
395
576
|
}
|
|
396
577
|
audit = await auditQuickCompletion(sessionRoot, mergedGuard, {})
|
|
@@ -419,14 +600,39 @@ async function computeQuickAudit({ cwd, platformOpts, sessionId, located, collec
|
|
|
419
600
|
return { ...base, ok: false, degradedReason: `quick 窗口审计失败: ${hardFailure}` }
|
|
420
601
|
}
|
|
421
602
|
|
|
422
|
-
//
|
|
423
|
-
// declared
|
|
603
|
+
// 归属切分(quick-ec5fc714 平台实测修正):scope-audit 是**范围视图**不是门禁——按
|
|
604
|
+
// 「声明即归属」硬切:declared=声明∩窗口;soft=软归属;其余(含 quick 元数据/其他变更
|
|
605
|
+
// 目录/linkedChanges 文件——audit 侧刻意保留它们供审计行落盘不静默挤走,范围视图则不该
|
|
606
|
+
// 把别人的活算进来)归为 undeclared 仅 note 交代。窗口文件全在 undeclared(如声明文件
|
|
607
|
+
// 已提交无 diff)时,undeclared 升格进 rows(范围视图只展示改了的东西)——本 quick
|
|
608
|
+
// 实测:声明两 pi-rpc 文件已提交 + 他者变更目录 19 文件未声明,行数贡献恰来自后者。
|
|
424
609
|
const attributed = Array.isArray(audit.attributedFiles) ? audit.attributedFiles.map(toPosix) : []
|
|
425
610
|
const undeclared = Array.isArray(audit.undeclaredFiles) ? audit.undeclaredFiles.map(toPosix) : []
|
|
426
611
|
const softSet = new Set((Array.isArray(audit.softTestFiles) ? audit.softTestFiles : []).map(toPosix))
|
|
427
612
|
const attrSet = new Set(attributed)
|
|
428
|
-
|
|
429
|
-
const
|
|
613
|
+
const linkedChangesArr = Array.isArray(guard.linkedChanges) ? guard.linkedChanges.map(c => String(c).replace(/\\/g, '/')) : []
|
|
614
|
+
const isLinkedLeftover = (f) => linkedChangesArr.some(lc => f.startsWith(`.sillyspec/changes/${lc}/`) || f.startsWith(`.sillyspec/changes/archive/${lc}/`))
|
|
615
|
+
|
|
616
|
+
let rowFiles
|
|
617
|
+
let dropNote = null
|
|
618
|
+
if (attributed.length > 0 || softSet.size > 0) {
|
|
619
|
+
// 有归属文件时:undeclared 全部剔出范围视图——undeclared 即「不属于本 quick」的文件
|
|
620
|
+
// (他者变更目录/quicklog 产物/无关未提交),audit 侧留它们供审计行追溯,范围视图不该
|
|
621
|
+
// 展示(平台仓实测:窗口 13 个文件里 11 个是他者变更目录,唯二归属文件已提交)。
|
|
622
|
+
rowFiles = [...new Set([...attributed, ...softSet])]
|
|
623
|
+
if (undeclared.length > 0) {
|
|
624
|
+
dropNote = `${undeclared.length} 个未声明文件不计入范围视图(他者变更目录/quicklog 产物/漏声明——audit 行有追溯)`
|
|
625
|
+
}
|
|
626
|
+
} else {
|
|
627
|
+
// 窗口全 undeclared(如声明文件已提交无 diff):范围视图仍剔 quick 元数据/变更目录
|
|
628
|
+
// ——audit 侧留它们供审计行追溯,范围视图不展示他者活(platform quick-ffb92f60 实测:
|
|
629
|
+
// 19 个 undeclared 中 17 个是他者变更目录/quicklog 产物,唯二声明文件已提交)。真未知的
|
|
630
|
+
// 源码/业务文件保留进 rows(他者窗口改动或漏声明,note 交代)。
|
|
631
|
+
rowFiles = [...new Set(undeclared.filter(f => !isQuickMetadata(f, linkedChangesArr) && !isLinkedLeftover(f)))]
|
|
632
|
+
if (rowFiles.length < undeclared.length) {
|
|
633
|
+
dropNote = `${undeclared.length - rowFiles.length} 个 quick 元数据/他者变更目录文件不计入范围视图`
|
|
634
|
+
}
|
|
635
|
+
}
|
|
430
636
|
const stats = collectNumstatByPath(sessionRoot, rowFiles, { baseRef: 'HEAD' })
|
|
431
637
|
|
|
432
638
|
const rows = rowFiles.map(f => {
|
|
@@ -446,7 +652,7 @@ async function computeQuickAudit({ cwd, platformOpts, sessionId, located, collec
|
|
|
446
652
|
|
|
447
653
|
// 「quick 已提交」降级(design 总体方案):窗口空(changedFiles 空)且 QUICKLOG 已有该会话
|
|
448
654
|
// 条目 → 记录态提示读 QUICKLOG,不以空表冒充实时。
|
|
449
|
-
let note =
|
|
655
|
+
let note = dropNote
|
|
450
656
|
if (rowFiles.length === 0) {
|
|
451
657
|
const qlId = guard.quicklogId || null
|
|
452
658
|
if (quicklogHasEntry(guardSpecBase, qlId)) {
|
|
@@ -475,6 +681,11 @@ async function computeQuickAudit({ cwd, platformOpts, sessionId, located, collec
|
|
|
475
681
|
},
|
|
476
682
|
frozenPatch,
|
|
477
683
|
note,
|
|
684
|
+
// 画像透传(FR-04 / task-03 gateProfile-json):auditQuickCompletion 挂的 review.gateProfile
|
|
685
|
+
// 原样随 review 携带进结果对象——不重复计算(与 --done 时点同源);fail-open 时为 null
|
|
686
|
+
// (消费方按存在性读取)。complete-handlers --done 落 quicklog/patches/<qlId>.json 时随
|
|
687
|
+
// 快照对象冻结,供冻结重放路径透传。
|
|
688
|
+
gateProfile: audit.gateProfile && typeof audit.gateProfile === 'object' ? audit.gateProfile : null,
|
|
478
689
|
}
|
|
479
690
|
}
|
|
480
691
|
|
|
@@ -562,16 +773,71 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
562
773
|
: `design.md 不存在(${toPosix(designMdPath)})——降级实际侧 only 视图(不出三态列)`
|
|
563
774
|
}
|
|
564
775
|
|
|
565
|
-
// ——
|
|
566
|
-
//
|
|
776
|
+
// —— 实际侧采集 ——
|
|
777
|
+
// 形态 A(worktree 存活,quick-ec5fc714):**自采纯 worktree 口径**——锚(meta baselineCommit
|
|
778
|
+
// 优先)..worktree 工作区 = 本变更真实边界(子代理未提交改动 + 分支已提交)。不沿用
|
|
779
|
+
// resolveVerifyChangedFiles(其语义是「verify 证据核验面」:主仓∪worktree 并集——对证据核验
|
|
780
|
+
// 合理,对本变更范围全是噪音:主仓侧 20 并行文件混入,且它们对 worktree 锚 diff miss 后落
|
|
781
|
+
// wc-l 档记成整文件行数,两放大器叠加出 +11840/22 文件的巨型表)。
|
|
782
|
+
// 形态 B(post-apply):继续走 resolveReconcileActualFiles 三源(本变更活已回主仓,语义正确)。
|
|
567
783
|
let actual = null
|
|
568
784
|
let actualFailure = null
|
|
569
785
|
try {
|
|
570
|
-
const
|
|
571
|
-
if (
|
|
572
|
-
|
|
786
|
+
const metaPath = join(sb, '.runtime', 'worktrees', changeName, 'meta.json')
|
|
787
|
+
if (existsSync(metaPath)) {
|
|
788
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
|
|
789
|
+
const wtRoot = (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath))
|
|
790
|
+
? meta.worktreePath
|
|
791
|
+
: cwd
|
|
792
|
+
const anchor = meta.baselineCommit || meta.actualBaseHash || meta.baseHash || null
|
|
793
|
+
if (anchor) {
|
|
794
|
+
const diffOut = safeGit(wtRoot, ['diff', '--name-only', anchor], { timeout: 30 * 1000 })
|
|
795
|
+
if (!diffOut.error && typeof diffOut.value === 'string') {
|
|
796
|
+
// 过滤口径与 resolveVerifyChangedFiles 的 wtFiles 同款(保留 docs 交付物,排运行时/进度面)
|
|
797
|
+
const wtFilter = (p) => p && p !== '.sillyspec'
|
|
798
|
+
&& !p.startsWith('.sillyspec/changes/')
|
|
799
|
+
&& !p.startsWith('.sillyspec/.runtime/')
|
|
800
|
+
&& !p.startsWith('.sillyspec/quicklog/')
|
|
801
|
+
const files = diffOut.value.split('\n')
|
|
802
|
+
.map(p => toPosix(p.replace(/^"|"$/g, '').trim()))
|
|
803
|
+
.filter(wtFilter)
|
|
804
|
+
// untracked 新文件不在 diff(git diff 只看 tracked)——status --porcelain 并入
|
|
805
|
+
// (与旧链路 includeWorkingTree 的 wtFiles 半边同覆盖,行数侧走 wc-l 档)
|
|
806
|
+
const st = safeGit(wtRoot, ['status', '--porcelain', '--untracked-files=all'], { timeout: 30 * 1000, trim: false })
|
|
807
|
+
if (!st.error && typeof st.value === 'string') {
|
|
808
|
+
for (const line of st.value.split('\n')) {
|
|
809
|
+
if (!line.startsWith('??')) continue
|
|
810
|
+
const p = toPosix(line.slice(3).trim().replace(/^"|"$/g, '').split(' -> ').pop() || '')
|
|
811
|
+
if (wtFilter(p) && !files.includes(p)) files.push(p)
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
actual = { ok: true, form: 'worktree', files, sources: ['worktree:diff-anchor..worktree', 'worktree:status-untracked'], foreignExcluded: 0, degradedReason: null, baseAnchor: anchor }
|
|
815
|
+
} else {
|
|
816
|
+
actual = { ok: false, form: 'worktree', files: [], sources: [], foreignExcluded: 0, degradedReason: 'worktree 锚定 diff 失败', baseAnchor: null }
|
|
817
|
+
}
|
|
818
|
+
} else {
|
|
819
|
+
// 无锚 meta(baseHash 等字段缺失):退 HEAD 未提交窗口口径——文件集=status 全量
|
|
820
|
+
// (tracked 改动 + untracked),行数侧由主链路 HEAD 兜底(quick-8aa52289 契约不变)
|
|
821
|
+
const st = safeGit(wtRoot, ['status', '--porcelain', '--untracked-files=all'], { timeout: 30 * 1000, trim: false })
|
|
822
|
+
const files = []
|
|
823
|
+
if (!st.error && typeof st.value === 'string') {
|
|
824
|
+
for (const line of st.value.split('\n')) {
|
|
825
|
+
const p = toPosix(line.slice(3).trim().replace(/^"|"$/g, '').split(' -> ').pop() || '')
|
|
826
|
+
if (p && p !== '.sillyspec'
|
|
827
|
+
&& !p.startsWith('.sillyspec/changes/')
|
|
828
|
+
&& !p.startsWith('.sillyspec/.runtime/')
|
|
829
|
+
&& !p.startsWith('.sillyspec/quicklog/')) files.push(p)
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
actual = { ok: true, form: 'worktree', files, sources: ['worktree:status-porcelain(HEAD-window)'], foreignExcluded: 0, degradedReason: null, baseAnchor: null }
|
|
833
|
+
}
|
|
834
|
+
} else {
|
|
835
|
+
const mod = await import('./verify-postcheck.js')
|
|
836
|
+
if (typeof mod.resolveReconcileActualFiles !== 'function') {
|
|
837
|
+
throw new Error('resolveReconcileActualFiles 未导出(依赖契约未就绪)')
|
|
838
|
+
}
|
|
839
|
+
actual = mod.resolveReconcileActualFiles({ cwd, specBase: sb, runtimeRoot, changeName })
|
|
573
840
|
}
|
|
574
|
-
actual = mod.resolveReconcileActualFiles({ cwd, specBase: sb, runtimeRoot, changeName })
|
|
575
841
|
} catch (e) {
|
|
576
842
|
actualFailure = e && e.message ? String(e.message).split('\n')[0] : String(e)
|
|
577
843
|
}
|
|
@@ -596,6 +862,29 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
596
862
|
return { ...base, degradedReason: [reason, planDegraded].filter(Boolean).join(';') }
|
|
597
863
|
}
|
|
598
864
|
|
|
865
|
+
// —— execute --done 采集·apply-pathspec 归属收窄(quick-ec5fc714):apply 时点在 .runtime
|
|
866
|
+
// 落过本变更精确文件清单(worktree-apply.js 产 apply-pathspec-<change>.txt)——execute 采集
|
|
867
|
+
// 形态(freshActual+collectPatch,仅 printExecuteScopeAudit 组合)下按它收窄 actual.files,
|
|
868
|
+
// 快照/patch 冻**归属面**而非开放区间(平台仓实测:186KB 快照中 23/44 段为并行会话 docs
|
|
869
|
+
// 同步等噪音)。查询面不收窄(快照说什么是什么);pathspec 缺失退开放区间(in-place 等)。
|
|
870
|
+
let narrowNote = null
|
|
871
|
+
if (freshActual && collectPatch) {
|
|
872
|
+
try {
|
|
873
|
+
const pathspecFile = join(runtimeRoot, `apply-pathspec-${changeName}.txt`)
|
|
874
|
+
const owned = readFileSync(pathspecFile, 'utf8').split('\n').map(l => toPosix(l.trim())).filter(Boolean)
|
|
875
|
+
if (owned.length > 0) {
|
|
876
|
+
const ownedSet = new Set(owned)
|
|
877
|
+
const before = actual.files.length
|
|
878
|
+
actual = { ...actual, files: actual.files.filter(f => ownedSet.has(toPosix(f))) }
|
|
879
|
+
// 收窄掉的并行噪音可见性保留(审计不静默挤走)
|
|
880
|
+
const dropped = before - actual.files.length
|
|
881
|
+
if (dropped > 0) {
|
|
882
|
+
narrowNote = `快照已按 apply-pathspec 收窄至本变更归属文件(剔除开放区间并行文件 ${dropped} 个——它们归属各自会话/变更)`
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
} catch { /* pathspec 缺失 → 开放区间(旧行为) */ }
|
|
886
|
+
}
|
|
887
|
+
|
|
599
888
|
// —— 已收尾变更·冻结快照优先(真实 > 冻结记录 > 实时开放区间):归档变更的范围应封闭在
|
|
600
889
|
// apply 时点,不随后续主仓改动漂移(quick-f5acdeeb);quick-bd84b852 放宽——**post-apply
|
|
601
890
|
// (分支已删 = execute 已 apply 收尾)的活跃变更同样收尾**,此后实时区间只会被并行演进泡大
|
|
@@ -669,6 +958,7 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
669
958
|
const degraded = []
|
|
670
959
|
if (planDegraded) degraded.push(planDegraded)
|
|
671
960
|
const notes = []
|
|
961
|
+
if (narrowNote) notes.push(narrowNote)
|
|
672
962
|
// 已收尾但快照缺失(上方分支置位):实时开放区间兜底,明示漂移语义——快照机制上线
|
|
673
963
|
// (2026-09-10)前收尾的旧变更冻结记录无法重建,如实告知替代渠道。
|
|
674
964
|
if (noSnapshotDrift) {
|
|
@@ -766,7 +1056,12 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
766
1056
|
* kind: 'binary'|'new'|'modified'|'deleted',
|
|
767
1057
|
* verdict?: 'planned'|'unplanned'|'untouched', attribution?: 'declared'|'soft'|'undeclared' }>,
|
|
768
1058
|
* excluded: { foreignDeclared: Array<{ file: string, sessions: string[] }> },
|
|
769
|
-
* note?: string|null
|
|
1059
|
+
* note?: string|null,
|
|
1060
|
+
* gateProfile?: object|null }>}
|
|
1061
|
+
* gateProfile(2026-09-14-quick-exit-tiered-gates task-03 增量字段,消费方按存在性读取):
|
|
1062
|
+
* 仅 quick 模式携带——实时态透传 auditQuickCompletion 挂的 review.gateProfile(不重复计算);
|
|
1063
|
+
* 冻结重放态透传 quicklog/patches 记录冻结值(旧记录无 → rows 文件清单现算重放)。full-flow
|
|
1064
|
+
* 全流程变更恒无该字段(零回归契约)。
|
|
770
1065
|
*/
|
|
771
1066
|
export async function computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts, collectPatch, freshActual } = {}) {
|
|
772
1067
|
const empty = {
|
|
@@ -778,6 +1073,21 @@ export async function computeChangeScopeAudit({ cwd, specBase, changeName, platf
|
|
|
778
1073
|
if (!cwd || !changeName) {
|
|
779
1074
|
return { ...empty, degradedReason: '参数缺失:cwd 与 changeName 必填' }
|
|
780
1075
|
}
|
|
1076
|
+
// ql-xxx 反查(「为什么不直接用 ql-xxx 查」根治):patches 记录按 qlId 命名且 json 冗余
|
|
1077
|
+
// sessionId——ql-xxx → quick-xxx 映射在 patches 里持久可查(guard 已清也能出记录态)。
|
|
1078
|
+
// 提升到 quick 判定外层:ql-xxx 不匹配 QUICK_SID_RE,在内部分支永远进不去。
|
|
1079
|
+
if (QL_ID_RE.test(changeName)) {
|
|
1080
|
+
const byQl = findQuickSessionByQlId(cwd, changeName)
|
|
1081
|
+
if (byQl) {
|
|
1082
|
+
const rec = { record: byQl.record, patchPath: byQl.patchPath, qlId: byQl.qlId, specBase: byQl.specBase }
|
|
1083
|
+
return buildQuickRecordResult(rec, changeName)
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
...empty,
|
|
1087
|
+
mode: 'quick',
|
|
1088
|
+
degradedReason: `quick 条目 ${changeName} 在 quicklog/patches/ 无记录——会话未产生改动、早于记录机制,或 patches 目录已被清理`,
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
781
1091
|
// quick 模式判定(pattern + guard 双条件,与 run/command.js :140/:652 口径一致)
|
|
782
1092
|
if (QUICK_SID_RE.test(changeName)) {
|
|
783
1093
|
const located = locateQuickSessionGuard(cwd, changeName)
|
|
@@ -788,21 +1098,7 @@ export async function computeChangeScopeAudit({ cwd, specBase, changeName, platf
|
|
|
788
1098
|
// 命中出冻结记录(行数为 --done 时点真值),patch 可供 --file 切片
|
|
789
1099
|
const rec = findQuickPatchRecord(cwd, changeName)
|
|
790
1100
|
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
|
-
}
|
|
1101
|
+
return buildQuickRecordResult(rec, changeName)
|
|
806
1102
|
}
|
|
807
1103
|
return {
|
|
808
1104
|
...empty,
|
|
@@ -936,6 +1232,8 @@ function fmtCount(n) {
|
|
|
936
1232
|
*
|
|
937
1233
|
* 防御式:result 任意字段缺失/畸形不抛(advisory 展示层)。opts.maxRows 截断 + 指引行
|
|
938
1234
|
* (R-03:大表注入场景由调用方传 60,命令直跑缺省全表)。
|
|
1235
|
+
* [gate] 画像段(task-03,gate-table-section):result.gateProfile 存在时(quick 模式)追加
|
|
1236
|
+
* 级别/跨度/模块/风险命中/缺失检查项段;字段缺失/为 null(full-flow / 重放不可得)→ 零输出零回归。
|
|
939
1237
|
*
|
|
940
1238
|
* @param {object} result computeChangeScopeAudit 返回值
|
|
941
1239
|
* @param {{ maxRows?: number }} [opts] 可选截断(缺省不截)
|
|
@@ -1001,6 +1299,38 @@ export function renderScopeAuditTable(result, opts = {}) {
|
|
|
1001
1299
|
const undeclared = rows.filter(x => x && x.attribution === 'undeclared').length
|
|
1002
1300
|
if (undeclared > 0) lines.push(` ⚠️ 未声明 ${undeclared} 文件——超出 allowedFiles 声明面,补 --files 声明或注明原因`)
|
|
1003
1301
|
|
|
1302
|
+
// ── [gate] 画像段(2026-09-14-quick-exit-tiered-gates task-03,FR-04 / D-008 gate-table-section):
|
|
1303
|
+
// quick 会话分级门禁画像的表格出口。按 gateProfile 存在性渲染——full-flow 变更 / 画像为 null
|
|
1304
|
+
// → 零输出零回归;检查项随级别分层(L1:每文件注记+测试增量;L2:模块文档认领+风险命中),
|
|
1305
|
+
// 与 run/quick-audit.js [gate] 打印块同一 review.gateProfile 数据源、同 tier 语义(advisory 展示,
|
|
1306
|
+
// 不构成门禁)。防御式:字段缺失/畸形不抛(degraded 无 span、modules 空集等均有占位)。
|
|
1307
|
+
const gate = r.gateProfile && typeof r.gateProfile === 'object' ? r.gateProfile : null
|
|
1308
|
+
if (gate) {
|
|
1309
|
+
const gateModules = Array.isArray(gate.modules) ? gate.modules.filter(m => m && typeof m === 'object') : []
|
|
1310
|
+
const spanTxt = gate.degraded
|
|
1311
|
+
? '无 module-map(降级档:span 不参与判级)'
|
|
1312
|
+
: `跨 ${gate.moduleSpan ?? '—'} 模块${gateModules.length > 0 ? `(${gateModules.map(m => String(m.id)).join(' · ')})` : ''}`
|
|
1313
|
+
lines.push(` 🚦 [gate] ${gate.level ?? '—'} 规模门(${spanTxt} · ${gate.fileCount ?? '—'} 文件:${gate.codeFileCount ?? '—'} 代码 / ${gate.testFileCount ?? '—'} 测试)——advisory 画像不阻断`)
|
|
1314
|
+
if (Array.isArray(gate.unmappedFiles) && gate.unmappedFiles.length > 0) {
|
|
1315
|
+
lines.push(` ⚠️ ${gate.unmappedFiles.length} 个文件未命中 module-map(${gate.unmappedFiles.slice(0, 5).join(', ')}${gate.unmappedFiles.length > 5 ? ' 等' : ''})`)
|
|
1316
|
+
}
|
|
1317
|
+
const checks = gate.checks && typeof gate.checks === 'object' ? gate.checks : {}
|
|
1318
|
+
if (gate.level === 'L1') {
|
|
1319
|
+
if (checks.perFileNotes === false) lines.push(' - 每文件注记缺失——补 --file-notes "path1::注 || path2::注"(覆盖变更文件全集)')
|
|
1320
|
+
if (checks.testDelta === 'missing') lines.push(` - 测试增量缺失——${gate.codeFileCount} 个代码文件改动无测试文件`)
|
|
1321
|
+
} else if (gate.level === 'L2') {
|
|
1322
|
+
if (checks.docClaim === 'missing') lines.push(' - 模块文档认领缺失——触及模块的卡片文件不在改动集(补模块卡/changelog 或 --no-docs 豁免)')
|
|
1323
|
+
if (checks.docClaim === 'exempt-no-docs') lines.push(' - 模块文档认领:--no-docs 显式豁免')
|
|
1324
|
+
}
|
|
1325
|
+
if (Array.isArray(gate.riskHits) && gate.riskHits.length > 0) {
|
|
1326
|
+
lines.push(` - 风险命中 ${gate.riskHits.length} 处(运行时证据要求——风险路径改动需附运行验证说明):`)
|
|
1327
|
+
for (const h of gate.riskHits.slice(0, 5)) {
|
|
1328
|
+
if (h && typeof h === 'object') lines.push(` · [${h.pattern}] ${h.file}`)
|
|
1329
|
+
}
|
|
1330
|
+
if (gate.riskHits.length > 5) lines.push(` … 共 ${gate.riskHits.length} 处`)
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1004
1334
|
// 他者会话声明排除面(R-04:不进 rows,单列可见)
|
|
1005
1335
|
if (foreignDeclared.length > 0) {
|
|
1006
1336
|
lines.push(` 他者会话声明(不进上表,归属对应会话审计):`)
|