sillyspec 3.32.0 → 3.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/flow-parity.js +137 -1
- package/src/flow.js +60 -23
- package/src/foreign-declared.js +7 -3
- package/src/knowledge-stats.js +39 -0
- package/src/run/complete.js +22 -11
- package/src/scope-audit.js +129 -3
- package/src/verify-postcheck.js +11 -3
package/package.json
CHANGED
package/src/flow-parity.js
CHANGED
|
@@ -10,15 +10,21 @@
|
|
|
10
10
|
* 厚道有轻量变更缺的审计资产;机器合成勿手改。
|
|
11
11
|
* ③ harvestSlot4Decision:design 槽4(风险与死路)实质作答收割合成 decisions.md——轻量变更决策
|
|
12
12
|
* 产出为零的补口(死路与风险取舍正是 decisions.md 该记的内容;已有文件不覆盖)。
|
|
13
|
+
* ④ writeCloseTraceArtifacts / buildThinSnapshotRows:双通道收尾留痕统一写
|
|
14
|
+
* (2026-10-07-unify-close-trace)——thin flow done 与 heavy execute --done 共用一处一次
|
|
15
|
+
* 写齐四件(沉淀资产面 + 对账快照面,sha256 双套同锚),新变更两通道留痕对称。
|
|
13
16
|
*/
|
|
14
17
|
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs'
|
|
15
18
|
import { join, relative } from 'node:path'
|
|
19
|
+
import { createHash } from 'node:crypto'
|
|
16
20
|
import { execFileSync } from 'node:child_process'
|
|
17
21
|
import { splitOwnVsForeignDiffFiles, commitAttributionForChange, isForeignByCommit, parseChangeNamesFromSubject } from './foreign-declared.js'
|
|
18
22
|
import { collectModuleMaps, normalizeMapPath } from './module-resolve.js'
|
|
19
23
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
20
24
|
import { readV2SectionAnswer } from './flow-review.js'
|
|
21
25
|
import { DESIGN_QUESTIONS } from './flow-draft.js'
|
|
26
|
+
import { pathMatches } from './change-list.js'
|
|
27
|
+
import { classifyToolScaffold, filterDeliverableFiles } from './worktree-apply.js'
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
30
|
* 模块文档对账(advisory)+ 结构化落盘面(2026-09-27-thin-module-scope-persist)。
|
|
@@ -252,7 +258,11 @@ export function collectFreezeFiles({ cwd, specBase, change, committed, exclusive
|
|
|
252
258
|
const deliverable = (f) => !String(f).replace(/\\/g, '/').startsWith('.sillyspec/')
|
|
253
259
|
let dirty = []
|
|
254
260
|
try {
|
|
255
|
-
|
|
261
|
+
// -uall(2026-10-08-thin-done-dirty-gate-and-paren-attribution):全新目录会折叠成
|
|
262
|
+
// `dir/` token(porcelain 缺省形态)——readFileSync(目录) 静默 skip,--freeze-dirty
|
|
263
|
+
// 声称并入却漏目录内交付文件(dg-freeze 夹具实证 src/ 折叠)。逐文件展开与
|
|
264
|
+
// scope-audit/flow.js 状态采集口径对齐。
|
|
265
|
+
const out = execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], { cwd, encoding: 'utf8', timeout: 30000, windowsHide: true })
|
|
256
266
|
for (const line of String(out).split('\n')) {
|
|
257
267
|
if (!line || line.length < 4) continue
|
|
258
268
|
const p = line.slice(3).trim().replace(/^"|"$/g, '')
|
|
@@ -272,6 +282,132 @@ export function collectFreezeFiles({ cwd, specBase, change, committed, exclusive
|
|
|
272
282
|
return { files: [...committed], dirtyAdded: [], dirtyWarned: ownDirty }
|
|
273
283
|
}
|
|
274
284
|
|
|
285
|
+
/**
|
|
286
|
+
* thin 通道三态行构造(writeCloseTraceArtifacts 的 snapObj.rows 数据源,纯函数):
|
|
287
|
+
* ownFiles 过 filterDeliverableFiles(与 scope-audit actual 侧同口径——治理工件目录不进
|
|
288
|
+
* 快照行)× design 清单 pathMatches 判三态(planned/unplanned;与主链路三态判定同款);
|
|
289
|
+
* stats(collectNumstatByPath 产物 Map)缺档落 null 行数(不出伪数据);planEntries 空/缺省
|
|
290
|
+
* → 实际侧 only 行(无 verdict,与主链路计划侧降级同语义)。清单条目未命中冻结面 →
|
|
291
|
+
* untouched 0/0 补行(跨仓条目带 crossRepo 保持 ⊘ 形态,主链路补行同语义)。
|
|
292
|
+
* @returns {{ rows: Array<object>, totals: { files: number, additions: number, deletions: number } }}
|
|
293
|
+
*/
|
|
294
|
+
export function buildThinSnapshotRows({ ownFiles, stats, planEntries }) {
|
|
295
|
+
const plan = Array.isArray(planEntries) ? planEntries : []
|
|
296
|
+
const statMap = stats instanceof Map ? stats : new Map()
|
|
297
|
+
const rowFiles = filterDeliverableFiles([...new Set((Array.isArray(ownFiles) ? ownFiles : [])
|
|
298
|
+
.map((f) => String(f).replace(/\\/g, '/')).filter(Boolean))]).sort()
|
|
299
|
+
const rows = []
|
|
300
|
+
const matched = new Set()
|
|
301
|
+
for (const f of rowFiles) {
|
|
302
|
+
const st = statMap.get(f) || {}
|
|
303
|
+
const row = {
|
|
304
|
+
path: f,
|
|
305
|
+
additions: Number.isFinite(st.additions) ? st.additions : null,
|
|
306
|
+
deletions: Number.isFinite(st.deletions) ? st.deletions : null,
|
|
307
|
+
kind: st.kind || 'modified',
|
|
308
|
+
}
|
|
309
|
+
if (plan.length > 0) {
|
|
310
|
+
const entry = plan.find((e) => e && pathMatches(f, e.path))
|
|
311
|
+
if (entry) {
|
|
312
|
+
matched.add(entry.path)
|
|
313
|
+
row.planned = entry.operation || null
|
|
314
|
+
row.verdict = 'planned'
|
|
315
|
+
} else {
|
|
316
|
+
row.planned = null
|
|
317
|
+
row.verdict = 'unplanned'
|
|
318
|
+
const facility = classifyToolScaffold(f)
|
|
319
|
+
if (facility) row.facility = facility
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
rows.push(row)
|
|
323
|
+
}
|
|
324
|
+
for (const e of plan) {
|
|
325
|
+
if (!e || matched.has(e.path)) continue
|
|
326
|
+
rows.push({
|
|
327
|
+
path: e.path, planned: e.operation || null, additions: 0, deletions: 0,
|
|
328
|
+
kind: 'modified', verdict: 'untouched',
|
|
329
|
+
...(e.repo ? { crossRepo: e.repo } : {}),
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
let additions = 0
|
|
333
|
+
let deletions = 0
|
|
334
|
+
for (const r of rows) {
|
|
335
|
+
if (Number.isFinite(r.additions)) additions += r.additions
|
|
336
|
+
if (Number.isFinite(r.deletions)) deletions += r.deletions
|
|
337
|
+
}
|
|
338
|
+
return { rows, totals: { files: rows.length, additions, deletions } }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* heavy 通道沉淀资产面投影(writeCloseTraceArtifacts 的 files/totals 数据源,纯函数;
|
|
343
|
+
* 2026-10-07-unify-close-trace,评审 P3 覆盖清偿抽纯函数):rows → 主仓实改行——
|
|
344
|
+
* untouched 0/0 是声明不是改动、crossRepo 行 patch 不在主仓,均不入;verdict 缺失行
|
|
345
|
+
* (计划侧降级形态)按实改保留;null 行数不计合计(不出伪数据)。
|
|
346
|
+
* @returns {{ rows: object[], files: string[], totals: { files: number, additions: number, deletions: number } }}
|
|
347
|
+
*/
|
|
348
|
+
export function projectTraceFaceRows(rows) {
|
|
349
|
+
const face = (Array.isArray(rows) ? rows : [])
|
|
350
|
+
.filter((r) => r && typeof r === 'object' && !r.crossRepo && r.verdict !== 'untouched')
|
|
351
|
+
let additions = 0
|
|
352
|
+
let deletions = 0
|
|
353
|
+
for (const r of face) {
|
|
354
|
+
if (Number.isFinite(r.additions)) additions += r.additions
|
|
355
|
+
if (Number.isFinite(r.deletions)) deletions += r.deletions
|
|
356
|
+
}
|
|
357
|
+
return { rows: face, files: face.map((r) => r.path), totals: { files: face.length, additions, deletions } }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 双通道收尾留痕统一写(2026-10-07-unify-close-trace,缺陷 thin-flow-done-no-scope-
|
|
362
|
+
* audit-snapshot 修复方向③):thin(flow done)与 heavy(execute --done)此前各写各的
|
|
363
|
+
* 留痕(沉淀资产面 change.patch/change-patch.json vs 对账快照面 scope-audit.patch/
|
|
364
|
+
* scope-audit.json),消费方各认一份致每类变更恰好一张卡失真——本函数一处写齐四件,
|
|
365
|
+
* sha256/patchStatus 双套同锚(同一 patchText 一次计算:change.patch ≡ scope-audit.patch
|
|
366
|
+
* 字节一致、两 json 同 hash,两套留痕不可能漂移)。patchText 空/null → 双套同标 failed、
|
|
367
|
+
* 不落 patch 文件(heavy 旧「空串当 ok」形态收口)。读侧兼容链(快照 > change-patch
|
|
368
|
+
* 回放 > 实时区间,2026-10-07-scope-audit-thin-patch-replay)不动——本函数让新变更两套
|
|
369
|
+
* 齐备,回退退化为存量归档兜底。
|
|
370
|
+
*
|
|
371
|
+
* @param {string} changeDir 变更目录(活跃或归档侧——归档竞态由调用方解析)
|
|
372
|
+
* @param {object} snapObj scope-audit.json 主体(mode/ok/degradedReason/baseAnchor/totals/
|
|
373
|
+
* rows/excluded/note[/repos];closedBy 由调用方携带标收尾通道——读侧回放按它标注,
|
|
374
|
+
* 缺省 'execute --done' 兼容旧快照)
|
|
375
|
+
* @param {object} meta change-patch.json 增量键(note/moduleScope 等,展开在 totals 后)
|
|
376
|
+
* @returns {{ patchStatus: 'ok'|'failed', patchSha256: string|null }}
|
|
377
|
+
*/
|
|
378
|
+
export function writeCloseTraceArtifacts({ changeDir, change, baseline, head, files, metaTotals, patchText, savedAt, snapObj, meta = {} }) {
|
|
379
|
+
const fileList = Array.isArray(files) ? files : []
|
|
380
|
+
const patchOk = typeof patchText === 'string' && patchText.length > 0
|
|
381
|
+
const patchBody = patchOk ? (patchText.endsWith('\n') ? patchText : patchText + '\n') : null
|
|
382
|
+
const patchSha256 = patchBody
|
|
383
|
+
? createHash('sha256').update(patchBody.replace(/\r\n/g, '\n'), 'utf8').digest('hex')
|
|
384
|
+
: null
|
|
385
|
+
if (patchOk) {
|
|
386
|
+
writeFileSync(join(changeDir, 'change.patch'), patchBody)
|
|
387
|
+
writeFileSync(join(changeDir, 'scope-audit.patch'), patchBody)
|
|
388
|
+
}
|
|
389
|
+
writeFileSync(join(changeDir, 'change-patch.json'), JSON.stringify({
|
|
390
|
+
change,
|
|
391
|
+
baseline: baseline ?? null,
|
|
392
|
+
head: head ?? null,
|
|
393
|
+
files: fileList,
|
|
394
|
+
totals: metaTotals && typeof metaTotals === 'object'
|
|
395
|
+
? metaTotals
|
|
396
|
+
: { files: fileList.length, additions: 0, deletions: 0 },
|
|
397
|
+
savedAt,
|
|
398
|
+
...meta,
|
|
399
|
+
...(patchSha256 ? { patchSha256 } : {}),
|
|
400
|
+
patchStatus: patchOk ? 'ok' : 'failed',
|
|
401
|
+
}, null, 2) + '\n')
|
|
402
|
+
writeFileSync(join(changeDir, 'scope-audit.json'), JSON.stringify({
|
|
403
|
+
...snapObj,
|
|
404
|
+
...(patchSha256 ? { patchSha256 } : {}),
|
|
405
|
+
patchStatus: patchOk ? 'ok' : 'failed',
|
|
406
|
+
savedAt,
|
|
407
|
+
}, null, 2) + '\n')
|
|
408
|
+
return { patchStatus: patchOk ? 'ok' : 'failed', patchSha256 }
|
|
409
|
+
}
|
|
410
|
+
|
|
275
411
|
/**
|
|
276
412
|
* 实测面回填(修复③:ledger 子步断点续跑 skip 后回执不失忆)——从 verify-runs 最近
|
|
277
413
|
* test-result.json(带 change 键)重建摘要;无匹配返回 null。
|
package/src/flow.js
CHANGED
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
* 幂等循 task-done 先例:子步各查自身完成标记,中断半态重入断点续,中段失败精确报告。
|
|
27
27
|
*/
|
|
28
28
|
import { existsSync, readFileSync, writeFileSync, readdirSync, appendFileSync, renameSync, statSync } from 'node:fs'
|
|
29
|
-
import { createHash } from 'node:crypto'
|
|
30
29
|
import { join, relative, dirname } from 'node:path'
|
|
31
30
|
import yaml from 'js-yaml'
|
|
32
31
|
import { git, gitQuiet } from './git-helper.js'
|
|
@@ -863,7 +862,7 @@ export function remainingSubstepsAtFail({ snapshotSubsteps = {}, doneList = [],
|
|
|
863
862
|
return SUBSTEPS.filter((k) => k !== failed && snapshotSubsteps?.[k] !== 'done' && !doneNow.has(k))
|
|
864
863
|
}
|
|
865
864
|
|
|
866
|
-
export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null, confirmArchive = true, freezeDirty = false, allowBatchTick = false }) {
|
|
865
|
+
export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null, confirmArchive = true, freezeDirty = false, acceptDirtyGap = false, allowBatchTick = false }) {
|
|
867
866
|
const changeDir = join(specBase, 'changes', change)
|
|
868
867
|
const st = readFlowState(changeDir)
|
|
869
868
|
if (!st) {
|
|
@@ -1396,11 +1395,30 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1396
1395
|
}
|
|
1397
1396
|
if (freeze.dirtyWarned.length > 0) {
|
|
1398
1397
|
console.warn(`⚠️ ${freeze.dirtyWarned.length} 个未提交交付文件未入冻结面(共享主仓无法归属)——三选一:`)
|
|
1399
|
-
console.warn(` ①
|
|
1398
|
+
console.warn(` ① 先提交这些文件(信息尾缀带变更名)后重跑 ② 确认全部归属本变更:重跑 flow done --freeze-dirty 并入冻结 ③ 确认接受审计缺口:重跑 flow done --accept-dirty-gap(缺口数随 change-patch.json 留痕)。Git 中间提交可归档后压扁(见收口指引)`)
|
|
1400
1399
|
console.warn(` ${freeze.dirtyWarned.slice(0, 3).join('、')}${freeze.dirtyWarned.length > 3 ? ' 等' : ''}`)
|
|
1400
|
+
// 收口阻断门(2026-10-08-thin-done-dirty-gate-and-paren-attribution,坑①修复:
|
|
1401
|
+
// multi-agent-platform docs/sillyspec/thin-done-src-commit-order-and-attribution-paren.md)
|
|
1402
|
+
// ——旧行为警告后继续归档,autopilot 单跑到底把三选一变成不可达选项(归档即拒收重入)。
|
|
1403
|
+
// 缺口在场且未显式处置 → 停在 patch 子步标记之前(半态可重入,与 review 中断同款
|
|
1404
|
+
// exit 语义):处置后重跑,patch 全量重建自动并入已提交 src。
|
|
1405
|
+
if (!freezeDirty && !acceptDirtyGap) {
|
|
1406
|
+
console.error('⛔ 未提交交付缺口未显式处置——收口阻断(不归档;处置后重跑同一条命令从断点续):')
|
|
1407
|
+
console.error(` ① git commit 这些文件(信息尾缀带 (${change}) 或 (${change}),重跑本命令——冻结面自动并入`)
|
|
1408
|
+
console.error(` ② 全部归属本变更:flow done --change ${change} --freeze-dirty`)
|
|
1409
|
+
console.error(` ③ 接受审计缺口:flow done --change ${change} --accept-dirty-gap`)
|
|
1410
|
+
reportMidFail('patch')
|
|
1411
|
+
process.exit(1)
|
|
1412
|
+
}
|
|
1413
|
+
if (acceptDirtyGap) {
|
|
1414
|
+
// 双 flag 同给时 --freeze-dirty 优先(exclusive 路径 dirtyWarned 恒空,本分支不进
|
|
1415
|
+
// ——设计接口契约已披露;无告警代码即无死代码,二轮评审 P3 清偿)
|
|
1416
|
+
console.warn(`⚠️ --accept-dirty-gap:显式接受 ${freeze.dirtyWarned.length} 个未提交交付文件不进冻结面(缺口数随 change-patch.json acceptedDirtyGap 留痕)`)
|
|
1417
|
+
}
|
|
1401
1418
|
}
|
|
1402
1419
|
const ownFiles = [...new Set([...freeze.files, ...changeDirFiles])]
|
|
1403
|
-
.filter((f) => f && !f.endsWith('change.patch') && !f.endsWith('change-patch.json')
|
|
1420
|
+
.filter((f) => f && !f.endsWith('change.patch') && !f.endsWith('change-patch.json')
|
|
1421
|
+
&& !f.endsWith('scope-audit.json') && !f.endsWith('scope-audit.patch'))
|
|
1404
1422
|
if (ownFiles.length === 0) {
|
|
1405
1423
|
console.log('📦 patch 留档跳过:本变更可归属文件面为空')
|
|
1406
1424
|
patchOk = true
|
|
@@ -1416,7 +1434,8 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1416
1434
|
// diff 能捕捉提交后编辑)∪ 独占树未提交交付(dirtyAdded);其余交付面走提交区间。
|
|
1417
1435
|
// 排除项与 ownFiles 同款(change.patch/change-patch.json)——否则上一轮冻结件作为
|
|
1418
1436
|
// untracked 新文件被全文自嵌入(自引用循环:旧 patch 内容混进新 patch,三评 P1 根因)。
|
|
1419
|
-
const dirFilesForPatch = changeDirFiles.filter((f) => !f.endsWith('change.patch') && !f.endsWith('change-patch.json')
|
|
1437
|
+
const dirFilesForPatch = changeDirFiles.filter((f) => !f.endsWith('change.patch') && !f.endsWith('change-patch.json')
|
|
1438
|
+
&& !f.endsWith('scope-audit.json') && !f.endsWith('scope-audit.patch'))
|
|
1420
1439
|
const worktreeSet = new Set([...dirFilesForPatch, ...((freeze.dirtyAdded) || [])].map(toPosix))
|
|
1421
1440
|
const committedFace = freeze.files.map(toPosix).filter((f) => !worktreeSet.has(f))
|
|
1422
1441
|
const frozenCommitted = buildFrozenPatch(cwd, committedFace, { baseRef: st.baseline_commit, headRef: headCommit })
|
|
@@ -1445,28 +1464,46 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1445
1464
|
if (rec.lines.length > 0) for (const l of rec.lines) console.log(l)
|
|
1446
1465
|
moduleScope = { modules: rec.modules, uncoveredDirs: rec.uncoveredDirs, moduleMaps: rec.moduleMaps }
|
|
1447
1466
|
} catch { /* 对账 best-effort(三键空数组兜底) */ }
|
|
1448
|
-
|
|
1467
|
+
// 双通道收尾留痕统一(2026-10-07-unify-close-trace):共用 writeCloseTraceArtifacts
|
|
1468
|
+
// 一次写齐四件——沉淀资产面(change.patch + change-patch.json,键结构/口径不变)+
|
|
1469
|
+
// 对账快照面(scope-audit.json + scope-audit.patch,thin 通道新增):同点同锚
|
|
1470
|
+
// (同一 patchText 一次 sha256),新变更两条通道留痕对称,读侧回退退化为存量兜底。
|
|
1471
|
+
let planEntries = []
|
|
1472
|
+
try {
|
|
1473
|
+
const { parseFileChangeListDetailed } = await import('./change-list.js')
|
|
1474
|
+
planEntries = parseFileChangeListDetailed(join(changeDir, 'design.md'), { keepSillyspecDocs: true })
|
|
1475
|
+
} catch { /* 清单缺失/不可解析 → 实际侧 only 快照行(无 verdict,与主链路降级同语义) */ }
|
|
1476
|
+
const { buildThinSnapshotRows, writeCloseTraceArtifacts } = await import('./flow-parity.js')
|
|
1477
|
+
const { rows: snapRows, totals: snapTotals } = buildThinSnapshotRows({ ownFiles, stats, planEntries })
|
|
1478
|
+
const closeTrace = writeCloseTraceArtifacts({
|
|
1479
|
+
changeDir,
|
|
1449
1480
|
change,
|
|
1450
1481
|
baseline: st.baseline_commit,
|
|
1451
1482
|
head: typeof head === 'string' ? head.trim() : null,
|
|
1452
1483
|
files: ownFiles,
|
|
1453
|
-
|
|
1484
|
+
metaTotals: { files: ownFiles.length, additions, deletions },
|
|
1485
|
+
patchText: typeof frozen === 'string' && frozen ? frozen : null,
|
|
1454
1486
|
savedAt: new Date().toISOString(),
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
meta
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1487
|
+
snapObj: {
|
|
1488
|
+
mode: 'full-flow', ok: true, degradedReason: null,
|
|
1489
|
+
baseAnchor: st.baseline_commit || null,
|
|
1490
|
+
totals: snapTotals,
|
|
1491
|
+
rows: snapRows,
|
|
1492
|
+
excluded: { foreignDeclared: [] },
|
|
1493
|
+
note: 'flow done 时点冻结(本文件落盘时采集)',
|
|
1494
|
+
closedBy: 'flow done',
|
|
1495
|
+
},
|
|
1496
|
+
meta: {
|
|
1497
|
+
note: 'flow done 时点冻结(本变更可归属面:baseline..HEAD 提交面过滤 .sillyspec/ 非本变更目录但保留 .sillyspec/docs/ 交付文档 + 本变更目录工作树件;含未提交与 untracked,排除 flow-state.yaml 运行态)',
|
|
1498
|
+
// 模块对账结构化面(2026-09-27-thin-module-scope-persist):done 时点口径
|
|
1499
|
+
// (与 files[] 同时点语义,不随模块图后续变更回写);modules[] 空≠无影响
|
|
1500
|
+
...moduleScope,
|
|
1501
|
+
// dirty 缺口显式接受留痕(2026-10-08-thin-done-dirty-gate-and-paren-attribution):
|
|
1502
|
+
// --accept-dirty-gap 放行时缺口数入档(审计面可见,非只 console)
|
|
1503
|
+
...(acceptDirtyGap && freeze.dirtyWarned.length > 0 ? { acceptedDirtyGap: freeze.dirtyWarned.length } : {}),
|
|
1504
|
+
},
|
|
1505
|
+
})
|
|
1506
|
+
console.log(`📦 变更 patch 留档(双轨统一):change.patch + change-patch.json(${ownFiles.length} 文件,+${additions}/-${deletions})+ scope-audit.json + scope-audit.patch(对账快照同点冻结)${closeTrace.patchStatus === 'ok' ? ',sha256 已锚' : '——patch 采集失败已留痕'}`)
|
|
1470
1507
|
// design 声明面自证(2026-09-25-platform-feedback-batch2 E):design.md 文件变更清单
|
|
1471
1508
|
// 声明的交付文件是否都在冻结面——不在=承诺改了但没交付(承诺未兑现面,advisory 不
|
|
1472
1509
|
// 阻断——可能是范围裁剪了但 design 没同步更新)
|
|
@@ -2051,7 +2088,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
|
|
|
2051
2088
|
console.log('🔄 --refreeze:patch 子步标记已重置,本次 done 将重新冻结(change.patch 按 baseline..HEAD 最新面重建)')
|
|
2052
2089
|
}
|
|
2053
2090
|
}
|
|
2054
|
-
return cmdFlowDone({ change, cwd, specBase, runtimeRootOpt, freezeDirty: hasFlag('--freeze-dirty'), allowBatchTick: hasFlag('--allow-batch-tick') })
|
|
2091
|
+
return cmdFlowDone({ change, cwd, specBase, runtimeRootOpt, freezeDirty: hasFlag('--freeze-dirty'), acceptDirtyGap: hasFlag('--accept-dirty-gap'), allowBatchTick: hasFlag('--allow-batch-tick') })
|
|
2055
2092
|
}
|
|
2056
2093
|
if (sub === 'amend-draft') {
|
|
2057
2094
|
// 机器稿唯一留痕修改通道(R7 切片三 / FR-08):重锚哈希 + ledger amendment 审计;
|
package/src/foreign-declared.js
CHANGED
|
@@ -204,12 +204,16 @@ function loadOwnDeclaredSet(specBase, runtimeRoot, currentChangeName) {
|
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
206
|
* 提交标题 → 变更名数组(2026-10-05-diff-commit-attribution):项目提交惯例 message 尾部
|
|
207
|
-
*
|
|
208
|
-
*
|
|
207
|
+
* 携带变更名括号后缀(多变更并列各自提取;归档提交 chore(archive): <名> 的标题首部同名
|
|
208
|
+
* 形态不提取——归档提交触碰的是治理面移动,交付文件归属看交付提交)。
|
|
209
|
+
* 括号形态双收(2026-10-08-thin-done-dirty-gate-and-paren-attribution,坑
|
|
210
|
+
* thin-done-src-commit-order-and-attribution-paren:中文输入法惯例写全角
|
|
211
|
+
* `(thin <名>;…)`,旧正则只认半角 → drift 归因静默失灵):开/闭括号半全角任意组合,
|
|
212
|
+
* 括号内允许 `thin ` 前缀与名字后附注文本;变更名本体模式(日期前缀 + 白名单字符)不放宽。
|
|
209
213
|
*/
|
|
210
214
|
export function parseChangeNamesFromSubject(subject) {
|
|
211
215
|
const out = []
|
|
212
|
-
const re =
|
|
216
|
+
const re = /[((]\s*(?:thin\s+)?(\d{4}-\d{2}-\d{2}-[A-Za-z0-9._-]+)[^()()]*[))]/g
|
|
213
217
|
let m
|
|
214
218
|
while ((m = re.exec(String(subject || '')))) out.push(m[1])
|
|
215
219
|
return out
|
package/src/knowledge-stats.js
CHANGED
|
@@ -299,6 +299,45 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
299
299
|
const result = buildHitMatrix(knowledgeDir, runtimeDir, { sinceDays })
|
|
300
300
|
const frIndex = buildFrIndexStats(knowledgeDir, runtimeDir, { sinceDays })
|
|
301
301
|
|
|
302
|
+
// --fr-only(2026-10-08-knowledge-stats-fr-only):输出层过滤——仅保留 FR 索引段/键。
|
|
303
|
+
// 计算层保持全量(buildHitMatrix/buildFrIndexStats 签名与共用面零改动),flag 只影响渲染。
|
|
304
|
+
const frOnly = args.includes('--fr-only')
|
|
305
|
+
if (frOnly) {
|
|
306
|
+
if (asJson) {
|
|
307
|
+
// envelope 顶层键集与不带 --fr-only 时完全一致(outputJson 的外壳 schema_version/ok 不动),
|
|
308
|
+
// data 仅含 frIndex——跳过 matrix/conventions/neverHit/totalInjects/totalClassifies
|
|
309
|
+
outputJson(true, { sinceDays, hasTelemetry, lastEventAt, frIndex })
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
// 人类模式:仅渲染 FR 索引段(标题到承接引用率/裁决候选——不加遥测计数行和矩阵段)
|
|
313
|
+
const lines = []
|
|
314
|
+
if (frIndex.present) {
|
|
315
|
+
const e = frIndex.events
|
|
316
|
+
const i = frIndex.index
|
|
317
|
+
lines.push(`🔬 FR 索引实验(窗口内事件 / 索引面全量——L3 裁决指标,证伪条款见 2026-09-18-fr-index-l1 design)`)
|
|
318
|
+
lines.push(` 注入 fr-inject ${e.frInject} 次(${e.frInjectChanges} 变更) | 取代链 fr-supersede ${e.frSupersede} 次(${e.frSupersedeChanges} 变更) | 重复拦截 fr-duplicate-warning ${e.frDuplicateWarning} 次(${e.frDuplicateWarningCandidates || e.frDuplicateCandidates || 0} 候选)`)
|
|
319
|
+
lines.push(` 腐烂 suspect fr-rot-suspect ${e.frRotSuspect || 0} 次(${e.frRotSuspectChanges || 0} 变更${e.frRotSuspectByDomain && e.frRotSuspectByDomain.length > 0 ? `,按域 ${e.frRotSuspectByDomain.map(d => `${d.domain}×${d.count}`).join(',')}` : ''})`)
|
|
320
|
+
if (e.frUnreferenced.length > 0) {
|
|
321
|
+
const top = e.frUnreferenced.slice(0, 5).map((u) => `${u.domain}×${u.count}`).join(',')
|
|
322
|
+
lines.push(` 删除缺口信号 fr-unreferenced:${top}${e.frUnreferenced.length > 5 ? ' …' : ''}(观察信号·不算 L3 门禁)`)
|
|
323
|
+
}
|
|
324
|
+
lines.push(` 索引面:${i.entries} 条(active ${i.active} / superseded ${i.superseded})| 来源变更 ${i.sourceChanges} 个 | 域 ${i.domains.join(', ') || '—'}`)
|
|
325
|
+
lines.push(` 承接引用率:${frIndex.supersedeRate === null ? '—(索引空)' : `${(frIndex.supersedeRate * 100).toFixed(0)}%(${e.frSupersedeChanges}/${i.sourceChanges},分子窗口内/分母全量——裁决用大窗口)`}`)
|
|
326
|
+
const cand = frIndex.adjudicationCandidates || []
|
|
327
|
+
if (cand.length > 0) {
|
|
328
|
+
lines.push(' 裁决候选 Top-10(suspect×unref 条目级聚合——候选非裁决,确认无人承接→承接翻链或人工退休):')
|
|
329
|
+
for (const c of cand.slice(0, 10)) {
|
|
330
|
+
lines.push(` - ${c.id} suspect×${c.suspect} unref×${c.unreferenced}${c.change ? `(来源 ${c.change})` : ''}`)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
lines.push('(FR 索引面不存在——knowledge/fr/ 目录缺失或无条目;归档完整流程变更后此处可查)')
|
|
335
|
+
}
|
|
336
|
+
console.log(lines.join('\n'))
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
const asJsonBak = asJson // fr-only 已在上方 return 短路,此处原路径零改动
|
|
340
|
+
void asJsonBak
|
|
302
341
|
if (asJson) {
|
|
303
342
|
outputJson(true, { sinceDays, hasTelemetry, lastEventAt, ...result, frIndex })
|
|
304
343
|
return
|
package/src/run/complete.js
CHANGED
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { join, isAbsolute, resolve } from 'node:path'
|
|
20
20
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
|
|
21
|
-
import { createHash } from 'node:crypto'
|
|
22
21
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
23
22
|
import { gitQuiet } from '../git-helper.js'
|
|
23
|
+
import { writeCloseTraceArtifacts, projectTraceFaceRows } from '../flow-parity.js'
|
|
24
24
|
import { withFileLock } from '../quicklog.js'
|
|
25
25
|
import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir, writePlatformDocsPointer } from './shared.js'
|
|
26
26
|
import { isExplicitReviewWrite, collectWorktreeChangedFiles } from '../task-review.js'
|
|
@@ -1109,16 +1109,27 @@ async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts,
|
|
|
1109
1109
|
mkdirSync(changeDir, { recursive: true })
|
|
1110
1110
|
const { frozenPatch, ...snap } = result
|
|
1111
1111
|
snap.note = 'execute --done 时点冻结(本文件落盘时采集)'
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1112
|
+
snap.closedBy = 'execute --done'
|
|
1113
|
+
// 沉淀资产面投影(2026-10-07-unify-close-trace):files/totals = 主仓实改行(planned+
|
|
1114
|
+
// unplanned;untouched 0/0 是声明不是改动、crossRepo 行 patch 不在主仓——均不入)。
|
|
1115
|
+
// 经共用 writeCloseTraceArtifacts 与 thin flow done 同一处写四件(sha256 双套同锚);
|
|
1116
|
+
// 投影口径抽 flow-parity.projectTraceFaceRows 纯函数(排除路径有单测钉住)。
|
|
1117
|
+
const face = projectTraceFaceRows(snap.rows)
|
|
1118
|
+
const closeTrace = writeCloseTraceArtifacts({
|
|
1119
|
+
changeDir,
|
|
1120
|
+
change: changeName,
|
|
1121
|
+
baseline: typeof snap.baseAnchor === 'string' ? snap.baseAnchor : null,
|
|
1122
|
+
head: String(gitQuiet(cwd, ['rev-parse', 'HEAD']) || '').trim() || null,
|
|
1123
|
+
files: face.files,
|
|
1124
|
+
metaTotals: face.totals,
|
|
1125
|
+
patchText: typeof frozenPatch === 'string' && frozenPatch ? frozenPatch : null,
|
|
1126
|
+
savedAt: new Date().toISOString(),
|
|
1127
|
+
snapObj: snap,
|
|
1128
|
+
meta: {
|
|
1129
|
+
note: 'execute --done 时点冻结(沉淀资产面:主仓实改行投影——thin flow done 对等;治理工件目录与其后 verify/archive 演进不在内)',
|
|
1130
|
+
},
|
|
1131
|
+
})
|
|
1132
|
+
console.log(` 📦 范围快照 + 沉淀资产双轨落盘(scope-audit.json${closeTrace.patchStatus === 'ok' ? ' + scope-audit.patch' : ''} + change-patch.json${closeTrace.patchStatus === 'ok' ? ' + change.patch' : ''}${closeTrace.patchStatus === 'ok' ? '——收尾时点冻结,sha256 同锚' : '——patch 采集失败已留痕'})`)
|
|
1122
1133
|
} catch (e) {
|
|
1123
1134
|
console.warn(` ⚠️ 范围对账快照写入失败(不阻断,verify 漂移对比将按无快照降级):${e && e.message ? String(e.message).split('\n')[0] : e}`)
|
|
1124
1135
|
}
|
package/src/scope-audit.js
CHANGED
|
@@ -267,6 +267,72 @@ function readFrozenPatch(changeDir) {
|
|
|
267
267
|
} catch { return null }
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
/**
|
|
271
|
+
* flow done 冻结记录读取(2026-10-07-scope-audit-thin-patch-replay):change-patch.json 是
|
|
272
|
+
* thin 轻量变更 flow done 时点冻结的归属面(files/baseline/head/totals/patchSha256)——thin
|
|
273
|
+
* 通道不落 execute --done 快照,本件即其对账级冻结记录。缺 json / files 非数组 → null
|
|
274
|
+
* (调用方回退实时开放区间,不出伪数据)。
|
|
275
|
+
*/
|
|
276
|
+
function readChangePatchMeta(changeDir) {
|
|
277
|
+
if (!changeDir) return null
|
|
278
|
+
try {
|
|
279
|
+
const meta = JSON.parse(readFileSync(join(changeDir, 'change-patch.json'), 'utf8'))
|
|
280
|
+
if (meta && typeof meta === 'object' && Array.isArray(meta.files)) return meta
|
|
281
|
+
} catch { /* 缺失/损坏 → null */ }
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 冻结 patch 正文读取(thin 回放数据源):变更目录 change.patch;缺失/空 → null。
|
|
287
|
+
* @returns {string|null}
|
|
288
|
+
*/
|
|
289
|
+
function readChangePatchText(changeDir) {
|
|
290
|
+
if (!changeDir) return null
|
|
291
|
+
try {
|
|
292
|
+
const text = readFileSync(join(changeDir, 'change.patch'), 'utf8')
|
|
293
|
+
return text && text.trim() ? text : null
|
|
294
|
+
} catch { return null }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 冻结 patch 按段行数统计(thin 回放数据源):`diff --git a/<old> b/<new>` 段头切分
|
|
299
|
+
* (正则与 filterPatchForFiles/slicePatchForFile 同款,取 b/ 新路径),段内 +/− 前缀行计数
|
|
300
|
+
* (+++ / --- 文件头与 \ 续行标记不计);`new file mode` → new、`deleted file mode` →
|
|
301
|
+
* deleted、Binary/GIT binary patch → null 档——三档与 collectNumstatByPath 口径对齐。
|
|
302
|
+
* 自包含解析(不采 git numstat):冻结面含 done 时点工作树件(治理工件/untracked),且不
|
|
303
|
+
* 依赖 git 对象库存活。
|
|
304
|
+
* @returns {Map<string, {additions: number|null, deletions: number|null, kind: 'binary'|'new'|'modified'|'deleted'}>}
|
|
305
|
+
*/
|
|
306
|
+
function parseFrozenPatchStats(patchText) {
|
|
307
|
+
const stats = new Map()
|
|
308
|
+
if (!patchText) return stats
|
|
309
|
+
let cur = null
|
|
310
|
+
const flush = () => {
|
|
311
|
+
if (cur && cur.path) {
|
|
312
|
+
stats.set(cur.path, cur.binary
|
|
313
|
+
? { additions: null, deletions: null, kind: 'binary' }
|
|
314
|
+
: { additions: cur.additions, deletions: cur.deletions, kind: cur.kind })
|
|
315
|
+
}
|
|
316
|
+
cur = null
|
|
317
|
+
}
|
|
318
|
+
for (const line of String(patchText).split('\n')) {
|
|
319
|
+
if (line.startsWith('diff --git ')) {
|
|
320
|
+
flush()
|
|
321
|
+
const m = line.match(/^diff --git a\/(.+) b\/(.+)$/)
|
|
322
|
+
cur = m ? { path: toPosix(m[2]), additions: 0, deletions: 0, kind: 'modified', binary: false } : null
|
|
323
|
+
continue
|
|
324
|
+
}
|
|
325
|
+
if (!cur) continue
|
|
326
|
+
if (line.startsWith('new file mode')) cur.kind = 'new'
|
|
327
|
+
else if (line.startsWith('deleted file mode')) cur.kind = 'deleted'
|
|
328
|
+
else if (line.startsWith('Binary') || line.startsWith('GIT binary patch')) cur.binary = true
|
|
329
|
+
else if (line.startsWith('+') && !line.startsWith('+++')) cur.additions++
|
|
330
|
+
else if (line.startsWith('-') && !line.startsWith('---')) cur.deletions++
|
|
331
|
+
}
|
|
332
|
+
flush()
|
|
333
|
+
return stats
|
|
334
|
+
}
|
|
335
|
+
|
|
270
336
|
/**
|
|
271
337
|
* patch 段过滤(quick-90015473):全量 diff 按 `diff --git a/<old> b/<new>` 段头切分,
|
|
272
338
|
* 只保留 files 集合内的段——json rows(退栈归属集)与 patch 同口径,多会话仓不把并行
|
|
@@ -972,6 +1038,9 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
972
1038
|
// execute --done 落盘,complete.js `...snap` 展开零改动自动携带)原样回放;旧快照无该键
|
|
973
1039
|
// → undefined(JSON.stringify 自动省略——单仓/旧快照零新增字段,additive 契约)。
|
|
974
1040
|
const snapRepos = Array.isArray(snap.repos) ? snap.repos : undefined
|
|
1041
|
+
// 通道标注(2026-10-07-unify-close-trace):新快照带 closedBy(flow done / execute --done)
|
|
1042
|
+
// ——thin 快照回放不再误标 execute --done;旧快照无键缺省兼容。
|
|
1043
|
+
const closeChannel = typeof snap.closedBy === 'string' && snap.closedBy ? snap.closedBy : 'execute --done'
|
|
975
1044
|
// 旧快照跨仓注记(R-07):快照冻结于跨仓对账上线前(跨仓行恒 untouched 的 v1 ⊘ 形态、
|
|
976
1045
|
// 无 repos 键)——照旧回放不重算(「快照说什么是什么」契约不破),注记指明人工渠道
|
|
977
1046
|
// (该仓锚点锡点冻结在 execute-runs reviews,可人工 git diff 核实)。
|
|
@@ -1015,7 +1084,7 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
1015
1084
|
: { foreignDeclared: [] },
|
|
1016
1085
|
patchSha256: snap.patchSha256 || null,
|
|
1017
1086
|
patchStatus: snap.patchStatus || null,
|
|
1018
|
-
note: `${settleLabel}
|
|
1087
|
+
note: `${settleLabel}——${closeChannel} 时点冻结快照${snap.savedAt ? '(' + String(snap.savedAt).replace('T', ' ').slice(0, 19) + ' 落盘)' : ''}:文件集封闭在收尾时点,主仓后续新文件不进表;行数按锚 ${actual.baseAnchor.slice(0, 7)}→当前工作树 补采(同文件后续演进会计入)${legacyCrossNote}`,
|
|
1019
1088
|
}
|
|
1020
1089
|
}
|
|
1021
1090
|
return {
|
|
@@ -1033,7 +1102,64 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
1033
1102
|
: { foreignDeclared: [] },
|
|
1034
1103
|
patchSha256: snap.patchSha256 || null,
|
|
1035
1104
|
patchStatus: snap.patchStatus || null,
|
|
1036
|
-
note: `${settleLabel}
|
|
1105
|
+
note: `${settleLabel}——${closeChannel} 时点冻结快照${snap.savedAt ? '(' + String(snap.savedAt).replace('T', ' ').slice(0, 19) + ' 落盘)' : ''}:范围封闭在收尾时点,主仓后续改动不反映到本表;需要看当前工作区实时状态请跑 git status${legacyCrossNote}`,
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
// —— thin flow done 冻结记录回放(2026-10-07-scope-audit-thin-patch-replay):thin 轻量变更
|
|
1109
|
+
// flow done 只冻 change-patch.json + change.patch、不落 execute 快照——快照缺失时回读冻结件
|
|
1110
|
+
// 出真实三态(存量归档立即受益),不再落「实时开放区间 → 干净树空 → 清单全行计划未动
|
|
1111
|
+
// +0/−0」的失真链。文件集 = meta.files 过 filterDeliverableFiles(与 actual 侧同口径,
|
|
1112
|
+
// 治理工件不进表);行数自冻结 patch 按段统计(parseFrozenPatchStats,自包含);三态按
|
|
1113
|
+
// design 清单 pathMatches 判定(与主链路三态判定同款);跨仓条目维持 v1 ⊘ untouched 形态
|
|
1114
|
+
// (thin 冻结件只有主仓 patch,不假装对账过)。
|
|
1115
|
+
const doneMeta = readChangePatchMeta(changeDirInfo.dir)
|
|
1116
|
+
if (doneMeta && doneMeta.files.length > 0) {
|
|
1117
|
+
const frozenFiles = filterDeliverableFiles([...new Set(doneMeta.files.map(toPosix).filter(Boolean))]).sort()
|
|
1118
|
+
const patchOk = doneMeta.patchStatus === 'ok'
|
|
1119
|
+
const segStats = patchOk ? parseFrozenPatchStats(readChangePatchText(changeDirInfo.dir)) : new Map()
|
|
1120
|
+
const doneRows = []
|
|
1121
|
+
const doneMatched = new Set()
|
|
1122
|
+
for (const f of frozenFiles) {
|
|
1123
|
+
// 段缺失(冻结面零 diff 文件)/ patch 采集失败 → 行数 null 档(—),不出伪数据
|
|
1124
|
+
const st = segStats.get(f) || { additions: null, deletions: null, kind: 'modified' }
|
|
1125
|
+
const row = { path: f, additions: st.additions, deletions: st.deletions, kind: st.kind }
|
|
1126
|
+
if (!planDegraded) {
|
|
1127
|
+
const entry = plannedEntries.find(e => pathMatches(f, e.path))
|
|
1128
|
+
if (entry) {
|
|
1129
|
+
doneMatched.add(entry.path)
|
|
1130
|
+
row.planned = entry.operation || null
|
|
1131
|
+
row.verdict = 'planned'
|
|
1132
|
+
} else {
|
|
1133
|
+
row.planned = null
|
|
1134
|
+
row.verdict = 'unplanned'
|
|
1135
|
+
const facility = classifyToolScaffold(f)
|
|
1136
|
+
if (facility) row.facility = facility
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
doneRows.push(row)
|
|
1140
|
+
}
|
|
1141
|
+
// 计划未动补行:清单文件不在冻结面 → untouched 0/0(与主链路补行同款语义)
|
|
1142
|
+
for (const e of plannedEntries) {
|
|
1143
|
+
if (doneMatched.has(e.path)) continue
|
|
1144
|
+
doneRows.push(e.repo
|
|
1145
|
+
? { path: e.path, planned: e.operation || null, additions: 0, deletions: 0, kind: 'modified', verdict: 'untouched', crossRepo: e.repo }
|
|
1146
|
+
: { path: e.path, planned: e.operation || null, additions: 0, deletions: 0, kind: 'modified', verdict: 'untouched' })
|
|
1147
|
+
}
|
|
1148
|
+
const doneSettleLabel = changeDirInfo.archived ? '已归档' : 'execute 已收尾(分支已清理,待 verify/archive)'
|
|
1149
|
+
const doneSavedAt = doneMeta.savedAt ? ',' + String(doneMeta.savedAt).replace('T', ' ').slice(0, 19) + ' 落盘' : ''
|
|
1150
|
+
return {
|
|
1151
|
+
...base,
|
|
1152
|
+
ok: true,
|
|
1153
|
+
degradedReason: planDegraded,
|
|
1154
|
+
baseAnchor: typeof doneMeta.baseline === 'string' && /^[0-9a-f]{7,40}$/.test(doneMeta.baseline) ? doneMeta.baseline : null,
|
|
1155
|
+
totals: { files: doneRows.length, ...sumTotals(doneRows) },
|
|
1156
|
+
rows: doneRows,
|
|
1157
|
+
excluded: { foreignDeclared: [] },
|
|
1158
|
+
patchSha256: doneMeta.patchSha256 || null,
|
|
1159
|
+
patchStatus: doneMeta.patchStatus || null,
|
|
1160
|
+
// --file 冻结切片消费(getFileDiff 唯一读取方):指向 change.patch,sha256 校验 A-F01 同款
|
|
1161
|
+
...(patchOk ? { frozenPatchPath: join(changeDirInfo.dir, 'change.patch') } : {}),
|
|
1162
|
+
note: `${doneSettleLabel}——flow done 时点冻结 patch 记录(change-patch.json + change.patch${doneSavedAt}):文件集与行数封闭在 done 时点(thin 轻量变更无 execute 快照),主仓后续改动不反映到本表${patchOk ? '' : ';冻结 patch 当时采集失败——行数列不可得(—),文件集仍为 done 时点冻结面'};需要看当前工作区实时状态请跑 git status`,
|
|
1037
1163
|
}
|
|
1038
1164
|
}
|
|
1039
1165
|
// 快照缺失 → 实时开放区间兜底(下方主链路),note 追加漂移警告
|
|
@@ -1047,7 +1173,7 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
1047
1173
|
// 已收尾但快照缺失(上方分支置位):实时开放区间兜底,明示漂移语义——快照机制上线
|
|
1048
1174
|
// (2026-09-10)前收尾的旧变更冻结记录无法重建,如实告知替代渠道。
|
|
1049
1175
|
if (noSnapshotDrift) {
|
|
1050
|
-
notes.push('快照缺失(变更已收尾——已归档或 execute
|
|
1176
|
+
notes.push('快照缺失(变更已收尾——已归档或 execute 分支已清理;快照与 flow done 冻结 patch 记录(change-patch.json)均无,无法重建冻结记录——快照机制与冻结件留档上线前收尾的变更即此形态)——下表为实时开放区间(基点→当前工作树,含并行会话与后续演进,随主仓改动漂移),非本变更冻结范围;记录态参考 verify-result.md / git log')
|
|
1051
1177
|
}
|
|
1052
1178
|
|
|
1053
1179
|
// —— 行数采集根与基点 ——
|
package/src/verify-postcheck.js
CHANGED
|
@@ -2773,8 +2773,15 @@ function buildDepsBatches({ deps, changedFiles = [], hits = [], cwd = null, prio
|
|
|
2773
2773
|
const prio = (f) => (changedSet.has(norm(f)) || prioritySet.has(norm(f))) ? 0 : 1 // 变更/FR 回归测试文件优先
|
|
2774
2774
|
const py = deps.filter(f => f.endsWith('.py')).sort((a, b) => (prio(a) > prio(b) ? 1 : prio(a) < prio(b) ? -1 : a.localeCompare(b)))
|
|
2775
2775
|
const js = deps.filter(f => !f.endsWith('.py')).sort((a, b) => (prio(a) > prio(b) ? 1 : prio(a) < prio(b) ? -1 : a.localeCompare(b)))
|
|
2776
|
+
// 套件编排器过滤(2026-10-08-thin-done-dirty-gate-and-paren-attribution,坑:FR 绑定把
|
|
2777
|
+
// run-tests.mjs 收进 deps 批 → 内嵌执行=递归全量套件——嵌套 runner 污染 + 15-20 分钟窗口,
|
|
2778
|
+
// 实测 not ok 假败阻断收口)。run-tests.mjs 是 npm test 的组卷入口不是单测文件:执行面剔除
|
|
2779
|
+
// 并 loud 披露;全量门由收口实测与 CI 层的 npm test 承担(本就覆盖该文件的全部语义)。
|
|
2780
|
+
const SUITE_META_RE = /(^|\/)run-tests\.mjs$/
|
|
2781
|
+
const jsMeta = js.filter(f => SUITE_META_RE.test(norm(f)))
|
|
2782
|
+
const jsList = js.filter(f => !SUITE_META_RE.test(norm(f)))
|
|
2776
2783
|
const CAP = 30
|
|
2777
|
-
const pyCap = py.length === 0 ? 0 :
|
|
2784
|
+
const pyCap = py.length === 0 ? 0 : jsList.length === 0 ? CAP : Math.max(5, Math.min(CAP - 5, Math.round(CAP * py.length / (py.length + jsList.length))))
|
|
2778
2785
|
const jsCap = CAP - pyCap
|
|
2779
2786
|
// 配额制组卷(2026-10-06-fr-regress-cap-drop):优先面整跑豁免帽,普通依赖填剩余席位;
|
|
2780
2787
|
// dropped 只计普通依赖弃置(优先面零弃置是构造保证),组内序保持「优先前缀+字母序」。
|
|
@@ -2785,7 +2792,7 @@ function buildDepsBatches({ deps, changedFiles = [], hits = [], cwd = null, prio
|
|
|
2785
2792
|
return { run: [...prioInGroup, ...ordinaryRun], prioCount: prioInGroup.length, dropped: ordinary.length - ordinaryRun.length, prioDropped: 0 }
|
|
2786
2793
|
}
|
|
2787
2794
|
const pyQ = quotaFill(py, pyCap)
|
|
2788
|
-
const jsQ = quotaFill(
|
|
2795
|
+
const jsQ = quotaFill(jsList, jsCap)
|
|
2789
2796
|
const pyRun = pyQ.run
|
|
2790
2797
|
const jsRun = jsQ.run
|
|
2791
2798
|
// .py 运行器推断双源(2026-09-26-dynamic-test-inference):① 命中模块命令串(旧路径,兼容期);
|
|
@@ -2855,7 +2862,8 @@ function buildDepsBatches({ deps, changedFiles = [], hits = [], cwd = null, prio
|
|
|
2855
2862
|
// dropped 只计普通依赖弃置、prioDropped=防御路径计数(构造上恒 0——quotaFill 保证优先面零弃置,
|
|
2856
2863
|
// >0 即组卷逻辑被破坏,消费面 loud 披露)。js 批经 jsNative/jsProject 内容分流后计数随分流面。
|
|
2857
2864
|
if (pyRun.length > 0) batches.push({ name: 'deps(auto-py)', short: 'py', command: `${pyRunner} ${pyRun.map(rebase).join(' ')}`, count: pyRun.length, prioCount: pyQ.prioCount, dropped: pyQ.dropped, prioDropped: pyQ.prioDropped, files: pyRun })
|
|
2858
|
-
if (jsNative.length > 0) batches.push({ name: 'deps(auto-js)', short: 'js', command: `node --test --test-reporter=spec --test-reporter-destination=stderr --test-reporter=tap --test-reporter-destination=stdout ${jsNative.join(' ')}`, count: jsNative.length, prioCount: jsNative.filter(f => prio(f) === 0).length, dropped:
|
|
2865
|
+
if (jsNative.length > 0) batches.push({ name: 'deps(auto-js)', short: 'js', command: `node --test --test-reporter=spec --test-reporter-destination=stderr --test-reporter=tap --test-reporter-destination=stdout ${jsNative.join(' ')}`, count: jsNative.length, prioCount: jsNative.filter(f => prio(f) === 0).length, dropped: jsList.length - jsRun.length, prioDropped: jsQ.prioDropped, tap: true, files: jsNative })
|
|
2866
|
+
if (jsMeta.length > 0) batches.push({ name: 'deps(auto-js-meta-skip)', short: 'js-meta-skip', command: null, count: jsMeta.length, dropped: 0, skip: true, files: jsMeta, reason: '套件编排器(run-tests.mjs=npm test 组卷入口)非单测文件——内嵌执行=递归全量套件(嵌套 runner 污染假败);全量门由收口实测/CI 的 npm test 承担' })
|
|
2859
2867
|
if (jsProjectRun.length > 0) {
|
|
2860
2868
|
if (jsxRunner) {
|
|
2861
2869
|
const isVitest = /vitest/.test(jsxRunner)
|