sillyspec 3.28.5 → 3.28.6
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/sa-rename-probe/src/new.js +4 -0
- package/src/config-schema.js +13 -0
- package/src/decision-distill.js +8 -2
- package/src/docs-check.js +9 -5
- package/src/friction-tally.js +221 -0
- package/src/index.js +4 -1
- package/src/progress/change-registry.js +2 -0
- package/src/progress.js +3 -0
- package/src/run/command.js +17 -1
- package/src/run/complete-handlers.js +74 -1
- package/src/run/complete.js +39 -8
- package/src/run/gate-snapshot.js +73 -2
- package/src/run/gates.js +53 -16
- package/src/run/quick-audit.js +40 -3
- package/src/run/shared.js +29 -1
- package/src/run/stage.js +14 -0
- package/src/run/verify-quality-scan.js +52 -5
- package/src/scope-audit.js +231 -25
- package/src/stages/brainstorm.js +3 -3
package/src/scope-audit.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
22
22
|
import { join, dirname } from 'node:path'
|
|
23
|
-
import { locateQuickSessionGuard, auditQuickCompletion, resolveRuntimeRoot, collectOtherQuickSessionDeclarations } from './run/shared.js'
|
|
23
|
+
import { locateQuickSessionGuard, auditQuickCompletion, resolveRuntimeRoot, collectOtherQuickSessionDeclarations, ancestorSpecDirs } from './run/shared.js'
|
|
24
24
|
import { safeGit } from './git-helper.js'
|
|
25
25
|
import { parseFileChangeListDetailed, pathMatches } from './change-list.js'
|
|
26
26
|
|
|
@@ -212,9 +212,16 @@ function resolveDiffRoot(sb, changeName, form, cwd) {
|
|
|
212
212
|
|
|
213
213
|
/**
|
|
214
214
|
* execute --done 时点快照读取(归档记录态数据源):结构即 computeFullFlowAudit 返回值 +
|
|
215
|
-
* savedAt
|
|
215
|
+
* savedAt。读取链(quick-359a48f1):变更目录 scope-audit.json(审计级,随归档入库)→
|
|
216
|
+
* .runtime/scope-audit-<change>.json(存量兼容)→ null(调用方降级,不出伪数据)。
|
|
216
217
|
*/
|
|
217
|
-
function readScopeSnapshot(runtimeRoot, changeName) {
|
|
218
|
+
function readScopeSnapshot(changeDir, runtimeRoot, changeName) {
|
|
219
|
+
if (changeDir) {
|
|
220
|
+
try {
|
|
221
|
+
const snap = JSON.parse(readFileSync(join(changeDir, 'scope-audit.json'), 'utf8'))
|
|
222
|
+
if (snap && typeof snap === 'object' && Array.isArray(snap.rows)) return snap
|
|
223
|
+
} catch { /* 缺失/损坏 → 下一源 */ }
|
|
224
|
+
}
|
|
218
225
|
try {
|
|
219
226
|
const snap = JSON.parse(readFileSync(join(runtimeRoot, `scope-audit-${changeName}.json`), 'utf8'))
|
|
220
227
|
if (snap && typeof snap === 'object' && Array.isArray(snap.rows)) return snap
|
|
@@ -222,6 +229,122 @@ function readScopeSnapshot(runtimeRoot, changeName) {
|
|
|
222
229
|
return null
|
|
223
230
|
}
|
|
224
231
|
|
|
232
|
+
/**
|
|
233
|
+
* 冻结 patch 读取(审计级存储,--file 已收尾切片的数据源):变更目录 scope-audit.patch。
|
|
234
|
+
* @returns {string|null}
|
|
235
|
+
*/
|
|
236
|
+
function readFrozenPatch(changeDir) {
|
|
237
|
+
if (!changeDir) return null
|
|
238
|
+
try {
|
|
239
|
+
const text = readFileSync(join(changeDir, 'scope-audit.patch'), 'utf8')
|
|
240
|
+
return text && text.trim() ? text : null
|
|
241
|
+
} catch { return null }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* patch 段过滤(quick-90015473):全量 diff 按 `diff --git a/<old> b/<new>` 段头切分,
|
|
246
|
+
* 只保留 files 集合内的段——json rows(退栈归属集)与 patch 同口径,多会话仓不把并行
|
|
247
|
+
* 会话改动重复冻结进每份 patch(实测 341KB 中 98% 为并行文件)。
|
|
248
|
+
*/
|
|
249
|
+
function filterPatchForFiles(patchText, filesSet) {
|
|
250
|
+
if (!patchText) return ''
|
|
251
|
+
const lines = patchText.split('\n')
|
|
252
|
+
const out = []
|
|
253
|
+
let capturing = false
|
|
254
|
+
for (const line of lines) {
|
|
255
|
+
if (line.startsWith('diff --git ')) {
|
|
256
|
+
const m = line.match(/^diff --git a\/(.+) b\/(.+)$/)
|
|
257
|
+
const p = m ? toPosix(m[2]) : null
|
|
258
|
+
capturing = !!(p && filesSet.has(p))
|
|
259
|
+
}
|
|
260
|
+
if (capturing) out.push(line)
|
|
261
|
+
}
|
|
262
|
+
return out.join('\n')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 冻结 patch 生成(quick-359a48f1 审计级 / quick-90015473 归属口径修正):tracked 改动 =
|
|
267
|
+
* `git diff --no-color <baseRef>` 后**按 files 过滤段**(与 rows 同归属口径,不含并行会话
|
|
268
|
+
* 文件);untracked 新文件(git diff 天然不含)自拼 new file hunk(避 `diff --no-index`
|
|
269
|
+
* exit 1 坑),binary 只记标记行(防 base64 体积爆炸)。
|
|
270
|
+
* @param {string} root diff 执行根(execute=worktree 根 / quick=会话根 / 主仓根)
|
|
271
|
+
* @param {string[]} files 归属文件集(tracked 段过滤 + untracked 判定双重来源)
|
|
272
|
+
* @param {{ baseRef: string }} opts
|
|
273
|
+
* @returns {string|null} null=采集失败(调用方 fail-soft 不落伪 patch)
|
|
274
|
+
*/
|
|
275
|
+
export function buildFrozenPatch(root, files, { baseRef } = {}) {
|
|
276
|
+
if (!root || !baseRef) return null
|
|
277
|
+
const wanted = new Set((Array.isArray(files) ? files : []).map(toPosix).filter(Boolean))
|
|
278
|
+
const parts = []
|
|
279
|
+
try {
|
|
280
|
+
const r = safeGit(root, ['diff', '--no-color', baseRef], { timeout: 60 * 1000 })
|
|
281
|
+
const filtered = filterPatchForFiles(r.error ? null : r.value, wanted)
|
|
282
|
+
if (filtered) parts.push(filtered)
|
|
283
|
+
const trackedOut = safeGit(root, ['ls-files'], { timeout: 30 * 1000 })
|
|
284
|
+
const trackedSet = new Set(String(trackedOut.value || '').split('\n').map(toPosix).filter(Boolean))
|
|
285
|
+
for (const f of (Array.isArray(files) ? files : []).map(toPosix).filter(Boolean)) {
|
|
286
|
+
if (trackedSet.has(f)) continue
|
|
287
|
+
const abs = join(root, f)
|
|
288
|
+
if (!existsSync(abs)) continue
|
|
289
|
+
let buf
|
|
290
|
+
try { buf = readFileSync(abs) } catch { continue }
|
|
291
|
+
if (buf.includes(0)) {
|
|
292
|
+
parts.push(`diff --git a/${f} b/${f}\nnew file mode 100644\nBinary file ${f} differs\n`)
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
const lines = buf.toString('utf8').split('\n')
|
|
296
|
+
if (lines.length && lines[lines.length - 1] === '') lines.pop()
|
|
297
|
+
const body = lines.map(l => '+' + l).join('\n')
|
|
298
|
+
parts.push(`diff --git a/${f} b/${f}\nnew file mode 100644\n--- /dev/null\n+++ b/${f}\n@@ -0,0 +1,${lines.length} @@\n${body}\n`)
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
return null
|
|
302
|
+
}
|
|
303
|
+
return parts.length > 0 ? parts.join('\n') : ''
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* patch 切片(--file 已收尾优先路径):从全量 patch 文本切出目标文件的 hunk 段
|
|
308
|
+
* (`diff --git a/<old> b/<new>` 到下一个 diff 头)。按 b/ 新路径匹配(rename 兼容旧路径)。
|
|
309
|
+
* @returns {string|null}
|
|
310
|
+
*/
|
|
311
|
+
function slicePatchForFile(patchText, filePath) {
|
|
312
|
+
if (!patchText || !filePath) return null
|
|
313
|
+
const norm = toPosix(filePath)
|
|
314
|
+
const lines = patchText.split('\n')
|
|
315
|
+
const out = []
|
|
316
|
+
let capturing = false
|
|
317
|
+
for (const line of lines) {
|
|
318
|
+
if (line.startsWith('diff --git ')) {
|
|
319
|
+
const m = line.match(/^diff --git a\/(.+) b\/(.+)$/)
|
|
320
|
+
capturing = !!(m && (toPosix(m[2]) === norm || toPosix(m[1]) === norm))
|
|
321
|
+
}
|
|
322
|
+
if (capturing) out.push(line)
|
|
323
|
+
}
|
|
324
|
+
return out.length > 0 ? out.join('\n') : null
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* quick 记录态反查(guard 清理后):扫祖先链各 specBase 的 quicklog/patches/*.json,
|
|
329
|
+
* 按 json 内冗余的 sessionId 匹配。返回记录 + patch 路径(--file 切片源)。
|
|
330
|
+
*/
|
|
331
|
+
function findQuickPatchRecord(cwd, sessionId) {
|
|
332
|
+
for (const sb of ancestorSpecDirs(cwd)) {
|
|
333
|
+
const dir = join(sb, 'quicklog', 'patches')
|
|
334
|
+
let files
|
|
335
|
+
try { files = readdirSync(dir) } catch { continue }
|
|
336
|
+
for (const f of files.filter(x => x.endsWith('.json'))) {
|
|
337
|
+
try {
|
|
338
|
+
const j = JSON.parse(readFileSync(join(dir, f), 'utf8'))
|
|
339
|
+
if (j && j.sessionId === sessionId && Array.isArray(j.rows)) {
|
|
340
|
+
return { record: j, patchPath: join(dir, f.replace(/\.json$/, '.patch')), qlId: j.qlId || f.replace(/\.json$/, '') }
|
|
341
|
+
}
|
|
342
|
+
} catch { /* 单文件损坏跳过 */ }
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return null
|
|
346
|
+
}
|
|
347
|
+
|
|
225
348
|
/** 汇总非 null 行数(binary 与降级 null 不计入,不出伪数据) */
|
|
226
349
|
function sumTotals(rows) {
|
|
227
350
|
let additions = 0
|
|
@@ -238,7 +361,7 @@ function sumTotals(rows) {
|
|
|
238
361
|
* 他者会话声明索引与 --done 收尾同源实时采集(complete-handlers :1050 同款 merge),保证
|
|
239
362
|
* foreignDeclared 豁免面与门禁口径一致(R-04:行数只对归属本会话的文件采集)。
|
|
240
363
|
*/
|
|
241
|
-
async function computeQuickAudit({ cwd, platformOpts, sessionId, located }) {
|
|
364
|
+
async function computeQuickAudit({ cwd, platformOpts, sessionId, located, collectPatch }) {
|
|
242
365
|
const guardSpecBase = located.specBase
|
|
243
366
|
const guard = located.guard
|
|
244
367
|
// 审计在「创建会话的项目根」跑(located.specBase 父目录):cd 漂移后 cwd 可能是子目录,
|
|
@@ -315,6 +438,10 @@ async function computeQuickAudit({ cwd, platformOpts, sessionId, located }) {
|
|
|
315
438
|
}
|
|
316
439
|
}
|
|
317
440
|
|
|
441
|
+
const frozenPatch = collectPatch
|
|
442
|
+
? buildFrozenPatch(sessionRoot, rowFiles, { baseRef: 'HEAD' })
|
|
443
|
+
: undefined
|
|
444
|
+
|
|
318
445
|
return {
|
|
319
446
|
...base,
|
|
320
447
|
totals,
|
|
@@ -324,6 +451,7 @@ async function computeQuickAudit({ cwd, platformOpts, sessionId, located }) {
|
|
|
324
451
|
? audit.foreignSessionDeclared.filter(x => x && typeof x === 'object')
|
|
325
452
|
: [],
|
|
326
453
|
},
|
|
454
|
+
frozenPatch,
|
|
327
455
|
note,
|
|
328
456
|
}
|
|
329
457
|
}
|
|
@@ -334,11 +462,44 @@ async function computeQuickAudit({ cwd, platformOpts, sessionId, located }) {
|
|
|
334
462
|
* only;baseAnchor 缺失 → 行数按 HEAD 未提交窗口兜底(quick 模式同口径,不再恒 —);
|
|
335
463
|
* 归档且实时窗口空 → execute 时点快照记录态(快照也缺 → 空表诚实说明)。
|
|
336
464
|
*/
|
|
337
|
-
async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts }) {
|
|
465
|
+
async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, collectPatch }) {
|
|
338
466
|
const sb = specBase || join(cwd, '.sillyspec')
|
|
339
467
|
const runtimeRoot = resolveRuntimeRoot(platformOpts, sb)
|
|
340
468
|
const changeDirInfo = resolveChangeDir(sb, changeName)
|
|
341
469
|
|
|
470
|
+
// —— 预执行形态(quick-bbb5037c):执行证据三信号(worktree meta / 分支 sillyspec/<名> /
|
|
471
|
+
// 审计 tag)全无 = 尚未进入 execute——实际侧不存在(B2 会吞整个工作区脏文件误归属本变更,
|
|
472
|
+
// 2026-09-11-cross-change-decision-guard 实证 28 个「计划外」全是并行在途)。出计划清单
|
|
473
|
+
// 视图:rows=design 清单 untouched(待实现),工作区改动归属各自会话不进表。
|
|
474
|
+
const hasWorktreeMeta = existsSync(join(sb, '.runtime', 'worktrees', changeName, 'meta.json'))
|
|
475
|
+
const hasBranch = !!safeGit(cwd, ['rev-parse', '--verify', '--quiet', `sillyspec/${changeName}^{commit}`], { timeout: 15 * 1000 }).value
|
|
476
|
+
const hasAuditTag = !!safeGit(cwd, ['rev-parse', '--verify', '--quiet', `sillyspec-audit/sillyspec/${changeName}^{commit}`], { timeout: 15 * 1000 }).value
|
|
477
|
+
if (!hasWorktreeMeta && !hasBranch && !hasAuditTag) {
|
|
478
|
+
const emptyBase = {
|
|
479
|
+
mode: 'full-flow', ok: true, degradedReason: null, baseAnchor: null,
|
|
480
|
+
totals: { files: 0, additions: 0, deletions: 0 }, rows: [],
|
|
481
|
+
excluded: { foreignDeclared: [] }, frozenPatch: undefined,
|
|
482
|
+
}
|
|
483
|
+
let prePlanned = []
|
|
484
|
+
try {
|
|
485
|
+
prePlanned = parseFileChangeListDetailed(
|
|
486
|
+
join(changeDirInfo ? changeDirInfo.dir : join(sb, 'changes', changeName), 'design.md'),
|
|
487
|
+
{ keepSillyspecDocs: true })
|
|
488
|
+
} catch { /* 清单解析失败 → 空清单 + 说明 */ }
|
|
489
|
+
const preRows = prePlanned.map(e => ({
|
|
490
|
+
path: e.path, planned: e.operation || null, additions: 0, deletions: 0,
|
|
491
|
+
kind: 'modified', verdict: 'untouched',
|
|
492
|
+
}))
|
|
493
|
+
return {
|
|
494
|
+
...emptyBase,
|
|
495
|
+
totals: { files: preRows.length, additions: 0, deletions: 0 },
|
|
496
|
+
rows: preRows,
|
|
497
|
+
note: preRows.length > 0
|
|
498
|
+
? '变更尚未进入 execute(无 worktree/分支/审计 tag)——下表为 design 清单(全部待实现);工作区其他未提交改动归属各自会话/变更,不在本表(若确已执行过但三信号均被手工清理,此处会误判——git log 核实)'
|
|
499
|
+
: '变更尚未进入 execute(无 worktree/分支/审计 tag)且 design 无可解析清单——暂无可对账内容',
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
342
503
|
// —— 计划侧(单一真相:change-list.js,禁自研表格解析;归档形态目录兼容)——
|
|
343
504
|
const designMdPath = join(changeDirInfo ? changeDirInfo.dir : join(sb, 'changes', changeName), 'design.md')
|
|
344
505
|
let plannedEntries = []
|
|
@@ -380,7 +541,7 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts })
|
|
|
380
541
|
excluded: { foreignDeclared: [] },
|
|
381
542
|
note: null,
|
|
382
543
|
}
|
|
383
|
-
let
|
|
544
|
+
let noSnapshotDrift = false
|
|
384
545
|
|
|
385
546
|
if (!actual || actual.ok === false) {
|
|
386
547
|
// 实际侧整体失败:三态无锚不出表(fail-soft 单行提示由注入点兑现),计划侧降级信息一并带出
|
|
@@ -390,14 +551,18 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts })
|
|
|
390
551
|
return { ...base, degradedReason: [reason, planDegraded].filter(Boolean).join(';') }
|
|
391
552
|
}
|
|
392
553
|
|
|
393
|
-
// ——
|
|
394
|
-
// apply 时点,不随后续主仓改动漂移(quick-f5acdeeb
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
554
|
+
// —— 已收尾变更·冻结快照优先(真实 > 冻结记录 > 实时开放区间):归档变更的范围应封闭在
|
|
555
|
+
// apply 时点,不随后续主仓改动漂移(quick-f5acdeeb);quick-bd84b852 放宽——**post-apply
|
|
556
|
+
// (分支已删 = execute 已 apply 收尾)的活跃变更同样收尾**,此后实时区间只会被并行演进泡大
|
|
557
|
+
// (multi-agent-platform 2026-09-04 滞留变更实证:741 文件零提示刷屏)。execute --done 落的
|
|
558
|
+
// 快照即冻结记录(含行数)——已收尾一律先出快照;实时窗口(开放区间)只作快照缺失的兜底,
|
|
559
|
+
// note 明示会漂。tag 封闭区间不可用:分支上仅 baseline checkpoint(并行 WIP 快照),本变更
|
|
560
|
+
// 改动经工作树 apply 从未 commit 到分支。 ——
|
|
561
|
+
const settled = changeDirInfo && (changeDirInfo.archived || actual.form === 'post-apply')
|
|
562
|
+
if (settled) {
|
|
563
|
+
const snap = readScopeSnapshot(changeDirInfo.dir, runtimeRoot, changeName)
|
|
400
564
|
if (snap && snap.rows.length > 0) {
|
|
565
|
+
const settleLabel = changeDirInfo.archived ? '已归档' : 'execute 已收尾(分支已清理,待 verify/archive)'
|
|
401
566
|
// 行数补采:快照可能落盘于 apply 后、tag 锚落地前的窗口(行数列全 —);tag 锚
|
|
402
567
|
// (quick-df1fed77)恢复 merge-base 后,对冻结文件集按锚补采行数——文件集不重算
|
|
403
568
|
// (冻结语义不变),仅行数从 — 复活;含主仓后续演进(同文件被再改会计入),口径标注。
|
|
@@ -426,7 +591,7 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts })
|
|
|
426
591
|
excluded: snap.excluded && Array.isArray(snap.excluded.foreignDeclared)
|
|
427
592
|
? snap.excluded
|
|
428
593
|
: { foreignDeclared: [] },
|
|
429
|
-
note:
|
|
594
|
+
note: `${settleLabel}——execute --done 时点冻结快照${snap.savedAt ? '(' + String(snap.savedAt).replace('T', ' ').slice(0, 19) + ' 落盘)' : ''}:文件集封闭在 apply 时点,主仓后续新文件不进表;行数按锚 ${actual.baseAnchor.slice(0, 7)}→当前工作树 补采(同文件后续演进会计入)`,
|
|
430
595
|
}
|
|
431
596
|
}
|
|
432
597
|
return {
|
|
@@ -441,19 +606,20 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts })
|
|
|
441
606
|
excluded: snap.excluded && Array.isArray(snap.excluded.foreignDeclared)
|
|
442
607
|
? snap.excluded
|
|
443
608
|
: { foreignDeclared: [] },
|
|
444
|
-
note:
|
|
609
|
+
note: `${settleLabel}——execute --done 时点冻结快照${snap.savedAt ? '(' + String(snap.savedAt).replace('T', ' ').slice(0, 19) + ' 落盘)' : ''}:范围封闭在 apply 时点,主仓后续改动不反映到本表;需要看当前工作区实时状态请跑 git status`,
|
|
445
610
|
}
|
|
446
611
|
}
|
|
447
612
|
// 快照缺失 → 实时开放区间兜底(下方主链路),note 追加漂移警告
|
|
448
|
-
|
|
613
|
+
noSnapshotDrift = true
|
|
449
614
|
}
|
|
450
615
|
|
|
451
616
|
const degraded = []
|
|
452
617
|
if (planDegraded) degraded.push(planDegraded)
|
|
453
618
|
const notes = []
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
619
|
+
// 已收尾但快照缺失(上方分支置位):实时开放区间兜底,明示漂移语义——快照机制上线
|
|
620
|
+
// (2026-09-10)前收尾的旧变更冻结记录无法重建,如实告知替代渠道。
|
|
621
|
+
if (noSnapshotDrift) {
|
|
622
|
+
notes.push('快照缺失(变更已收尾——已归档或 execute 分支已清理;快照机制上线前收尾的变更无法重建冻结记录)——下表为实时开放区间(基点→当前工作树,含并行会话与后续演进,随主仓改动漂移),非本变更冻结范围;记录态参考 verify-result.md / git log')
|
|
457
623
|
}
|
|
458
624
|
|
|
459
625
|
// —— 行数采集根与基点 ——
|
|
@@ -507,6 +673,12 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts })
|
|
|
507
673
|
rows.push({ path: e.path, planned: e.operation || null, additions: 0, deletions: 0, kind: 'modified', verdict: 'untouched' })
|
|
508
674
|
}
|
|
509
675
|
|
|
676
|
+
// 冻结 patch(collectPatch,execute --done 落盘方消费):与行数同根同锚(numstatRoot +
|
|
677
|
+
// baseAnchor;无锚 HEAD 兜底同口径),全窗口 diff + untracked 自拼 hunk(buildFrozenPatch)
|
|
678
|
+
const frozenPatch = collectPatch
|
|
679
|
+
? buildFrozenPatch(numstatRoot, actual.files, { baseRef: actual.baseAnchor || 'HEAD' })
|
|
680
|
+
: undefined
|
|
681
|
+
|
|
510
682
|
return {
|
|
511
683
|
...base,
|
|
512
684
|
ok: true,
|
|
@@ -515,6 +687,7 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts })
|
|
|
515
687
|
totals: { files: rows.length, ...sumTotals(rows) },
|
|
516
688
|
rows,
|
|
517
689
|
excluded: { foreignDeclared: [] },
|
|
690
|
+
frozenPatch,
|
|
518
691
|
note: notes.length > 0 ? notes.join(';') : null,
|
|
519
692
|
}
|
|
520
693
|
}
|
|
@@ -542,7 +715,7 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts })
|
|
|
542
715
|
* excluded: { foreignDeclared: Array<{ file: string, sessions: string[] }> },
|
|
543
716
|
* note?: string|null }>}
|
|
544
717
|
*/
|
|
545
|
-
export async function computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts } = {}) {
|
|
718
|
+
export async function computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts, collectPatch } = {}) {
|
|
546
719
|
const empty = {
|
|
547
720
|
mode: 'full-flow', ok: false, degradedReason: null, baseAnchor: null,
|
|
548
721
|
totals: { files: 0, additions: 0, deletions: 0 },
|
|
@@ -556,15 +729,33 @@ export async function computeChangeScopeAudit({ cwd, specBase, changeName, platf
|
|
|
556
729
|
if (QUICK_SID_RE.test(changeName)) {
|
|
557
730
|
const located = locateQuickSessionGuard(cwd, changeName)
|
|
558
731
|
if (located) {
|
|
559
|
-
return await computeQuickAudit({ cwd, platformOpts, sessionId: changeName, located })
|
|
732
|
+
return await computeQuickAudit({ cwd, platformOpts, sessionId: changeName, located, collectPatch })
|
|
733
|
+
}
|
|
734
|
+
// guard 已清理(会话收尾)→ quicklog/patches/ 记录态反查(quick-359a48f1):
|
|
735
|
+
// 命中出冻结记录(行数为 --done 时点真值),patch 可供 --file 切片
|
|
736
|
+
const rec = findQuickPatchRecord(cwd, changeName)
|
|
737
|
+
if (rec) {
|
|
738
|
+
return {
|
|
739
|
+
mode: 'quick', ok: true, degradedReason: null,
|
|
740
|
+
baseAnchor: rec.record.baseAnchor || `quick-window:${changeName}`,
|
|
741
|
+
totals: rec.record.totals && typeof rec.record.totals === 'object'
|
|
742
|
+
? rec.record.totals
|
|
743
|
+
: { files: rec.record.rows.length, ...sumTotals(rec.record.rows) },
|
|
744
|
+
rows: rec.record.rows,
|
|
745
|
+
excluded: rec.record.excluded && Array.isArray(rec.record.excluded.foreignDeclared)
|
|
746
|
+
? rec.record.excluded
|
|
747
|
+
: { foreignDeclared: [] },
|
|
748
|
+
frozenPatchPath: rec.patchPath,
|
|
749
|
+
note: `quick 会话已收尾——记录态(quicklog/patches/${rec.qlId},${rec.record.savedAt ? String(rec.record.savedAt).replace('T', ' ').slice(0, 19) + ' 落盘' : '--done 时点冻结'})`,
|
|
750
|
+
}
|
|
560
751
|
}
|
|
561
752
|
return {
|
|
562
753
|
...empty,
|
|
563
754
|
mode: 'quick',
|
|
564
|
-
degradedReason: `quick 会话 ${changeName}
|
|
755
|
+
degradedReason: `quick 会话 ${changeName} 不存在(guard 已清理且 quicklog/patches/ 无记录——会话未产生改动或早于记录机制)`,
|
|
565
756
|
}
|
|
566
757
|
}
|
|
567
|
-
return await computeFullFlowAudit({ cwd, specBase, changeName, platformOpts })
|
|
758
|
+
return await computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, collectPatch })
|
|
568
759
|
} catch (e) {
|
|
569
760
|
// fail-soft 兜底(D-006 / 兼容策略):注入点只打一行提示,不阻断阶段完成
|
|
570
761
|
return {
|
|
@@ -592,6 +783,23 @@ export async function getFileDiff({ cwd, specBase, changeName, platformOpts, fil
|
|
|
592
783
|
}
|
|
593
784
|
try {
|
|
594
785
|
const result = await computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts })
|
|
786
|
+
const norm = toPosix(filePath)
|
|
787
|
+
const sb = specBase || join(cwd, '.sillyspec')
|
|
788
|
+
|
|
789
|
+
// —— 真·当时内容比对(quick-359a48f1):已收尾变更优先冻结 patch 切片 ——
|
|
790
|
+
// ① quick 记录态自带 frozenPatchPath;② full-flow 变更目录 scope-audit.patch。
|
|
791
|
+
// patch 冻结在收尾时点,不含后续演进——实时锚 diff 只作 patch 缺失的兜底(口径标注)。
|
|
792
|
+
const frozenPatch = (result.frozenPatchPath
|
|
793
|
+
&& (() => { try { return readFileSync(result.frozenPatchPath, 'utf8') } catch { return null } })())
|
|
794
|
+
|| readFrozenPatch(resolveChangeDir(sb, changeName)?.dir || null)
|
|
795
|
+
if (frozenPatch) {
|
|
796
|
+
const sliced = slicePatchForFile(frozenPatch, norm)
|
|
797
|
+
if (sliced) {
|
|
798
|
+
return { ok: true, mode: result.mode, root: cwd, baseRef: null, anchorLabel: '冻结 patch(收尾时点内容,无后续演进混入)', diff: sliced, note: null }
|
|
799
|
+
}
|
|
800
|
+
return { ok: true, mode: result.mode, root: cwd, baseRef: null, anchorLabel: '冻结 patch', diff: null, note: '该文件不在冻结 patch 内(窗口外文件)' }
|
|
801
|
+
}
|
|
802
|
+
|
|
595
803
|
// 锚点解析(语义锚 head-uncommitted-window → 实际 baseRef 'HEAD';无锚同兜底并标注)
|
|
596
804
|
let baseRef = 'HEAD'
|
|
597
805
|
let anchorLabel = 'HEAD 未提交窗口(无锚点兜底)'
|
|
@@ -604,7 +812,6 @@ export async function getFileDiff({ cwd, specBase, changeName, platformOpts, fil
|
|
|
604
812
|
|
|
605
813
|
// 执行根:quick=会话创建根(防 cd 漂移);full-flow=resolveDiffRoot 共享口径
|
|
606
814
|
let root = cwd
|
|
607
|
-
const sb = specBase || join(cwd, '.sillyspec')
|
|
608
815
|
if (result.mode === 'quick') {
|
|
609
816
|
const located = locateQuickSessionGuard(cwd, changeName)
|
|
610
817
|
if (located) root = dirname(located.specBase)
|
|
@@ -613,7 +820,6 @@ export async function getFileDiff({ cwd, specBase, changeName, platformOpts, fil
|
|
|
613
820
|
root = resolveDiffRoot(sb, changeName, form, cwd)
|
|
614
821
|
}
|
|
615
822
|
|
|
616
|
-
const norm = toPosix(filePath)
|
|
617
823
|
const r = safeGit(root, ['diff', '--no-color', baseRef, '--', norm], { timeout: 30 * 1000 })
|
|
618
824
|
if (r.error) {
|
|
619
825
|
return { ok: false, mode: result.mode, root, baseRef, anchorLabel, diff: null, note: `git diff 执行失败: ${r.error}` }
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -229,7 +229,7 @@ export const definition = {
|
|
|
229
229
|
1. 简单项目:几句话整体描述
|
|
230
230
|
2. 复杂项目:按模块/Phase 分段展示,每段 200-300 字
|
|
231
231
|
3. 展示完整设计方案(不要逐段停顿,一次性展示)
|
|
232
|
-
4. 确认变更名(格式:\`YYYY-MM-DD-<简短描述>\`,例如 \`2026-05-13-user-auth
|
|
232
|
+
4. 确认变更名(格式:\`YYYY-MM-DD-<简短描述>\`,例如 \`2026-05-13-user-auth\`——CLI 已强制:新建/改名不带日期前缀直接 exit 2)
|
|
233
233
|
5. 按「HTML 原型分级」生成原型或写出跳过原因(二选一必填,见下)
|
|
234
234
|
6. 暂停等待用户确认或修改意见
|
|
235
235
|
|
|
@@ -263,7 +263,7 @@ export const definition = {
|
|
|
263
263
|
|
|
264
264
|
### 注意
|
|
265
265
|
- 不要一次输出大段文字,按模块/Phase 分段
|
|
266
|
-
- 变更名必须以当天日期开头(YYYY-MM-DD
|
|
266
|
+
- 变更名必须以当天日期开头(YYYY-MM-DD-),后跟英文短横线分隔的简短描述(CLI 强制:不合规 exit 2)`,
|
|
267
267
|
outputHint: '用户确认的设计方案',
|
|
268
268
|
optional: false
|
|
269
269
|
},
|
|
@@ -342,7 +342,7 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
|
|
|
342
342
|
|
|
343
343
|
### 操作
|
|
344
344
|
1. 确认变更目录存在:\`mkdir -p {SPEC_ROOT}/changes/<change-name>\`(Windows 用 \`mkdir {SPEC_ROOT}/changes\\<变更名>\` 或 PowerShell \`New-Item -ItemType Directory -Force -Path {SPEC_ROOT}/changes/<change-name>\`)
|
|
345
|
-
- 变更名格式必须为 \`YYYY-MM-DD-<简短描述>\`(如 \`2026-05-13-user-auth
|
|
345
|
+
- 变更名格式必须为 \`YYYY-MM-DD-<简短描述>\`(如 \`2026-05-13-user-auth\`;CLI 强制,不合规 exit 2)
|
|
346
346
|
2. 将确认的设计写入 \`{SPEC_ROOT}/changes/<change-name>/design.md\`——优先跑 \`sillyspec design-init --change <change-name>\` 生成骨架再填散文(决策追踪表已按 decisions.md 当前版本预填,逐行补覆盖点后把「待确认」改「已覆盖」;文件清单表骨架就位);存量手写路径仍合法(design-init 对已存在的 design.md 不覆盖)
|
|
347
347
|
3. 对账整理 \`{SPEC_ROOT}/changes/<change-name>/decisions.md\`(「对话式探索」「提出方案」步已按增量规则随答落盘,本步做对账而非从零写入):
|
|
348
348
|
- decisions.md 是本次变更的决策台账,不是长期术语表
|