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/package.json
CHANGED
package/src/config-schema.js
CHANGED
|
@@ -165,6 +165,14 @@ export const LOCAL_YAML_SCHEMA = {
|
|
|
165
165
|
{ path: 'decisions.behind_threshold', type: 'integer', optional: true, status: 'live', readers: ['readDecisionRulesConfig (src/docs-check.js)'], desc: '决策 behind 复核阈值(锚定模块源码在「最近确认」commit 后的前进数),缺省 10;超阈报「决策待复核」提示。', example: '10' },
|
|
166
166
|
],
|
|
167
167
|
},
|
|
168
|
+
{
|
|
169
|
+
id: 'friction_hint',
|
|
170
|
+
title: '摩擦提示开关',
|
|
171
|
+
note: 'quick/verify 收尾的摩擦信号计数提示配置(friction-signal-hint)。读键按「存在则读、不存在用缺省」容错:读失败与未配置同兜底为默认开。',
|
|
172
|
+
keys: [
|
|
173
|
+
{ path: 'friction_hint.enabled', type: 'boolean', optional: true, status: 'live', readers: ['readFrictionHintEnabled (src/friction-tally.js)'], desc: '摩擦提示开关,默认 true;设 false 关闭摩擦提示(gate 回滚/验证失败/审查打回的收尾 advisory 与计数落盘全停,.runtime 零写入)。', example: 'true' },
|
|
174
|
+
],
|
|
175
|
+
},
|
|
168
176
|
],
|
|
169
177
|
};
|
|
170
178
|
|
|
@@ -294,6 +302,11 @@ test_strategy: full
|
|
|
294
302
|
decisions:
|
|
295
303
|
behind_threshold: 10
|
|
296
304
|
|
|
305
|
+
# ── 摩擦提示开关(quick/verify 收尾的摩擦信号一行 advisory:gate 回滚/验证失败/审查打回计数)──
|
|
306
|
+
# 默认 true 开启;false 一键全关(计数与提示双停,.runtime 零写入)
|
|
307
|
+
friction_hint:
|
|
308
|
+
enabled: true
|
|
309
|
+
|
|
297
310
|
# ── 预存失败豁免清单(变更前就失败的测试行;务必定期复核,防误豁免真实失败)──
|
|
298
311
|
known_failures:
|
|
299
312
|
- tests/some-old-stable-failure
|
package/src/decision-distill.js
CHANGED
|
@@ -25,7 +25,10 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
|
|
|
25
25
|
import { basename, dirname, join } from 'node:path'
|
|
26
26
|
|
|
27
27
|
/** FR-02:有实现影响的五类 type(scope 与未知 type 的 confirmed/accepted 不入选) */
|
|
28
|
-
|
|
28
|
+
// feasibility 补录(2026-09-11 驾驭第十二批,本仓归档 2026-08-08-concurrent-write-preflight 实证:
|
|
29
|
+
// 8 条 feasibility+accepted 决策因五类集漏选从未入知识库)——可行性裁决是设计决策(技术选型/
|
|
30
|
+
// 可行 vs 不可行),与五类同格;排除集本意只针对 scope(范围裁决不入库)。
|
|
31
|
+
const IMPLEMENTED_TYPES = new Set(['architecture', 'compatibility', 'boundary', 'definition', 'process', 'feasibility', 'consistency'])
|
|
29
32
|
/** FR-02:implemented 入选的 status(superseded 等其余状态不入选) */
|
|
30
33
|
const IMPLEMENTED_STATUSES = new Set(['confirmed', 'accepted'])
|
|
31
34
|
/** 三级兜底终点域(仍不中归此;域文件由本模块自管) */
|
|
@@ -116,7 +119,10 @@ export function parseDecisions(changeDir) {
|
|
|
116
119
|
// ①标题式(模板正统):`## D-001@v2 标题`,正文行 `- 字段:值`
|
|
117
120
|
// ②扁平列表式(agent 实际产出):`- D-001@v2:标题` 或 `- D-001@v2 | 状态:implemented | 模块域:x`
|
|
118
121
|
// ——行内 | 分段即字段;后续缩进子项 ` - 字段:值` 同样进字段
|
|
119
|
-
|
|
122
|
+
// 标题级放宽 ^#{2,4}(坑 decision-heading-h3-colon-variant,2026-09-11 驾驭第十二批,
|
|
123
|
+
// 本仓 2026-08-08-concurrent-write-preflight 实证:### D-001@v1: 标题 形态 8 条 accepted
|
|
124
|
+
// 决策未入知识库)——agent 落盘的标题级漂移(##/###/####)与标题前导冒号均容收
|
|
125
|
+
const h = line.match(/^#{2,4}\s+(D-\d+)(?:@v(\d+))?\s*[::]?\s*(.*)$/)
|
|
120
126
|
if (h) {
|
|
121
127
|
flush()
|
|
122
128
|
const version = h[2] ? parseInt(h[2], 10) : 1
|
package/src/docs-check.js
CHANGED
|
@@ -28,13 +28,16 @@ import { parseModuleMapSimple } from './modules.js'
|
|
|
28
28
|
* 不同设备兄弟仓库位置不同,未配映射时默认跳过本地校验(防跨设备误报);
|
|
29
29
|
* 在本机 .sillyspec/local.yaml 的 docs-check.cross_repo_roots 配
|
|
30
30
|
* `<仓库名>: <本机绝对路径>` 后,走与本地引用完全相同的层1(行号边界)+ 层2(关键词窗口)校验。
|
|
31
|
-
* 正则组:1=仓库名(可选),2=文件,3=start,4=end
|
|
31
|
+
* 正则组:1=仓库名(可选),2=文件,3=start,4=end,5=纯位置锚标记(可选尾 `?`)。
|
|
32
|
+
* 纯位置锚 `file.js:123?`(2026-09-11 用户实证 docs gate 误伤:跨文件引用+反话论述——锚 A
|
|
33
|
+
* 文件而行内反引号 token 全是 B 概念,层 2 必失败,被迫删行号绕开):`?` 显式声明本锚只做
|
|
34
|
+
* 定位不做关键词断言,层 1(存在性+行界)照校——论述语境锚的一等语法,替代行号化绕开。
|
|
32
35
|
* 文件段展开循环形(D-006,2026-09-08-docs-fix-capability):普通段可选 + 零或多个「闭合括号段+普通段」
|
|
33
36
|
* 迭代——支持 Next.js 路由组 (dashboard) 等括号路径;markdown 链接 [t](foo.js:12) 在 `(` 处括号段
|
|
34
37
|
* 要求 `)` 先于 `:` 闭合而失败,回落 foo.js:12 零回归;嵌套 ((x)) 部分提取(与旧行为一致)。
|
|
35
38
|
* ⚠ 不用原子序列 (?:A+|B+)+——经典嵌套量词 ReDoS(Grill 实证 n=30 长 token 73.8s),
|
|
36
39
|
* 展开循环每次迭代必含括号段 → 划分唯一 → 线性。 */
|
|
37
|
-
const REF_RE = /(?:repo:\/\/([A-Za-z0-9_.\-]+)\/)?([A-Za-z0-9_.\-\/]*(?:\([A-Za-z0-9_.\-\/]+\)[A-Za-z0-9_.\-\/]*)*\.(?:js|mjs|cjs|ts|tsx|jsx|py|java|go)):(\d+)(?:-(\d+))?/g
|
|
40
|
+
const REF_RE = /(?:repo:\/\/([A-Za-z0-9_.\-]+)\/)?([A-Za-z0-9_.\-\/]*(?:\([A-Za-z0-9_.\-\/]+\)[A-Za-z0-9_.\-\/]*)*\.(?:js|mjs|cjs|ts|tsx|jsx|py|java|go)):(\d+)(?:-(\d+))?(\?)?/g
|
|
38
41
|
|
|
39
42
|
/** 缺省扫描范围:docs/ + .sillyspec/docs/(scan/modules 产物同是文档,失效即该暴露;2026-08-16 用户裁决改缺省,见 doc-consistency-debt.md §八)
|
|
40
43
|
* + .sillyspec/changes/ + .sillyspec/knowledge/(P1-1,noai-ir-roadmap §4:最活跃、最易产生
|
|
@@ -127,6 +130,7 @@ export function collectDocRefs(md) {
|
|
|
127
130
|
file: m[2],
|
|
128
131
|
start: parseInt(m[3], 10),
|
|
129
132
|
end: m[4] !== undefined ? parseInt(m[4], 10) : parseInt(m[3], 10),
|
|
133
|
+
kwSkip: m[5] === '?', // 纯位置锚(file.js:123?):层1 照校、层2 跳过
|
|
130
134
|
docLine: line,
|
|
131
135
|
})
|
|
132
136
|
}
|
|
@@ -312,7 +316,7 @@ export function collectInvalidDocRefs(projectRoot, docEntries) {
|
|
|
312
316
|
invalid.push({ doc: name, ref: ref.ref, reason: '文件不存在' })
|
|
313
317
|
continue
|
|
314
318
|
}
|
|
315
|
-
const tokens = extractExpectedTokensFromLine(docLines[ref.docLine - 1] || '')
|
|
319
|
+
const tokens = ref.kwSkip ? [] : extractExpectedTokensFromLine(docLines[ref.docLine - 1] || '')
|
|
316
320
|
const infos = candidates.map(abs => ({ lines: readLines(abs, linesCache) }))
|
|
317
321
|
const ok = infos.some(({ lines }) => {
|
|
318
322
|
if (!lines || lines.length === 0) return false
|
|
@@ -588,7 +592,7 @@ export function runDocsCheck(opts) {
|
|
|
588
592
|
continue
|
|
589
593
|
}
|
|
590
594
|
// 多候选宽容:逐候选跑层1+层2,任一全过即通过
|
|
591
|
-
const tokens = keywordAssert
|
|
595
|
+
const tokens = (keywordAssert && !r.kwSkip)
|
|
592
596
|
? extractExpectedTokensFromLine(mdLines[r.docLine - 1] || '')
|
|
593
597
|
: []
|
|
594
598
|
const candidateFails = []
|
|
@@ -613,7 +617,7 @@ export function runDocsCheck(opts) {
|
|
|
613
617
|
const repoPrefix = r.repo ? `跨仓 repo://${r.repo} → ` : ''
|
|
614
618
|
invalid.push({
|
|
615
619
|
doc: docRel, docLine: r.docLine, ref: r.ref,
|
|
616
|
-
reason: repoPrefix + (candidateFails.length > 1 ? `多候选全失败 → ${candidateFails.join(' | ')}` : candidateFails[0]),
|
|
620
|
+
reason: repoPrefix + (candidateFails.length > 1 ? `多候选全失败 → ${candidateFails.join(' | ')}` : candidateFails[0]) + (candidateFails.some(f => f.includes('关键词缺失')) ? '(跨文件引用/论述语境的纯位置锚:行号后加 ? 跳过关键词断言,层1 行界仍校验——勿删行号)' : ''),
|
|
617
621
|
// 建议行号(--suggest):token 在首个候选文件的全量命中行,供人工确认改锚——不自动改文件
|
|
618
622
|
suggest: suggestLines(candidates, tokens, linesCache),
|
|
619
623
|
// 修复分类(--fix 判定依据):全量候选 token 命中打分——唯一命中或选优严格领先可自动重锚
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* friction-tally.js — 摩擦信号计数数据层(friction-signal-hint Phase 1)
|
|
3
|
+
*
|
|
4
|
+
* 定位:gate 失败回滚 / verify 实测失败 / 审查打回三类摩擦事件全部流经 CLI 自身
|
|
5
|
+
* 状态机,但此前没有任何计数与消费——是否值得 postmortem 全凭 agent 自觉判断。
|
|
6
|
+
* 本模块用确定性代码做结构化计数(立场照抄 teamai-cli 摩擦触发器:顺利会话零
|
|
7
|
+
* 打扰、每次收尾最多提示一次、可配置关闭),收尾时计数非零才由调用方输出一行
|
|
8
|
+
* advisory,引导按 现象/根因/护栏/证据 补 postmortem。
|
|
9
|
+
*
|
|
10
|
+
* 落点红线(D-002/R-04):计数文件只落 .runtime 树(平台同步排除区)——quick
|
|
11
|
+
* 会话落 quick-sessions/<sessionId>/friction-tally.json(session 目录收尾整体删除,
|
|
12
|
+
* 天然二次保险清零),真实变更落 <runtimeRoot>/friction-tally-<changeName>.json
|
|
13
|
+
* (归档时由 pruneArchivedChangeRuntime 一并清理)。**永不落 changes/ 目录**——
|
|
14
|
+
* 变更目录上平台同步,计数是本机行为数据,污染他机即事故。路径解析统一走
|
|
15
|
+
* shared.js 的 resolveRuntimeRoot/resolveQuickSessionsDir(比 verify-lint-tally
|
|
16
|
+
* 的 specBase 直拼更对齐平台模式,Grill CC-09)。
|
|
17
|
+
*
|
|
18
|
+
* 隐私红线(D-005):落盘字段只含 count/lastAt/at/type/detail——type ∈ 三值枚举、
|
|
19
|
+
* detail ∈ 预定义标签集(gate 来源/测试类型),永不接触提示词与对话原文。
|
|
20
|
+
*
|
|
21
|
+
* 静默降级立场(同 verify-lint-tally「计数器不许反向阻断」):读写全程 try/catch,
|
|
22
|
+
* 任何异常(只读目录/损坏 JSON/非法路径)吞掉返回 null/空 hint——埋点都在 gate
|
|
23
|
+
* 回滚、verify 失败这些已出错路径上,计数自身再抛异常只会火上浇油。本模块自身
|
|
24
|
+
* 不打印任何输出,提示何时打印由调用方决定。
|
|
25
|
+
*/
|
|
26
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from 'fs'
|
|
27
|
+
import { dirname, join } from 'path'
|
|
28
|
+
import { writeAtomicSync } from './fs-atomic.js'
|
|
29
|
+
import { resolveRuntimeRoot, resolveQuickSessionsDir } from './run/shared.js'
|
|
30
|
+
|
|
31
|
+
/** 摩擦事件类型枚举(D-004):审查打回走专属 review_rejected,不与 gate_rollback 重复计 */
|
|
32
|
+
const FRICTION_TYPES = new Set(['gate_rollback', 'verify_run_failed', 'review_rejected'])
|
|
33
|
+
|
|
34
|
+
/** history 截尾上限(同 verify-lint-tally 的 20 条口径——台账防病态体量,不承载完整审计) */
|
|
35
|
+
const FRICTION_HISTORY_CAP = 20
|
|
36
|
+
|
|
37
|
+
/** quick 会话 id 形态:quick-<8 位 hex>(stage.js 建 session 目录的同一命名约定) */
|
|
38
|
+
const QUICK_SESSION_ID_RE = /^quick-[0-9a-f]{8}$/i
|
|
39
|
+
|
|
40
|
+
/** 提示文案的类型→中文标签映射(数组序即输出枚举序,固定 gate 回滚→验证失败→审查打回) */
|
|
41
|
+
const FRICTION_LABELS = [
|
|
42
|
+
['gate_rollback', 'gate 回滚'],
|
|
43
|
+
['verify_run_failed', '验证失败'],
|
|
44
|
+
['review_rejected', '审查打回'],
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* specBase 模块内部推导(D-006 CC-04):gates.js 的 rollbackCompletionAndReturn
|
|
49
|
+
* 等埋点签名里没有 specBase,调用方只传 cwd/platformOpts。推导序与 complete.js:125
|
|
50
|
+
* 收尾落盘同序(specRoot(平台) > specDriftAnchor(worktree 漂移锚定主仓) >
|
|
51
|
+
* cwd/.sillyspec(本地)),否则 quick 会话计到 worktree 副本、收尾从主仓读不到。
|
|
52
|
+
*/
|
|
53
|
+
function deriveSpecBase(cwd, platformOpts) {
|
|
54
|
+
return platformOpts?.specRoot || platformOpts?.specDriftAnchor || join(cwd || process.cwd(), '.sillyspec')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* changeName 安全校验:必须是单段文件名成分。含路径分隔符或「.」「..」会在拼
|
|
59
|
+
* friction-tally-<changeName>.json 时逃出 .runtime 树,违反 D-002 落点红线。
|
|
60
|
+
*/
|
|
61
|
+
function isSafeChangeSegment(name) {
|
|
62
|
+
return typeof name === 'string' && name !== '' && name !== '.' && name !== '..' && !/[\\/]/.test(name)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 计数文件路径的单一路由出口(D-002/R-04:路由单一出口 + 单测断言两类路径都在
|
|
67
|
+
* .runtime 树内):quick-<8hex> 会话形态 → <sessionsDir>/<changeName>/friction-tally.json;
|
|
68
|
+
* 真实变更 → <runtimeRoot>/friction-tally-<changeName>.json。返回 null = 无有效
|
|
69
|
+
* 路径,调用方按 no-op 处理(空/非法 changeName 不记不消)。
|
|
70
|
+
*/
|
|
71
|
+
function frictionTallyPath({ cwd, platformOpts, changeName }) {
|
|
72
|
+
if (!isSafeChangeSegment(changeName)) return null
|
|
73
|
+
const specBase = deriveSpecBase(cwd, platformOpts)
|
|
74
|
+
if (QUICK_SESSION_ID_RE.test(changeName)) {
|
|
75
|
+
return join(resolveQuickSessionsDir(platformOpts, specBase), changeName, 'friction-tally.json')
|
|
76
|
+
}
|
|
77
|
+
return join(resolveRuntimeRoot(platformOpts, specBase), `friction-tally-${changeName}.json`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** YAML 布尔值宽松解析:仅显式 false(含引号形态)为关,其余值一律默认开(R-05) */
|
|
81
|
+
function parseEnabledValue(raw) {
|
|
82
|
+
const v = String(raw || '').trim().replace(/^['"]|['"]$/g, '')
|
|
83
|
+
return v.toLowerCase() !== 'false'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 读 local.yaml 的 friction_hint.enabled(R-05:读失败=默认开,与「未配置=默认
|
|
88
|
+
* true」走同一条兜底路径——配置读取异常不该把提示误判为关)。
|
|
89
|
+
* 不引 yaml 依赖,行/正则解析(先例:extractTestCommand/extractLintCommand 同
|
|
90
|
+
* 风格);兼容嵌套(friction_hint:\n enabled: false)与 flat
|
|
91
|
+
* (friction_hint.enabled: false)两种写法;行尾先归一(坑
|
|
92
|
+
* verify-modules-crlf-blanket-fallback:Windows CRLF 残留致行式正则整体失配)。
|
|
93
|
+
* @param {string} specBase - spec 根(local.yaml 所在目录)
|
|
94
|
+
* @returns {boolean} false 仅当显式配了 false;缺键/文件缺失/解析异常/其它值 → true
|
|
95
|
+
*/
|
|
96
|
+
function readFrictionHintEnabled(specBase) {
|
|
97
|
+
try {
|
|
98
|
+
const yamlPath = join(specBase, 'local.yaml')
|
|
99
|
+
if (!existsSync(yamlPath)) return true
|
|
100
|
+
const text = String(readFileSync(yamlPath, 'utf8')).replace(/\r\n?/g, '\n')
|
|
101
|
+
let inSection = false
|
|
102
|
+
let sectionIndent = -1
|
|
103
|
+
for (const line of text.split('\n')) {
|
|
104
|
+
// flat 形态优先:键名带点,不与嵌套段头冲突
|
|
105
|
+
const flat = line.match(/^\s*friction_hint\.enabled\s*:\s*(.*?)\s*(?:#.*)?$/)
|
|
106
|
+
if (flat) return parseEnabledValue(flat[1])
|
|
107
|
+
const section = line.match(/^(\s*)friction_hint\s*:\s*(?:#.*)?$/)
|
|
108
|
+
if (section) {
|
|
109
|
+
inSection = true
|
|
110
|
+
sectionIndent = section[1].length
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
if (!inSection) continue
|
|
114
|
+
if (line.trim() === '' || line.trim().startsWith('#')) continue
|
|
115
|
+
// 缩进回落到段头层级(或更浅)= friction_hint 段结束,之后的 enabled 不属于它
|
|
116
|
+
if (/^\s*/.exec(line)[0].length <= sectionIndent) {
|
|
117
|
+
inSection = false
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
const kv = line.match(/^\s*enabled\s*:\s*(.*?)\s*(?:#.*)?$/)
|
|
121
|
+
if (kv) return parseEnabledValue(kv[1])
|
|
122
|
+
}
|
|
123
|
+
return true
|
|
124
|
+
} catch {
|
|
125
|
+
return true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 读旧计数并归一成 { events, history } 骨架:缺失/损坏 JSON/字段脏型按零计重新
|
|
131
|
+
* 累计(同 recordVerifyLintTally 立场——旁路计数器对脏数据从零重来,不修复不报错)。
|
|
132
|
+
*/
|
|
133
|
+
function readFrictionTally(tallyPath) {
|
|
134
|
+
const tally = { events: {}, history: [] }
|
|
135
|
+
try {
|
|
136
|
+
const raw = JSON.parse(readFileSync(tallyPath, 'utf8'))
|
|
137
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
138
|
+
if (raw.events && typeof raw.events === 'object' && !Array.isArray(raw.events)) tally.events = raw.events
|
|
139
|
+
if (Array.isArray(raw.history)) tally.history = raw.history
|
|
140
|
+
}
|
|
141
|
+
} catch { /* 首记/文件损坏 → 重置重新累计 */ }
|
|
142
|
+
return tally
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 纯渲染(单测用):counts(type→count 平面映射)→ 一行提示文案;空/全零 → null。
|
|
147
|
+
* 只枚举非零段(三类不必同时出现);措辞条件式(「若其中有」)防 agent 为消除
|
|
148
|
+
* 提示而灌水记录(R-03 三重防线之一)。
|
|
149
|
+
*/
|
|
150
|
+
export function renderFrictionHintLine(counts) {
|
|
151
|
+
const segments = []
|
|
152
|
+
for (const [type, label] of FRICTION_LABELS) {
|
|
153
|
+
const n = Number(counts ? counts[type] : null)
|
|
154
|
+
if (Number.isFinite(n) && n > 0) segments.push(`${label} ${n} 次`)
|
|
155
|
+
}
|
|
156
|
+
if (segments.length === 0) return null
|
|
157
|
+
return `🩹 本次会话累计摩擦信号:${segments.join('、')}——若其中有值得沉淀的坑,建议按 现象/根因/护栏/证据 补一条 postmortem(QUICKLOG 条目或正文核对);护栏结论经人工确认后归入 knowledge/known-issues.md`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 记一次摩擦事件。全静默降级:非法类型/无有效路径/enabled=false/读写异常一律
|
|
162
|
+
* 返回 null,绝不抛(埋点位于已出错路径,见文件头「静默降级立场」)。写走
|
|
163
|
+
* writeAtomicSync(计数文件会被收尾进程并发读,半截 JSON 会让 consume 误判全零)。
|
|
164
|
+
* @param {object} p
|
|
165
|
+
* @param {string} [p.cwd] 调用方 cwd(specBase 兜底推导用)
|
|
166
|
+
* @param {object} [p.platformOpts] 平台选项(runtimeRoot/specRoot/specDriftAnchor)
|
|
167
|
+
* @param {string} p.changeName 变更名或 quick 会话 id(空/非法段 → no-op)
|
|
168
|
+
* @param {string} p.type 摩擦类型,必须 ∈ FRICTION_TYPES
|
|
169
|
+
* @param {string} [p.detail] 预定义来源标签(如 verify-test/quick-audit),非字符串落 null
|
|
170
|
+
* @returns {{ type: string, count: number } | null} 落盘成功返回新计数;null = 未落盘
|
|
171
|
+
*/
|
|
172
|
+
export function recordFrictionEvent({ cwd, platformOpts, changeName, type, detail }) {
|
|
173
|
+
try {
|
|
174
|
+
if (!FRICTION_TYPES.has(type)) return null
|
|
175
|
+
const tallyPath = frictionTallyPath({ cwd, platformOpts, changeName })
|
|
176
|
+
if (!tallyPath) return null
|
|
177
|
+
// enabled=false 直通:record/consume 双 no-op,.runtime 零写入(D-003 一键全关)
|
|
178
|
+
if (!readFrictionHintEnabled(deriveSpecBase(cwd, platformOpts))) return null
|
|
179
|
+
const tally = readFrictionTally(tallyPath)
|
|
180
|
+
const at = new Date().toISOString()
|
|
181
|
+
const prev = Number(tally.events[type] && tally.events[type].count)
|
|
182
|
+
const count = (Number.isFinite(prev) ? prev : 0) + 1
|
|
183
|
+
tally.events[type] = { count, lastAt: at }
|
|
184
|
+
tally.history = [
|
|
185
|
+
...tally.history,
|
|
186
|
+
{ at, type, detail: typeof detail === 'string' && detail ? detail : null },
|
|
187
|
+
].slice(-FRICTION_HISTORY_CAP)
|
|
188
|
+
mkdirSync(dirname(tallyPath), { recursive: true })
|
|
189
|
+
writeAtomicSync(tallyPath, JSON.stringify(tally, null, 2))
|
|
190
|
+
return { type, count }
|
|
191
|
+
} catch {
|
|
192
|
+
return null
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 读计数并消费(D-003 清零语义):非零 → { hint: 一行提示, counts } 且删除计数
|
|
198
|
+
* 文件(提示后清零,防同一批摩擦在每个收尾点反复提示);全零/缺文件/读失败/
|
|
199
|
+
* enabled=false → { hint: null, counts: {} }。counts 只含 count>0 的枚举类型
|
|
200
|
+
* (未知类型/脏数据不进提示)。删文件失败不影响 hint 返回——残留计数下次收尾
|
|
201
|
+
* 会再消费一次,advisory 容忍重复,不因删失败丢提示。
|
|
202
|
+
* @returns {{ hint: string | null, counts: Record<string, number> }}
|
|
203
|
+
*/
|
|
204
|
+
export function consumeFrictionHint({ cwd, platformOpts, changeName }) {
|
|
205
|
+
try {
|
|
206
|
+
const tallyPath = frictionTallyPath({ cwd, platformOpts, changeName })
|
|
207
|
+
if (!tallyPath || !existsSync(tallyPath)) return { hint: null, counts: {} }
|
|
208
|
+
if (!readFrictionHintEnabled(deriveSpecBase(cwd, platformOpts))) return { hint: null, counts: {} }
|
|
209
|
+
const tally = readFrictionTally(tallyPath)
|
|
210
|
+
const counts = {}
|
|
211
|
+
for (const type of FRICTION_TYPES) {
|
|
212
|
+
const n = Number(tally.events[type] && tally.events[type].count)
|
|
213
|
+
if (Number.isFinite(n) && n > 0) counts[type] = n
|
|
214
|
+
}
|
|
215
|
+
if (Object.keys(counts).length === 0) return { hint: null, counts }
|
|
216
|
+
try { rmSync(tallyPath, { force: true }) } catch { /* 删失败不吞提示 */ }
|
|
217
|
+
return { hint: renderFrictionHintLine(counts), counts }
|
|
218
|
+
} catch {
|
|
219
|
+
return { hint: null, counts: {} }
|
|
220
|
+
}
|
|
221
|
+
}
|
package/src/index.js
CHANGED
|
@@ -198,7 +198,7 @@ async function main() {
|
|
|
198
198
|
|
|
199
199
|
// E22:重路径统一加载(轻路径 --version/help 已早退,未付此税)。
|
|
200
200
|
const { ProgressManager, resolvePlatformSpecDir, resolvePlatformOpts } = await import('./progress.js');
|
|
201
|
-
const { didYouMean, assertSafeChangeName, resolveSpecDir, detectWorktreeSpecDrift } = await import('./run/shared.js');
|
|
201
|
+
const { didYouMean, assertSafeChangeName, assertDatedChangeName, resolveSpecDir, detectWorktreeSpecDrift } = await import('./run/shared.js');
|
|
202
202
|
|
|
203
203
|
// 解析全局选项
|
|
204
204
|
let json = false;
|
|
@@ -3524,9 +3524,12 @@ checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";
|
|
|
3524
3524
|
process.exit(1);
|
|
3525
3525
|
}
|
|
3526
3526
|
// F6 路径穿越消毒:renameChange 会 mv changes/<old> → changes/<new>,名含 ../ 会逃出 changes/。
|
|
3527
|
+
// 日期前缀门禁(brainstorm step6 规则 CLI 化):改名产生的新名必须 YYYY-MM-DD-<描述>
|
|
3528
|
+
// (2026-09-11 实证:friction-signal-hint 重命名丢前缀被静默物化)。
|
|
3527
3529
|
try {
|
|
3528
3530
|
assertSafeChangeName(oldName, '旧变更名');
|
|
3529
3531
|
assertSafeChangeName(newName, '新变更名');
|
|
3532
|
+
assertDatedChangeName(newName, '新变更名');
|
|
3530
3533
|
} catch (e) {
|
|
3531
3534
|
console.error(`❌ ${e.message}`);
|
|
3532
3535
|
process.exit(2);
|
|
@@ -280,6 +280,8 @@ export class ChangeRegistry {
|
|
|
280
280
|
console.warn('⚠️ renameChange: 新旧名称相同,跳过');
|
|
281
281
|
return;
|
|
282
282
|
}
|
|
283
|
+
// 注:新名日期前缀门禁只在 CLI 边界强制(index.js change-rename 入口)——renameChange 是
|
|
284
|
+
// 库函数,测试/平台工具合法用任意名(2026-09-11 决策)。
|
|
283
285
|
const db = this.pm._ensureDB(cwd);
|
|
284
286
|
// 检查旧名是否存在
|
|
285
287
|
const existing = db.transaction(() => {
|
package/src/progress.js
CHANGED
|
@@ -992,6 +992,9 @@ export class ProgressManager {
|
|
|
992
992
|
}
|
|
993
993
|
// quick 会话 id(quick-<uuid8>,见 run.js QUICK_SID_RE)只作 progress 的跨进程 session key,
|
|
994
994
|
// 进度存 SQL 不需要实体 change 目录——跳过避免 changes/quick-<uuid>/ 空目录残留。
|
|
995
|
+
// 注:日期前缀门禁(assertDatedChangeName)只在 CLI 边界强制(run --change / change-rename,
|
|
996
|
+
// 见 run/command.js + index.js)——initChange 是库函数,测试/平台工具合法用任意名建 fixture,
|
|
997
|
+
// 不在此设门(2026-09-11 决策,避免 ~30 处测试 fixture 非日期名被拦)。
|
|
995
998
|
if (!/^quick-[0-9a-f]{8}$/.test(changeName)) {
|
|
996
999
|
this._ensureChangeDir(cwd, changeName);
|
|
997
1000
|
}
|
package/src/run/command.js
CHANGED
|
@@ -23,7 +23,7 @@ import { basename, join, resolve, dirname } from 'node:path'
|
|
|
23
23
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync, unlinkSync } from 'node:fs'
|
|
24
24
|
import { randomBytes, randomUUID } from 'node:crypto'
|
|
25
25
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
26
|
-
import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, isTempResidueSpecRoot, PLATFORM_MANAGED_FILENAME, warnSelfRefPointerOnce } from './shared.js'
|
|
26
|
+
import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, assertDatedChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, isTempResidueSpecRoot, PLATFORM_MANAGED_FILENAME, warnSelfRefPointerOnce } from './shared.js'
|
|
27
27
|
import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
28
28
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
29
29
|
import { completeStep, skipStep, waitStep, continueStep, synthesizeStepOutput } from './complete.js'
|
|
@@ -1046,6 +1046,22 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1046
1046
|
process.exit(2) // 用法/环境错(目标变更不在当前库)→ exit 2
|
|
1047
1047
|
}
|
|
1048
1048
|
}
|
|
1049
|
+
// ── 变更名日期前缀门禁(brainstorm step6 规则 CLI 化,2026-09-11 实证 friction-signal-hint)──
|
|
1050
|
+
// 格式 YYYY-MM-DD-<简短描述> 此前只是 prompt 约束,agent 自拟名/重命名丢前缀时 CLI 照单
|
|
1051
|
+
// 全收静默物化。只拦「净新建」:DB 无行(!progress 已证)+ changes/(含 archive/)无目录;
|
|
1052
|
+
// 已物化存量(含归档无前缀旧名 auto-flow-optimization 等)不追诉,照常自愈初始化。
|
|
1053
|
+
// quick 阶段豁免:其 changeName 是会话 key(quick-<8hex>),非变更名。
|
|
1054
|
+
if (changeName && stageName !== 'quick' && !/^quick-[0-9a-f]{8}$/.test(changeName)
|
|
1055
|
+
&& !existsSync(join(specBase, 'changes', changeName))
|
|
1056
|
+
&& !existsSync(join(specBase, 'changes', 'archive', changeName))) {
|
|
1057
|
+
try {
|
|
1058
|
+
assertDatedChangeName(changeName)
|
|
1059
|
+
} catch (e) {
|
|
1060
|
+
console.error(`❌ ${e.message}`)
|
|
1061
|
+
console.error(` 重试:sillyspec run ${stageName} --change <YYYY-MM-DD-简短描述>`)
|
|
1062
|
+
process.exit(2) // 用法错(新变更名格式非法)→ exit 2
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1049
1065
|
// 如果指定了变更名或有变更目录,自动初始化变更的 progress
|
|
1050
1066
|
const autoChange = changeName || resolveChangeNameAuto(cwd, specRoot)
|
|
1051
1067
|
if (autoChange) {
|
|
@@ -35,6 +35,7 @@ import { validateQuickResult, allocateQuicklogEntry, appendQuicklogEntryWithId,
|
|
|
35
35
|
import { getRule } from '../stage-contract-spec.js'
|
|
36
36
|
import { archiveDestDirName } from '../stage-contract.js'
|
|
37
37
|
import { collectNumstatByPath } from '../scope-audit.js'
|
|
38
|
+
import { recordFrictionEvent, consumeFrictionHint } from '../friction-tally.js'
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* 清洗项目名:只保留 ASCII 字母/数字/横线/下划线/点,过滤中文和特殊字符。
|
|
@@ -170,6 +171,13 @@ export function pruneArchivedChangeRuntime(runtimeRoot, changeName) {
|
|
|
170
171
|
if (existsSync(pathspec)) gone(pathspec)
|
|
171
172
|
} catch {}
|
|
172
173
|
|
|
174
|
+
// friction tally(friction-signal-hint FR-05):摩擦计数随变更终态一并回收——真实变更的
|
|
175
|
+
// friction-tally-<change>.json 落 runtimeRoot,变更归档/删除后无读者,残留即孤儿文件。
|
|
176
|
+
try {
|
|
177
|
+
const fp = join(runtimeRoot, `friction-tally-${changeName}.json`)
|
|
178
|
+
if (existsSync(fp)) gone(fp)
|
|
179
|
+
} catch {}
|
|
180
|
+
|
|
173
181
|
try {
|
|
174
182
|
const execDir = join(runtimeRoot, 'execute-runs')
|
|
175
183
|
if (existsSync(execDir)) {
|
|
@@ -1056,6 +1064,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1056
1064
|
steps[currentIdx].status = 'pending'
|
|
1057
1065
|
steps[currentIdx].completedAt = null
|
|
1058
1066
|
if (outputText) steps[currentIdx].output = null
|
|
1067
|
+
// 摩擦计数(friction-signal-hint task-04):quick 审计被拦也是摩擦信号——exit 前记
|
|
1068
|
+
// 一笔(changeName=quick 会话 id,tally 落 session 目录,成功收尾 consume 后随目录清理)。
|
|
1069
|
+
recordFrictionEvent({ cwd, changeName, platformOpts, type: 'gate_rollback', detail: 'quick-audit' })
|
|
1059
1070
|
process.exit(1)
|
|
1060
1071
|
}
|
|
1061
1072
|
// P0-2(2026-09-02 跨 agent 工单):触及 src/test 的 quick --done 内置 test+lint
|
|
@@ -1076,6 +1087,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1076
1087
|
steps[currentIdx].status = 'pending'
|
|
1077
1088
|
steps[currentIdx].completedAt = null
|
|
1078
1089
|
if (outputText) steps[currentIdx].output = null
|
|
1090
|
+
// 摩擦计数(friction-signal-hint task-04):test/lint 实测门禁拦下同记摩擦——exit 前
|
|
1091
|
+
// 落 session tally,与 quick-audit 同款静默降级。
|
|
1092
|
+
recordFrictionEvent({ cwd, changeName, platformOpts, type: 'gate_rollback', detail: 'quick-test-lint' })
|
|
1079
1093
|
process.exit(1)
|
|
1080
1094
|
}
|
|
1081
1095
|
progress.lastQuickReview = review
|
|
@@ -1223,6 +1237,10 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1223
1237
|
}
|
|
1224
1238
|
if (review?.docsCheckHint && review.docsCheckHint.invalid > 0) {
|
|
1225
1239
|
auditNotes.push(`📎 文档引用失效:${review.docsCheckHint.invalid}/${review.docsCheckHint.total} 处 file:line 失效(sillyspec docs check 可复现)`)
|
|
1240
|
+
// 逐条指名(2026-09-11 同 quick-audit 侧:门禁输出直接带文件:行号,封顶 5 保持注记紧凑)
|
|
1241
|
+
for (const i of (review.docsCheckHint.invalidRefs || []).slice(0, 5)) {
|
|
1242
|
+
auditNotes.push(` ❌ [${i.doc}:${i.docLine}] ${i.ref} → ${i.reason}`)
|
|
1243
|
+
}
|
|
1226
1244
|
// 行号漂移自动重锚(2026-09-08 用户反馈②:活文档 file:line 硬编码随任何插入失效,
|
|
1227
1245
|
// 每回手跑 docs check --fix 是机械活):autoReanchorDocRefs = --fix 主链路编程化封装
|
|
1228
1246
|
// (fixable 唯一/优选命中才改 + 定点替换 + 同口径回执),只作用于本次改动的文档。
|
|
@@ -1266,6 +1284,53 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1266
1284
|
softFiles,
|
|
1267
1285
|
})
|
|
1268
1286
|
console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
|
|
1287
|
+
// 范围快照/patch 审计级落盘(quick-359a48f1):quicklog/patches/<qlId>.json+.patch——按
|
|
1288
|
+
// ql-ID 对齐 QUICKLOG 条目(平台按条目抓取),json 冗余 sessionId 供 guard 清理后记录态
|
|
1289
|
+
// 反查(computeQuickAudit → findQuickPatchRecord 同链)。patch 为 --done 时点全量冻结
|
|
1290
|
+
// (HEAD 未提交窗口,untracked 自拼 hunk)。fail-soft:失败只提示,不阻断收尾。
|
|
1291
|
+
try {
|
|
1292
|
+
const { computeChangeScopeAudit, collectNumstatByPath, buildFrozenPatch } = await import('../scope-audit.js')
|
|
1293
|
+
// changeName == sessionId(handleQuickStageCompletion 作用域内,§4.6 注释同款约定)
|
|
1294
|
+
const snapResult = await computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts, collectPatch: true })
|
|
1295
|
+
let quickRows = snapResult && snapResult.mode === 'quick' && Array.isArray(snapResult.rows)
|
|
1296
|
+
? snapResult.rows.filter(r => r && r.attribution !== 'undeclared')
|
|
1297
|
+
: null
|
|
1298
|
+
let frozenPatch = snapResult ? snapResult.frozenPatch : undefined
|
|
1299
|
+
// 窗口空兜底(baseline 会话常态):--files 声明文件是会话启动时预存脏(audit baseline
|
|
1300
|
+
// 剔除后窗口空,quickRows 空),但声明即归属——改用声明文件集构造 rows + patch(行数按
|
|
1301
|
+
// HEAD 未提交窗口采集,同口径)。
|
|
1302
|
+
if ((!quickRows || quickRows.length === 0) && Array.isArray(guard?.allowedFiles) && guard.allowedFiles.length > 0) {
|
|
1303
|
+
const sessionRoot = dirname(specBase)
|
|
1304
|
+
const declaredPosix = guard.allowedFiles.map(f => String(f).replace(/\\/g, '/')).filter(Boolean)
|
|
1305
|
+
const stats = collectNumstatByPath(sessionRoot, declaredPosix, { baseRef: 'HEAD' })
|
|
1306
|
+
quickRows = declaredPosix.filter(f => stats.has(f) || existsSync(join(sessionRoot, f)))
|
|
1307
|
+
.map(f => {
|
|
1308
|
+
const st = stats.get(f) || { additions: null, deletions: null, kind: existsSync(join(sessionRoot, f)) ? 'modified' : 'deleted' }
|
|
1309
|
+
return { path: f, declared: true, additions: st.additions, deletions: st.deletions, kind: st.kind, attribution: 'declared' }
|
|
1310
|
+
})
|
|
1311
|
+
frozenPatch = buildFrozenPatch(sessionRoot, quickRows.map(r => r.path), { baseRef: 'HEAD' })
|
|
1312
|
+
}
|
|
1313
|
+
if (quickRows && quickRows.length > 0) {
|
|
1314
|
+
const patchesDir = join(specBase, 'quicklog', 'patches')
|
|
1315
|
+
mkdirSync(patchesDir, { recursive: true })
|
|
1316
|
+
const { ...snapRest } = snapResult || {}
|
|
1317
|
+
delete snapRest.frozenPatch
|
|
1318
|
+
writeFileSync(join(patchesDir, `${qlId}.json`), JSON.stringify({
|
|
1319
|
+
...snapRest,
|
|
1320
|
+
rows: quickRows,
|
|
1321
|
+
totals: { files: quickRows.length, additions: quickRows.reduce((n, r) => n + (Number.isFinite(r.additions) ? r.additions : 0), 0), deletions: quickRows.reduce((n, r) => n + (Number.isFinite(r.deletions) ? r.deletions : 0), 0) },
|
|
1322
|
+
qlId,
|
|
1323
|
+
sessionId: changeName,
|
|
1324
|
+
savedAt: new Date().toISOString(),
|
|
1325
|
+
}, null, 2) + '\n')
|
|
1326
|
+
if (typeof frozenPatch === 'string' && frozenPatch) {
|
|
1327
|
+
writeFileSync(join(patchesDir, `${qlId}.patch`), frozenPatch.endsWith('\n') ? frozenPatch : frozenPatch + '\n')
|
|
1328
|
+
}
|
|
1329
|
+
console.log(`📦 范围快照已落 quicklog/patches/${qlId}.json${typeof frozenPatch === 'string' && frozenPatch ? ' + .patch(--done 时点冻结)' : ''}`)
|
|
1330
|
+
}
|
|
1331
|
+
} catch (e) {
|
|
1332
|
+
console.warn(` ⚠️ quick 范围快照落盘失败(不阻断收尾):${e && e.message ? String(e.message).split('\n')[0] : e}`)
|
|
1333
|
+
}
|
|
1269
1334
|
// 刷新 DB title:从 step3「需求:」提取(agent 可改 title 的途径),覆盖启动时的兜底快照。
|
|
1270
1335
|
// 与 QUICKLOG 标题刷新(flipEntryInContent 内 extractTitleFromResult)同源,保持 DB↔QUICKLOG 一致。
|
|
1271
1336
|
try {
|
|
@@ -1291,6 +1356,14 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1291
1356
|
console.warn(`⚠️ 关联变更轻量归档失败(不阻断 quick 完成): ${e.message}`)
|
|
1292
1357
|
}
|
|
1293
1358
|
|
|
1359
|
+
// 摩擦信号消费(friction-signal-hint task-04):必须在 session 目录清理**前**——清理后
|
|
1360
|
+
// tally 随目录删除,晚于此就读不到。QUICKLOG 完成打印之后 consume,提示里的 postmortem
|
|
1361
|
+
// 建议有落点可循;全零/读失败零输出,摩擦提示失败不影响收尾。
|
|
1362
|
+
try {
|
|
1363
|
+
const fr = consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
1364
|
+
if (fr && fr.hint) console.log(`\n${fr.hint}`)
|
|
1365
|
+
} catch { /* 摩擦提示失败不影响收尾 */ }
|
|
1366
|
+
|
|
1294
1367
|
// 清理 session 目录(rmSync/unlinkSync 容忍不存在)。路径与写入对齐(Q4 resolveQuickSessionsDir)。
|
|
1295
1368
|
try {
|
|
1296
1369
|
if (changeName) {
|
|
@@ -1851,7 +1924,7 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
|
|
|
1851
1924
|
// untracked 新文件 wc-l 记全 + 行、binary 显 BIN)。纯展示列(advisory,D-006):采集失败/行数
|
|
1852
1925
|
// 不可得 → 括注省略不出伪数据;不改 auditQuickCompletion 判定、runQuickTestLintGate 与任何门禁。
|
|
1853
1926
|
// 放文件尾(函数声明提升)而不内联进 handleQuickStageCompletion:docs/platform-interface-map
|
|
1854
|
-
// 行号锚在上方(complete-handlers.js:
|
|
1927
|
+
// 行号锚在上方(complete-handlers.js:1695 handleScanStageCompleted),内联大块会把 doc-ref 锚
|
|
1855
1928
|
// 推出关键词窗口。
|
|
1856
1929
|
//
|
|
1857
1930
|
// 消费分工(调用方 quick 收尾):
|
package/src/run/complete.js
CHANGED
|
@@ -666,6 +666,17 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
666
666
|
// verify 完成出一行漂移确认。advisory fail-soft(D-006)——helper 内部全兜,绝不阻断完成。
|
|
667
667
|
await printStageCompletionScopeAudit({ stageName, cwd, changeName, specBase, platformOpts })
|
|
668
668
|
|
|
669
|
+
// 摩擦信号消费(friction-signal-hint task-03):verify 收尾读一次 friction tally,非零打
|
|
670
|
+
// 一行 advisory 引导补 postmortem(consume 内部提示后清零);全零/读失败零输出——
|
|
671
|
+
// 摩擦提示失败不影响收尾。
|
|
672
|
+
if (stageName === 'verify') {
|
|
673
|
+
try {
|
|
674
|
+
const { consumeFrictionHint } = await import('../friction-tally.js')
|
|
675
|
+
const r = consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
676
|
+
if (r && r.hint) console.log(`\n${r.hint}`)
|
|
677
|
+
} catch { /* 摩擦提示失败不影响收尾 */ }
|
|
678
|
+
}
|
|
679
|
+
|
|
669
680
|
if (stageName === 'execute') {
|
|
670
681
|
// execute run summary:展示真实可得的结构化信息
|
|
671
682
|
try {
|
|
@@ -804,15 +815,21 @@ function scopeAuditPathSet(result) {
|
|
|
804
815
|
return set
|
|
805
816
|
}
|
|
806
817
|
|
|
807
|
-
/** execute 完成路径:全表 +
|
|
818
|
+
/** execute 完成路径:全表 + 审计级快照/patch 落盘(quick-359a48f1:changes/<变更名>/ 随归档
|
|
819
|
+
* 入库;patch 为收尾时点全量冻结——--file 真·当时内容比对的数据源。写失败只提示,verify
|
|
820
|
+
* 对比按无快照降级) */
|
|
808
821
|
async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts, computeChangeScopeAudit, renderScopeAuditTable }) {
|
|
809
|
-
const result = await computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts })
|
|
822
|
+
const result = await computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts, collectPatch: true })
|
|
810
823
|
console.log(`\n${renderScopeAuditTable(result, { maxRows: 60 })}`)
|
|
811
824
|
try {
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
writeFileSync(
|
|
825
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
826
|
+
mkdirSync(changeDir, { recursive: true })
|
|
827
|
+
const { frozenPatch, ...snap } = result
|
|
828
|
+
writeFileSync(join(changeDir, 'scope-audit.json'), JSON.stringify({ ...snap, savedAt: new Date().toISOString() }, null, 2) + '\n')
|
|
829
|
+
if (typeof frozenPatch === 'string') {
|
|
830
|
+
writeFileSync(join(changeDir, 'scope-audit.patch'), frozenPatch.endsWith('\n') ? frozenPatch : frozenPatch + '\n')
|
|
831
|
+
}
|
|
832
|
+
console.log(` 📦 范围快照已落变更目录(scope-audit.json${typeof frozenPatch === 'string' ? ' + scope-audit.patch(收尾时点冻结)' : ''})`)
|
|
816
833
|
} catch (e) {
|
|
817
834
|
console.warn(` ⚠️ 范围对账快照写入失败(不阻断,verify 漂移对比将按无快照降级):${e && e.message ? String(e.message).split('\n')[0] : e}`)
|
|
818
835
|
}
|
|
@@ -829,9 +846,14 @@ async function printVerifyScopeDrift({ cwd, changeName, specBase, platformOpts,
|
|
|
829
846
|
return
|
|
830
847
|
}
|
|
831
848
|
let snapshot = null
|
|
849
|
+
// 快照读取链(quick-359a48f1):变更目录(审计级,execute --done 落)→ .runtime(存量兼容)
|
|
832
850
|
try {
|
|
833
|
-
snapshot = JSON.parse(readFileSync(join(
|
|
834
|
-
} catch {
|
|
851
|
+
snapshot = JSON.parse(readFileSync(join(specBase, 'changes', changeName, 'scope-audit.json'), 'utf8'))
|
|
852
|
+
} catch {
|
|
853
|
+
try {
|
|
854
|
+
snapshot = JSON.parse(readFileSync(join(resolveRuntimeRoot(platformOpts, specBase), `scope-audit-${changeName}.json`), 'utf8'))
|
|
855
|
+
} catch { /* 快照缺失/损坏 → 跳过对比只打当前 totals */ }
|
|
856
|
+
}
|
|
835
857
|
if (!snapshot || snapshot.ok !== true) {
|
|
836
858
|
console.log(` ${scopeLine}(无 execute 时点快照可对比——完整表跑 sillyspec scope-audit --change ${changeName})`)
|
|
837
859
|
return
|
|
@@ -1474,6 +1496,15 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
1474
1496
|
// task-04:wait 解除完成路径与 completeStep 完成分支同源注入(Grill G-2:本路径不漏,
|
|
1475
1497
|
// 否则 execute 快照缺失 → verify 假报「无快照可对比」)。
|
|
1476
1498
|
await printStageCompletionScopeAudit({ stageName, cwd, changeName, specBase, platformOpts })
|
|
1499
|
+
// 摩擦信号消费(friction-signal-hint task-03):与 completeStep 完成分支同款(Grill CC-03:
|
|
1500
|
+
// 漏此则 --continue 收尾丢提示丢清零)。全零/读失败零输出,摩擦提示失败不影响收尾。
|
|
1501
|
+
if (stageName === 'verify') {
|
|
1502
|
+
try {
|
|
1503
|
+
const { consumeFrictionHint } = await import('../friction-tally.js')
|
|
1504
|
+
const r = consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
1505
|
+
if (r && r.hint) console.log(`\n${r.hint}`)
|
|
1506
|
+
} catch { /* 摩擦提示失败不影响收尾 */ }
|
|
1507
|
+
}
|
|
1477
1508
|
// 阶段完成后明确下一步(agent 常卡:stageData completed 但不知要 run <下一阶段> 推进 currentStage)
|
|
1478
1509
|
const nextStageHint = { brainstorm: 'plan', plan: 'execute', execute: 'verify', verify: 'archive' }[stageName]
|
|
1479
1510
|
if (nextStageHint) {
|