sillyspec 3.32.0 → 3.32.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-explore/SKILL.md +6 -1
- package/package.json +2 -2
- package/src/doctor-diagnostics.js +84 -4
- package/src/flow-parity.js +137 -1
- package/src/flow-review.js +4 -0
- package/src/flow.js +67 -25
- package/src/foreign-declared.js +7 -3
- package/src/init.js +5 -1
- package/src/knowledge-graph.js +918 -0
- package/src/knowledge-match.js +11 -6
- package/src/knowledge-stats.js +39 -0
- package/src/knowledge-vector.js +17 -3
- package/src/run/complete.js +38 -13
- package/src/scope-audit.js +129 -3
- package/src/sentinel-assertions.js +27 -2
- package/src/stages/explore.js +5 -1
- package/src/stages/knowledge.js +11 -1
- package/src/verify-postcheck.js +11 -3
- package/src/watcher.js +6 -2
package/src/knowledge-match.js
CHANGED
|
@@ -62,7 +62,7 @@ function isDecisionRoute(entry) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const DECISION_HEADER_RE = /^##\s+(D-\d+@v\d+)\s*(.*)$/
|
|
65
|
-
const DECISION_FIELD_RE = /^(
|
|
65
|
+
const DECISION_FIELD_RE = /^(状态|文件|理由|否决理由|复潮条件|supersedes)\s*[::]\s*(.*)$/
|
|
66
66
|
// 「锚点:」单独标签精确匹配读入(仿 docs-check.js parseDecisionEntries 先例)——绝不并入
|
|
67
67
|
// DECISION_FIELD_RE:其 else-if 链末尾的 reason 回填(else if (!cur.reason))会把锚点值
|
|
68
68
|
// 误吞进 reason(Grill X-002 隐性回归)。
|
|
@@ -87,15 +87,19 @@ const ANCHOR_FILE_TOKEN_RE = /[\w./-]+\.(?:mjs|cjs|jsx|tsx|js|ts|py|go|java|rs)/
|
|
|
87
87
|
* 锚点值 → 代码文件路径集(条目无「文件:」字段时的兜底命中源)。
|
|
88
88
|
* 真实先例:D-905 形态「锚点:src/quicklog.js:493」→ 提取 src/quicklog.js
|
|
89
89
|
* (token 字符类不含 : 故止于行号前,再防御性剥 :line/:line-line/:符号 后缀)。
|
|
90
|
-
* 「未记录」/无路径形态 token → []
|
|
90
|
+
* 「未记录」/无路径形态 token → []。导出供 knowledge-graph.js 采集 anchors 边复用。
|
|
91
91
|
*/
|
|
92
|
-
function anchorFilePaths(anchor) {
|
|
92
|
+
export function anchorFilePaths(anchor) {
|
|
93
93
|
const a = String(anchor || '').trim().replace(/\\/g, '/')
|
|
94
94
|
if (!a || a === '未记录') return []
|
|
95
95
|
const out = new Set()
|
|
96
96
|
for (const token of a.match(ANCHOR_FILE_TOKEN_RE) || []) {
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
// 前导斜杠剥除(2026-10-09-graph-docrefs-noise):repo:// URL 被正则吃成 //sillyhub/... 碎片,
|
|
98
|
+
// 剥后 sillyhub/ 顶级由图侧按跨仓口径处理;纯数字/裸扩展段丢弃(解析噪声)
|
|
99
|
+
const p = token
|
|
100
|
+
.replace(/:(?:\d+(?:-\d+)?|[A-Za-z_$][A-Za-z0-9_$]*)$/, '')
|
|
101
|
+
.replace(/^\/+/, '')
|
|
102
|
+
if (p && !/^\d+$/.test(p)) out.add(p)
|
|
99
103
|
}
|
|
100
104
|
return [...out]
|
|
101
105
|
}
|
|
@@ -129,7 +133,7 @@ function parseDecisionFile(filePath, file) {
|
|
|
129
133
|
const h = line.match(DECISION_HEADER_RE)
|
|
130
134
|
if (h) {
|
|
131
135
|
flush()
|
|
132
|
-
cur = { file, id: h[1], title: h[2].trim(), status: '', reason: '', revisitWhen: '', files: [], anchor: '', change: '' }
|
|
136
|
+
cur = { file, id: h[1], title: h[2].trim(), status: '', reason: '', revisitWhen: '', files: [], anchor: '', change: '', supersedesText: '' }
|
|
133
137
|
continue
|
|
134
138
|
}
|
|
135
139
|
if (!cur) continue
|
|
@@ -145,6 +149,7 @@ function parseDecisionFile(filePath, file) {
|
|
|
145
149
|
const value = f[2].trim()
|
|
146
150
|
if (f[1] === '状态') cur.status = value.toLowerCase()
|
|
147
151
|
else if (f[1] === '文件') cur.files = splitDecisionFiles(value)
|
|
152
|
+
else if (f[1] === 'supersedes') cur.supersedesText = value
|
|
148
153
|
else if (f[1] === '否决理由') cur.reason = value
|
|
149
154
|
else if (f[1] === '复潮条件') cur.revisitWhen = value
|
|
150
155
|
else if (!cur.reason) cur.reason = value // 理由:仅作 implemented(或否决理由缺失)时的回填
|
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/knowledge-vector.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* knowledge-vector.js — 平台向量召回层(CLI 侧,2026-09-29-knowledge-vector-recall)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 检索四层(2026-10-08-knowledge-graph 起):路由 tag 命中 →(零命中)平台向量召回(本模块,
|
|
5
|
+
* 已连接平台时)→ scope 遍历召回(knowledge-graph.js,scopeFiles 在场时)→ 本地词片复现窗口
|
|
6
|
+
* (knowledge-match.js fallbackByQueryShingles)→ 空。
|
|
6
7
|
*
|
|
7
8
|
* 原则(用户裁决 2026-09-29):平台只做语义召回——返回「哪个文件哪个条目」的候选
|
|
8
9
|
* (spec_path+anchor+score);条目 status/deathPath/回显资格等全部策略面在本地解析
|
|
@@ -143,8 +144,11 @@ function buildResultFromPlatform(indexDir, pvResults) {
|
|
|
143
144
|
|
|
144
145
|
/**
|
|
145
146
|
* 异步三层检索入口(四消费方使用:flow 注入段/complete 门/prompt {DECISION_HITS}/
|
|
146
|
-
* knowledge search CLI):路由 → 平台向量(已连接且开关开)→ 本地词片 → 空。
|
|
147
|
+
* knowledge search CLI):路由 → 平台向量(已连接且开关开)→ scope 遍历 → 本地词片 → 空。
|
|
147
148
|
* opts.cwd = 仓根(平台配置与 local.yaml 定位用);缺省由 indexDir 上推两级。
|
|
149
|
+
* opts.scopeFiles(2026-10-08-knowledge-graph,FR-04 承接 FR-cli-entry-234):变更触碰文件集
|
|
150
|
+
* ——机器算的图查询键(D-004:flow 注入段传 touched、complete 门传 decisions.md 锚点提取)。
|
|
151
|
+
* 缺省/空数组 → 遍历层整体跳过,行为与不携带该参数的旧行为逐字段一致(回归钉死)。
|
|
148
152
|
*/
|
|
149
153
|
export async function matchKnowledgeHybrid(indexDir, taskContext, opts = {}) {
|
|
150
154
|
const r = matchByRouting(indexDir, taskContext)
|
|
@@ -155,6 +159,16 @@ export async function matchKnowledgeHybrid(indexDir, taskContext, opts = {}) {
|
|
|
155
159
|
const built = pv ? buildResultFromPlatform(indexDir, pv.results) : null
|
|
156
160
|
if (built) return built
|
|
157
161
|
}
|
|
162
|
+
// scope 遍历层(第三层):只在 scope 非空且向量零命中后接管;fail-soft(图构建异常静默降级词片)
|
|
163
|
+
const scopeFiles = Array.isArray(opts.scopeFiles) ? opts.scopeFiles.filter(Boolean) : []
|
|
164
|
+
if (scopeFiles.length > 0) {
|
|
165
|
+
try {
|
|
166
|
+
const { buildKnowledgeGraph, scopeRecall, buildScopeRecallResult } = await import('./knowledge-graph.js')
|
|
167
|
+
const specRoot = String(indexDir).replace(/[\\/]knowledge[\\/]?$/, '')
|
|
168
|
+
const built = buildScopeRecallResult(scopeRecall(buildKnowledgeGraph(specRoot), scopeFiles))
|
|
169
|
+
if (built) return built
|
|
170
|
+
} catch { /* 图构建 fail-soft——降级词片层 */ }
|
|
171
|
+
}
|
|
158
172
|
const fb = fallbackByQueryShingles(indexDir, taskContext)
|
|
159
173
|
return fb || r
|
|
160
174
|
}
|
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'
|
|
@@ -479,10 +479,23 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
479
479
|
_kgQuery = _kgQuery.slice(0, 4000)
|
|
480
480
|
if (_kgQuery.trim()) {
|
|
481
481
|
const { matchKnowledgeHybrid } = await import('../knowledge-vector.js')
|
|
482
|
-
const
|
|
482
|
+
const { scopeFromDecisionsMd } = await import('../knowledge-graph.js')
|
|
483
|
+
// scope 键=机器算(D-004 需求期入口):decisions.md「锚点:」字段路径提取——方案步的
|
|
484
|
+
// 结构化声明面,不解析自由文本。词面零命中时沿强边保底防复潮(FR-04)。
|
|
485
|
+
const km = await matchKnowledgeHybrid(join(specBase, 'knowledge'), _kgQuery, { cwd, scopeFiles: scopeFromDecisionsMd(_dm) })
|
|
483
486
|
// 零分不弹(2026-09-28-knowledge-gate-denoise):score 零且非死路的 rejected 与查询零主题
|
|
484
487
|
// 重叠——空标题条目靠状态蹭进回显是行为实测三例的噪音源;死路条目不受限(防复潮先验)。
|
|
485
488
|
const _hits = (km.decisionHits || []).filter((h) => h.deathPath || (h.status === 'rejected' && h.score > 0))
|
|
489
|
+
// impact 防复潮可达集(2026-10-09-brainstorm-impact-antirevival):锚点+模块域双结构键
|
|
490
|
+
// → graphImpact 闭包内 rejected/死路保底进场——方案步锚点常空(confirmed 才必填),
|
|
491
|
+
// 模块域是生成规范文件步硬校验的在场键;viaImpact 条目绕过 score 门槛(结构可达即
|
|
492
|
+
// 防复潮先验,与死路同待遇)。图构建 fail-soft(失败不阻断门)。
|
|
493
|
+
try {
|
|
494
|
+
const { buildKnowledgeGraph, impactFromDecisionsMd } = await import('../knowledge-graph.js')
|
|
495
|
+
const _viaImpact = impactFromDecisionsMd(buildKnowledgeGraph(specBase), _dm)
|
|
496
|
+
.filter((h) => !_hits.some((x) => x.file === h.file && x.id === h.id && (x.change || '') === (h.change || '')))
|
|
497
|
+
_hits.push(..._viaImpact)
|
|
498
|
+
} catch { /* 图构建/键解析 fail-soft——词面命中面不受影响 */ }
|
|
486
499
|
// 已回应不重弹:decisions.md 正文已含「命中 id+域文件名」共现(小白鼠回应形态
|
|
487
500
|
// 「unmapped.md D-001@v1」)→ 视为已甄别过,静默;同轮查询串仍含其词面属预期。
|
|
488
501
|
const _answered = _dm
|
|
@@ -500,7 +513,8 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
500
513
|
const _note = h.status === 'rejected'
|
|
501
514
|
? ` 否决理由:${h.reason || '(未记录)'}`
|
|
502
515
|
: h.deathPath ? ` ⚰️死路注记:${deathPathNote(h.reason)}` : ''
|
|
503
|
-
|
|
516
|
+
const _via = h.viaImpact ? ` 🧭impact 可达(键:${h.impactKey})` : ''
|
|
517
|
+
console.warn(` - ${h.id} ${h.title}(${h.file})status=${h.status || '?'}${_note}${_via}`)
|
|
504
518
|
}
|
|
505
519
|
for (const e of _entries.slice(0, 3)) {
|
|
506
520
|
const _base = e.anchor ? `${e.file}#${e.anchor}` : e.file
|
|
@@ -1109,16 +1123,27 @@ async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts,
|
|
|
1109
1123
|
mkdirSync(changeDir, { recursive: true })
|
|
1110
1124
|
const { frozenPatch, ...snap } = result
|
|
1111
1125
|
snap.note = 'execute --done 时点冻结(本文件落盘时采集)'
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1126
|
+
snap.closedBy = 'execute --done'
|
|
1127
|
+
// 沉淀资产面投影(2026-10-07-unify-close-trace):files/totals = 主仓实改行(planned+
|
|
1128
|
+
// unplanned;untouched 0/0 是声明不是改动、crossRepo 行 patch 不在主仓——均不入)。
|
|
1129
|
+
// 经共用 writeCloseTraceArtifacts 与 thin flow done 同一处写四件(sha256 双套同锚);
|
|
1130
|
+
// 投影口径抽 flow-parity.projectTraceFaceRows 纯函数(排除路径有单测钉住)。
|
|
1131
|
+
const face = projectTraceFaceRows(snap.rows)
|
|
1132
|
+
const closeTrace = writeCloseTraceArtifacts({
|
|
1133
|
+
changeDir,
|
|
1134
|
+
change: changeName,
|
|
1135
|
+
baseline: typeof snap.baseAnchor === 'string' ? snap.baseAnchor : null,
|
|
1136
|
+
head: String(gitQuiet(cwd, ['rev-parse', 'HEAD']) || '').trim() || null,
|
|
1137
|
+
files: face.files,
|
|
1138
|
+
metaTotals: face.totals,
|
|
1139
|
+
patchText: typeof frozenPatch === 'string' && frozenPatch ? frozenPatch : null,
|
|
1140
|
+
savedAt: new Date().toISOString(),
|
|
1141
|
+
snapObj: snap,
|
|
1142
|
+
meta: {
|
|
1143
|
+
note: 'execute --done 时点冻结(沉淀资产面:主仓实改行投影——thin flow done 对等;治理工件目录与其后 verify/archive 演进不在内)',
|
|
1144
|
+
},
|
|
1145
|
+
})
|
|
1146
|
+
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
1147
|
} catch (e) {
|
|
1123
1148
|
console.warn(` ⚠️ 范围对账快照写入失败(不阻断,verify 漂移对比将按无快照降级):${e && e.message ? String(e.message).split('\n')[0] : e}`)
|
|
1124
1149
|
}
|
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
|
// —— 行数采集根与基点 ——
|
|
@@ -25,6 +25,24 @@ function taskTokenRe(id) {
|
|
|
25
25
|
return new RegExp(`${id}(?!\\d)`);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* 提交消息连写 token 组展开(坑 flow-done-sentinel-token-split,2026-10-08 实证):
|
|
30
|
+
* `task-01/02/03`、`task-1、2,03` 等自然语言连写组只含首个完整 token——字面匹配判
|
|
31
|
+
* 其余为零完成证据误拒收。展开为独立规范 token(补零到两位,与判集行锚同形)后再判;
|
|
32
|
+
* 尾数不跟数字(防 task-013 版本串误切)。纯函数,供哨兵判定与 autopilot 勾选提取共用。
|
|
33
|
+
*/
|
|
34
|
+
export function expandTaskShorthand(messages) {
|
|
35
|
+
return (Array.isArray(messages) ? messages : []).map((msg) =>
|
|
36
|
+
String(msg ?? '').replace(
|
|
37
|
+
/task-(\d{1,2})((?:\s*[\/、,,;;]\s*\d{1,2}(?!\d))+)/gi,
|
|
38
|
+
(full, first, rest) =>
|
|
39
|
+
' ' + [first, ...(rest.match(/\d{1,2}/g) || [])]
|
|
40
|
+
.map((n) => `task-${String(Number(n)).padStart(2, '0')}`)
|
|
41
|
+
.join(' ') + ' '
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
/**
|
|
29
47
|
* review.json 在场证据清单(execute-runs 两级遍历;异常/缺失 → [],commit 证据照判)。
|
|
30
48
|
*/
|
|
@@ -84,11 +102,13 @@ export function detectFakeCheckCompletion({ changeDir, tasksMd, commits, opts =
|
|
|
84
102
|
return { status: 'none', claimTotal, checked, missing: [] };
|
|
85
103
|
}
|
|
86
104
|
const reviews = listReviewEvidence(changeDir, opts);
|
|
105
|
+
// 连写组先展开(坑 flow-done-sentinel-token-split):task-01/02/03 → 独立 token 再字面判
|
|
106
|
+
const expanded = expandTaskShorthand(messages);
|
|
87
107
|
const missing = entries
|
|
88
108
|
.filter((e) => {
|
|
89
109
|
if (!e.checked) return false;
|
|
90
110
|
const re = taskTokenRe(e.id);
|
|
91
|
-
const byCommit =
|
|
111
|
+
const byCommit = expanded.some((msg) => re.test(msg));
|
|
92
112
|
const byReview = reviews.some((p) => p.includes(`/tasks/${e.id}/`));
|
|
93
113
|
return !(byCommit || byReview);
|
|
94
114
|
})
|
|
@@ -109,7 +129,12 @@ export function detectFakeCheckCompletion({ changeDir, tasksMd, commits, opts =
|
|
|
109
129
|
export function detectBatchCheckCadence(events) {
|
|
110
130
|
const taskEvents = [];
|
|
111
131
|
for (const e of events || []) {
|
|
112
|
-
if (e
|
|
132
|
+
if (!e || e.kind !== 'task-done') continue;
|
|
133
|
+
// 节奏域过滤(2026-10-08-batch-tick-false-positive):只认任务队列面(stage='tasks',缺省
|
|
134
|
+
// 视同 tasks 兼容 legacy 流)。其他阶段的 task-done(如 design.md 自审预勾的首现计数)不是
|
|
135
|
+
// tick 信号——混入会把逐格勾选误判成单拍大跳(实证:design 0→8 拒收六次独立 tick)。
|
|
136
|
+
if (e.stage != null && e.stage !== 'tasks') continue;
|
|
137
|
+
taskEvents.push(e);
|
|
113
138
|
}
|
|
114
139
|
const parse = (e) => {
|
|
115
140
|
const m = /^checked (\d+)→(\d+)$/.exec(String(e.detail || ''));
|
package/src/stages/explore.js
CHANGED
|
@@ -15,7 +15,11 @@ export const definition = {
|
|
|
15
15
|
- \`.sillyspec/docs/<project>/scan/ARCHITECTURE.md\`
|
|
16
16
|
- \`.sillyspec/docs/<project>/scan/CONVENTIONS.md\`
|
|
17
17
|
- \`.sillyspec/changes/<change-name>/design.md\`
|
|
18
|
-
3.
|
|
18
|
+
3. 调查兵器按话题选(话题→兵器映射,全只读):
|
|
19
|
+
- 词面考古("这段代码在哪/怎么写的")→ \`rg\` / \`ls\` / \`cat\`
|
|
20
|
+
- 坑史检索("这话题以前踩过什么坑")→ \`sillyspec knowledge search --query "<关键词>"\`(四层召回)
|
|
21
|
+
- 关系面(影响面/历史决策/需求谱系)→ \`sillyspec knowledge graph impact <文件|模块|变更名>\` 多跳闭包与防复潮可达的已否决决策、\`neighbors <锚点>\` / \`path <A> <B>\` 推理链、\`summary\` 知识面健康度——比较方案前先看 impact 的 rejected/⚰️死路条目,已否决的路不重新兜售;全部支持 \`--json\`
|
|
22
|
+
- 在途面(出方向建议前)→ \`sillyspec status\` 活跃变更一览——结论别跟在途工作撞车
|
|
19
23
|
4. 输出 2-3 个有价值方向、关键风险和下一步建议
|
|
20
24
|
5. 如果用户要求保存结论,先明确保存位置,再写入对应文档
|
|
21
25
|
6. 讨论 UI/交互方案且用户明确要求原型时,按下方「HTML 原型生成」执行
|
package/src/stages/knowledge.js
CHANGED
|
@@ -609,6 +609,16 @@ export async function cmdKnowledge(args, dir, opts = {}) {
|
|
|
609
609
|
return cmdRefresh(dir, args.slice(1), opts)
|
|
610
610
|
case 'propose':
|
|
611
611
|
return cmdPropose(dir, args.slice(1), opts)
|
|
612
|
+
case 'graph': {
|
|
613
|
+
// 知识图谱查询面(2026-10-08-knowledge-graph):neighbors/path/impact/orphans/dangling,
|
|
614
|
+
// --edges 边型筛选;全 JSON 出口(与 search/validate 同族)。
|
|
615
|
+
const mod = await tryImportSubcommandImpl('../knowledge-graph.js', 'knowledge-graph.js')
|
|
616
|
+
if (!mod || typeof mod.cmdKnowledgeGraph !== 'function') {
|
|
617
|
+
output(false, {}, { code: 'not_implemented', subcommand: 'graph' })
|
|
618
|
+
return
|
|
619
|
+
}
|
|
620
|
+
return mod.cmdKnowledgeGraph(dir, args.slice(1), opts)
|
|
621
|
+
}
|
|
612
622
|
case 'classify': {
|
|
613
623
|
const mod = await tryImportSubcommandImpl('../knowledge-classify.js', 'knowledge-classify.js')
|
|
614
624
|
if (!mod || typeof mod.cmdKnowledgeClassify !== 'function') {
|
|
@@ -665,7 +675,7 @@ export async function cmdKnowledge(args, dir, opts = {}) {
|
|
|
665
675
|
output(false, {}, {
|
|
666
676
|
code: 'unknown_subcommand',
|
|
667
677
|
subcommand: subCommand,
|
|
668
|
-
available: ['search', 'inspect', 'validate', 'refresh', 'propose', 'classify', 'inbox', 'stats', 'digest'],
|
|
678
|
+
available: ['search', 'inspect', 'validate', 'refresh', 'propose', 'classify', 'inbox', 'stats', 'digest', 'graph'], // dump 随 graph 路由面(2026-10-09-graph-dump-layout)
|
|
669
679
|
})
|
|
670
680
|
}
|
|
671
681
|
}
|
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)
|
package/src/watcher.js
CHANGED
|
@@ -339,8 +339,12 @@ export function inferEvents(prev, next) {
|
|
|
339
339
|
const p = prev.files[key];
|
|
340
340
|
const n = next.files[key];
|
|
341
341
|
if (!p && n) {
|
|
342
|
-
events.push(mk('file', n.stage, `${key} 出现`))
|
|
343
|
-
|
|
342
|
+
events.push(mk('file', n.stage, `${key} 出现`))
|
|
343
|
+
// 勾选节奏域=任务队列文件(tasks.md / tasks/*.md,stage='tasks')。其余阶段文件首现的
|
|
344
|
+
// 勾选计数不是 tick 信号:design.md 自审清单天然预勾(整体写盘的已完成断言)——发了就是
|
|
345
|
+
// 幻影单拍跳(2026-10-08-batch-tick-false-positive 实证:design 首现 0→8 被节奏门误拒
|
|
346
|
+
// 六次独立 tick);tasks.md 首现带已勾仍发(保住「整卡预勾创建」的真实捕捉面)。
|
|
347
|
+
if (n.checked > 0 && n.stage === 'tasks') events.push(mk('task-done', n.stage, `checked 0→${n.checked}`))
|
|
344
348
|
} else if (p && n && p.hash !== n.hash) {
|
|
345
349
|
events.push(mk('file-update', n.stage, `${key} 内容变更`));
|
|
346
350
|
}
|