sillyspec 3.30.0 → 3.32.0
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-auto/SKILL.md +54 -54
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +10 -6
- package/.claude/skills/sillyspec-quick/SKILL.md +116 -18
- package/.zcodeignore +66 -0
- package/AGENTS.md +15 -15
- package/assets/command-cards/flow.md +9 -3
- package/assets/command-cards/run-quick.md +4 -1
- package/package.json +2 -2
- package/src/agent-session-log.js +27 -5
- package/src/config-schema.js +21 -0
- package/src/datetime.js +51 -0
- package/src/db.js +10 -3
- package/src/decision-distill.js +21 -12
- package/src/design-facts.js +46 -6
- package/src/flow-draft.js +468 -95
- package/src/flow-parity.js +62 -4
- package/src/flow-review.js +136 -6
- package/src/flow.js +729 -125
- package/src/foreign-declared.js +71 -3
- package/src/fr-index.js +148 -162
- package/src/git-helper.js +16 -3
- package/src/hooks/worktree-guard.js +16 -5
- package/src/hunk-attribution.js +28 -5
- package/src/index.js +184 -17
- package/src/knowledge-classify.js +3 -1
- package/src/knowledge-digest.js +55 -26
- package/src/knowledge-match.js +135 -7
- package/src/knowledge-stats.js +66 -4
- package/src/knowledge-vector.js +160 -0
- package/src/machine-interface.js +90 -2
- package/src/module-impact.js +22 -1
- package/src/modules.js +9 -0
- package/src/progress/change-registry.js +20 -0
- package/src/progress/stage-machine.js +4 -7
- package/src/progress.js +8 -1
- package/src/quicklog.js +20 -0
- package/src/redomain.js +23 -7
- package/src/route-hindsight.js +276 -0
- package/src/run/archive-distill.js +3 -1
- package/src/run/command.js +65 -12
- package/src/run/complete-handlers.js +219 -5
- package/src/run/complete.js +77 -11
- package/src/run/gate-snapshot.js +15 -0
- package/src/run/gates.js +60 -4
- package/src/run/prompt.js +159 -31
- package/src/run/quick-audit.js +66 -7
- package/src/run/shared.js +6 -11
- package/src/run/stage.js +5 -1
- package/src/run/verify-quality-scan.js +84 -0
- package/src/scan-facts.js +2 -1
- package/src/scope-audit.js +9 -2
- package/src/sentinel-assertions.js +67 -16
- package/src/stage-contract-engine.js +4 -1
- package/src/stage-contract-spec.js +103 -4
- package/src/stage-contract.js +55 -7
- package/src/stages/brainstorm.js +27 -14
- package/src/stages/knowledge.js +4 -3
- package/src/stages/plan-postcheck.js +98 -3
- package/src/stages/plan.js +22 -2
- package/src/stages/verify.js +2 -1
- package/src/task-review.js +1 -1
- package/src/task-tick.js +121 -0
- package/src/taskcard-frontmatter.js +31 -0
- package/src/ui-visual.js +43 -8
- package/src/verify-postcheck.js +509 -37
- package/src/verify-probes.js +216 -5
- package/src/watcher.js +1067 -1031
- package/src/worktree-apply.js +9 -1
- package/src/worktree.js +21 -6
- package/templates/agents-instruction.md +15 -15
- package/templates/prompts/taskcard-rules.md +1 -0
package/src/knowledge-match.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { existsSync, readFileSync } from 'fs'
|
|
13
13
|
import { join } from 'path'
|
|
14
|
+
import { frTitleOverlap } from './fr-index.js'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* 从 INDEX.md 解析所有知识条目
|
|
@@ -115,7 +116,13 @@ function parseDecisionFile(filePath, file) {
|
|
|
115
116
|
const hits = []
|
|
116
117
|
let cur = null
|
|
117
118
|
const flush = () => {
|
|
118
|
-
if (cur)
|
|
119
|
+
if (cur) {
|
|
120
|
+
// 死路注记识别(2026-09-28-knowledge-inject-ranking):「理由」含「死路:」字面标记的教训型
|
|
121
|
+
// 条目——记录它的决策本体是 accepted/implemented(死路只是理由里的注记),按状态过滤会被
|
|
122
|
+
// 压制到组外,恰丢最该防复潮的(实测:D-001@v1 枚举开放世界排 148/188 位永不回显)。
|
|
123
|
+
cur.deathPath = /死路[::]/.test(cur.reason || '')
|
|
124
|
+
hits.push(cur)
|
|
125
|
+
}
|
|
119
126
|
cur = null
|
|
120
127
|
}
|
|
121
128
|
for (const line of content.replace(/\r\n/g, '\n').split('\n')) {
|
|
@@ -209,6 +216,15 @@ export function matchDecisionsByFiles(indexDir, files) {
|
|
|
209
216
|
return result
|
|
210
217
|
}
|
|
211
218
|
|
|
219
|
+
/** 死路注记短句提取(消费方渲染用):理由里「死路:…」到首个句读为止;无标记时截前 60 字。
|
|
220
|
+
* 单一源供 flow 知识注入段与 complete.js knowledge-gate 回显共用(2026-09-28-knowledge-inject-ranking)。 */
|
|
221
|
+
export function deathPathNote(reason) {
|
|
222
|
+
const r = String(reason || '')
|
|
223
|
+
const m = r.match(/死路[::]\s*([^。;;,,\n]+)/)
|
|
224
|
+
if (m) return m[1].trim()
|
|
225
|
+
return r.slice(0, 60)
|
|
226
|
+
}
|
|
227
|
+
|
|
212
228
|
function escapeRegex(s) {
|
|
213
229
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
214
230
|
}
|
|
@@ -226,17 +242,104 @@ function keywordMatchesContext(keyword, contextLower) {
|
|
|
226
242
|
return new RegExp(`(^|[^a-z0-9])${escapeRegex(kw)}([^a-z0-9]|$)`).test(contextLower)
|
|
227
243
|
}
|
|
228
244
|
|
|
245
|
+
/** 查询词片抽取:CJK 连续段 bigram + ≥4 字符 ASCII 词(小写;短词与标点数字不参与;
|
|
246
|
+
* 纯 ASCII 查询不产词片——中文知识库,英文查询维持既有零命中行为)。 */
|
|
247
|
+
function queryShingles(taskContext) {
|
|
248
|
+
const raw = String(taskContext || '')
|
|
249
|
+
const shingles = new Set()
|
|
250
|
+
if (/[\u4e00-\u9fff]/.test(raw)) {
|
|
251
|
+
for (const w of raw.toLowerCase().match(/[a-z][a-z0-9-]{3,}/g) || []) shingles.add(w)
|
|
252
|
+
}
|
|
253
|
+
for (const seg of raw.match(/[\u4e00-\u9fff]+/g) || []) {
|
|
254
|
+
for (let i = 0; i < seg.length - 1; i++) shingles.add(seg.slice(i, i + 2))
|
|
255
|
+
}
|
|
256
|
+
return [...shingles]
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** ASCII 词片词边界命中(CJK 词片 includes 即可);转义防正则注入。 */
|
|
260
|
+
function asciiWordHit(sh, text) {
|
|
261
|
+
if (!/^[a-z]/.test(sh)) return text.includes(sh)
|
|
262
|
+
return new RegExp(`(^|[^a-z0-9-])${sh.replace(/[.*+?^${}()|[\]\\-]/g, '\\$&')}([^a-z0-9-]|$)`, 'i').test(text)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 零命中查询侧词片回退(2026-09-29-decision-route-vocab):路由行零命中时,只测「查询自己的
|
|
267
|
+
* 词片」——逐 decisions 域文件数词片在条目文本(标题∪理由)的出现次数,落在「跨条目复现但
|
|
268
|
+
* 非泛在」窗(count ∈ [2, max(2, floor(5%·条目数))])→ 文件级命中(合成路由条目进 entries,CLI
|
|
269
|
+
* search/digest 知识命中/门条目行三面共享);条目级命中 = 该文件内含 ≥1 复现词片的条目
|
|
270
|
+
* (score=复现词片数,消费方 score>0 资格判定兼容)。
|
|
271
|
+
* 设计依据:入库侧词表派生死于「同窗词片按序截断=抽签」(unmapped n∈[2,3] 窗 1176 个、
|
|
272
|
+
* 目标词排 759);查询侧只测自身几十词片,无挑选无 cap。机制词栖息在理由文本(模板标题不
|
|
273
|
+
* 含词形——Mouse B 实测四查全零的根因)。无可回退返回 null(保持既有零命中行为)。
|
|
274
|
+
*/
|
|
275
|
+
export function fallbackByQueryShingles(indexDir, taskContext) {
|
|
276
|
+
const shingles = queryShingles(taskContext)
|
|
277
|
+
if (shingles.length === 0) return null
|
|
278
|
+
const all = parseDecisionEntries(indexDir)
|
|
279
|
+
if (all.length === 0) return null
|
|
280
|
+
const byFile = new Map()
|
|
281
|
+
for (const h of all) {
|
|
282
|
+
let st = byFile.get(h.file)
|
|
283
|
+
if (!st) { st = { entries: [], texts: [] }; byFile.set(h.file, st) }
|
|
284
|
+
st.entries.push(h)
|
|
285
|
+
st.texts.push(`${h.title} ${h.reason || ''}`)
|
|
286
|
+
}
|
|
287
|
+
const corroborated = new Map()
|
|
288
|
+
for (const [file, st] of byFile) {
|
|
289
|
+
const text = st.texts.join('\n')
|
|
290
|
+
const ceil = Math.max(2, Math.floor(st.entries.length * 0.05))
|
|
291
|
+
const hits = new Set()
|
|
292
|
+
for (const sh of shingles) {
|
|
293
|
+
if (!asciiWordHit(sh, text)) continue
|
|
294
|
+
const n = text.split(sh).length - 1
|
|
295
|
+
if (n >= 2 && n <= ceil) hits.add(sh)
|
|
296
|
+
}
|
|
297
|
+
if (hits.size > 0) corroborated.set(file, hits)
|
|
298
|
+
}
|
|
299
|
+
if (corroborated.size === 0) return null
|
|
300
|
+
const fbEntries = [...corroborated.entries()].slice(0, 3).map(([file, hits]) => ({
|
|
301
|
+
category: 'Decisions',
|
|
302
|
+
keywords: [...hits].slice(0, 5),
|
|
303
|
+
file,
|
|
304
|
+
anchor: '',
|
|
305
|
+
display: `${file}(查询词片复现命中)`,
|
|
306
|
+
}))
|
|
307
|
+
const decisionHits = []
|
|
308
|
+
for (const [file, hits] of corroborated) {
|
|
309
|
+
for (const h of byFile.get(file).entries) {
|
|
310
|
+
const text = `${h.title} ${h.reason || ''}`
|
|
311
|
+
let mc = 0
|
|
312
|
+
for (const sh of hits) if (asciiWordHit(sh, text)) mc++
|
|
313
|
+
if (mc > 0) decisionHits.push({ ...h, score: mc })
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
decisionHits.sort((a, b) =>
|
|
317
|
+
(b.score - a.score) ||
|
|
318
|
+
(((b.status === 'rejected' || b.deathPath) ? 1 : 0) - ((a.status === 'rejected' || a.deathPath) ? 1 : 0))
|
|
319
|
+
)
|
|
320
|
+
return {
|
|
321
|
+
matched: true,
|
|
322
|
+
entries: fbEntries,
|
|
323
|
+
report: `Status: fallback matched (${[...corroborated.keys()].join(', ')} by query shingles)`,
|
|
324
|
+
json: {
|
|
325
|
+
matched: true, entry_count: fbEntries.length, fallback: true,
|
|
326
|
+
entries: fbEntries.map((e) => ({ file: e.file, anchor: e.anchor, keywords: e.keywords, category: e.category })),
|
|
327
|
+
},
|
|
328
|
+
decisionHits,
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
229
332
|
/**
|
|
230
333
|
* 用任务上下文匹配知识条目
|
|
231
334
|
* @param {string} indexDir - knowledge 目录路径
|
|
232
335
|
* @param {string} taskContext - 任务上下文(task 名称 + 描述,用于关键词匹配)
|
|
233
336
|
* @returns {{ matched: boolean, entries: Array, report: string, json: object,
|
|
234
|
-
* decisionHits: Array<{ file: string, id: string, title: string, status: string, reason: string, revisitWhen: string }> }}
|
|
337
|
+
* decisionHits: Array<{ file: string, id: string, title: string, status: string, reason: string, revisitWhen: string, deathPath?: boolean, score?: number }}> }}
|
|
235
338
|
* matched/entries/report/json 既有四字段结构与语义不变;decisionHits(task-04 新增)= 任务上下文
|
|
236
339
|
* 命中的 Decisions 路由行所指向 decisions/<域>.md 内的全部 D-xxx@vN 条目,rejected 优先排序;
|
|
237
340
|
* 无 decisions 库 / 路由行未命中 → decisionHits: [](其余字段行为与现状一致)。
|
|
238
341
|
*/
|
|
239
|
-
export function
|
|
342
|
+
export function matchByRouting(indexDir, taskContext) {
|
|
240
343
|
const indexPath = join(indexDir, 'INDEX.md')
|
|
241
344
|
|
|
242
345
|
// INDEX.md 不存在
|
|
@@ -302,14 +405,39 @@ export function matchKnowledge(indexDir, taskContext) {
|
|
|
302
405
|
}
|
|
303
406
|
|
|
304
407
|
// decisionHits(task-04):任务上下文命中的 Decisions 路由行 → 解析其指向的
|
|
305
|
-
// decisions/<域>.md
|
|
408
|
+
// decisions/<域>.md 内全部条目;防复潮优先(rejected ∪ 死路注记)+组内按查询×标题
|
|
409
|
+
// bigram 重叠率降序(2026-09-28-knowledge-inject-ranking:此前 rejected 优先+文件序,
|
|
410
|
+
// 空标题无关条目以文件序霸占回显前 N、相关条目沉底——实测 枚举/开放世界 查询下
|
|
411
|
+
// D-001@v1 排 148/188 位)。重叠判据复用 fr-index frTitleOverlap(字符 bigram,
|
|
412
|
+
// 封闭面非语义判定)。
|
|
306
413
|
// 不引入新顶层 hits 字段,不改 matched/entries/report/json 四个既有键。
|
|
307
414
|
const decisionRoutes = matched.filter(isDecisionRoute)
|
|
308
415
|
const allDecisionHits = decisionRoutes.length > 0 ? parseDecisionEntries(indexDir, decisionRoutes) : []
|
|
416
|
+
// 评分文本 = id+标题;评分前剥除数字与标点(2026-09-28-knowledge-score-denoise:{DECISION_HITS}
|
|
417
|
+
// 场景 taskContext=ASCII 变更名,日期「2026-09-28」与 id「D-009@v1」共享 -0/09 数字 bigram 即
|
|
418
|
+
// 反超死路条目——数字噪音在无内容词场景主导排序;只计内容字符后该场景全零分 → 平局 →
|
|
419
|
+
// 死路先验接管)。近义措辞(查询词只在理由不在标题)同样由零分平局裁决兜底。
|
|
420
|
+
// 刻意不把理由并进评分文本:Jaccard 比率偏爱短文本——短理由无关条目(「与需求直接冲突」共享
|
|
421
|
+
// 一个 bigram 即得高比率)会压过长理由被稀释的相关条目,实测复现;宁用零分平局先验。
|
|
422
|
+
const contentChars = (s) => String(s || '').replace(/[\d\p{P}\s_]+/gu, ' ')
|
|
423
|
+
const relScore = (h) => frTitleOverlap(contentChars(taskContext), contentChars(`${h.id} ${h.title}`))
|
|
424
|
+
const byOverlapDesc = (a, b) => (relScore(b) - relScore(a)) || ((b.deathPath ? 1 : 0) - (a.deathPath ? 1 : 0))
|
|
309
425
|
const decisionHits = [
|
|
310
|
-
...allDecisionHits.filter(h => h.status === 'rejected'),
|
|
311
|
-
...allDecisionHits.filter(h => h.status !== 'rejected')
|
|
312
|
-
]
|
|
426
|
+
...allDecisionHits.filter((h) => h.status === 'rejected' || h.deathPath).sort(byOverlapDesc),
|
|
427
|
+
...allDecisionHits.filter((h) => h.status !== 'rejected' && !h.deathPath).sort(byOverlapDesc),
|
|
428
|
+
].map((h) => ({ ...h, score: relScore(h) }))
|
|
313
429
|
|
|
314
430
|
return { matched: true, entries: matched, report, json, decisionHits }
|
|
315
431
|
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 同步检索入口(既有行为不变):路由 tag 命中 →(零命中)本地词片复现回退 → 空。
|
|
435
|
+
* 异步三层入口(平台向量优先于本地回退)见 knowledge-vector.js matchKnowledgeHybrid
|
|
436
|
+
* (2026-09-29-knowledge-vector-recall)——同步调用方与既有测试全部走本函数。
|
|
437
|
+
*/
|
|
438
|
+
export function matchKnowledge(indexDir, taskContext) {
|
|
439
|
+
const r = matchByRouting(indexDir, taskContext)
|
|
440
|
+
if (r.matched) return r
|
|
441
|
+
const fb = fallbackByQueryShingles(indexDir, taskContext)
|
|
442
|
+
return fb || r
|
|
443
|
+
}
|
package/src/knowledge-stats.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* - lastHitAt = 引用该文件的记录 at 最大值(ISO 原样保留;sinceDays 窗口启时时幸存记录
|
|
13
13
|
* 必有可解析 at——readKnowledgeHits 契约滤掉 at 缺失/坏值,'' 兜底仅防御性);
|
|
14
14
|
* - totalInjects / totalClassifies = 窗口内两类记录条数(按记录计,与 matrix 按条目计独立);
|
|
15
|
+
* - lastEventAt = 全量流(不加窗口)最新可解析 at 的原样 ISO(resolveLastEventAt,与
|
|
16
|
+
* matrix 的窗口内 lastHitAt 口径分离——「数据截至」不随窗口缩小回退);无有效记录 null;
|
|
15
17
|
* - 未知 type 不进矩阵不进计数(前向兼容:新事件类型不炸聚合)。
|
|
16
18
|
*
|
|
17
19
|
* neverHit:parseKnowledgeIndex(src/knowledge-match.js 同款解析)得 INDEX.md 唯一 file 全集
|
|
@@ -128,6 +130,15 @@ export function buildFrIndexStats(knowledgeDir, runtimeDir, { sinceDays = 30 } =
|
|
|
128
130
|
const frInjectBySource = new Map()
|
|
129
131
|
const rotSuspectByDomain = new Map()
|
|
130
132
|
const rotSuspectChanges = new Set()
|
|
133
|
+
// 条目级聚合(2026-10-03-fr-governance-telemetry):frIds/ids 事件 → id → {suspect, unreferenced}
|
|
134
|
+
// ——L3 裁决候选视图数据面;存量无字段事件自然跳过(向后兼容零破坏)。
|
|
135
|
+
const adjudicationById = new Map()
|
|
136
|
+
const bumpId = (id, key) => {
|
|
137
|
+
if (typeof id !== 'string' || !id) return
|
|
138
|
+
const row = adjudicationById.get(id) || { id, suspect: 0, unreferenced: 0 }
|
|
139
|
+
row[key] += 1
|
|
140
|
+
adjudicationById.set(id, row)
|
|
141
|
+
}
|
|
131
142
|
let frInject = 0
|
|
132
143
|
let frSupersede = 0
|
|
133
144
|
let frDuplicateWarning = 0
|
|
@@ -155,6 +166,7 @@ export function buildFrIndexStats(knowledgeDir, runtimeDir, { sinceDays = 30 } =
|
|
|
155
166
|
row.count += Number.isFinite(Number(r.count)) ? Number(r.count) : 0
|
|
156
167
|
rotSuspectByDomain.set(domain, row)
|
|
157
168
|
}
|
|
169
|
+
for (const id of Array.isArray(r.frIds) ? r.frIds : []) bumpId(id, 'suspect')
|
|
158
170
|
if (r.change) rotSuspectChanges.add(r.change)
|
|
159
171
|
} else if (r.type === 'fr-unreferenced') {
|
|
160
172
|
const domain = r.domain || 'unknown'
|
|
@@ -162,6 +174,7 @@ export function buildFrIndexStats(knowledgeDir, runtimeDir, { sinceDays = 30 } =
|
|
|
162
174
|
row.events += 1
|
|
163
175
|
row.count += Number.isFinite(Number(r.count)) ? Number(r.count) : 0
|
|
164
176
|
unreferencedByDomain.set(domain, row)
|
|
177
|
+
for (const id of Array.isArray(r.ids) ? r.ids : []) bumpId(id, 'unreferenced')
|
|
165
178
|
}
|
|
166
179
|
}
|
|
167
180
|
|
|
@@ -174,8 +187,18 @@ export function buildFrIndexStats(knowledgeDir, runtimeDir, { sinceDays = 30 } =
|
|
|
174
187
|
const denominator = sourceChanges.size
|
|
175
188
|
const supersedeRate = denominator > 0 ? supersedeChanges.size / denominator : null
|
|
176
189
|
|
|
190
|
+
// 裁决候选(2026-10-03-fr-governance-telemetry):id 级聚合 → 候选清单(附来源变更名——
|
|
191
|
+
// scanFrIndex entries 的 id→change join)。候选非裁决:高 suspect 天然来自热文件高频触达,
|
|
192
|
+
// 处置出口在渲染指引(承接翻链/人工退休)。存量无 id 事件不进聚合(视图零噪音)。
|
|
193
|
+
const changeById = new Map()
|
|
194
|
+
for (const e of entries) if (e.id && e.change) changeById.set(e.id, e.change)
|
|
195
|
+
const adjudicationCandidates = [...adjudicationById.values()]
|
|
196
|
+
.map((row) => ({ ...row, change: changeById.get(row.id) || null }))
|
|
197
|
+
.sort((a, b) => ((b.suspect + b.unreferenced) - (a.suspect + a.unreferenced)) || a.id.localeCompare(b.id))
|
|
198
|
+
|
|
177
199
|
return {
|
|
178
200
|
present: frDirExists || frEvents.length > 0,
|
|
201
|
+
adjudicationCandidates,
|
|
179
202
|
events: {
|
|
180
203
|
frInject,
|
|
181
204
|
frInjectBySource: Object.fromEntries(frInjectBySource),
|
|
@@ -200,6 +223,33 @@ export function buildFrIndexStats(knowledgeDir, runtimeDir, { sinceDays = 30 } =
|
|
|
200
223
|
}
|
|
201
224
|
}
|
|
202
225
|
|
|
226
|
+
// ── 新鲜度读数(2026-10-05-knowledge-stats-freshness)──
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 全量遥测流最后写入时间(数据新鲜度读数)。
|
|
230
|
+
*
|
|
231
|
+
* 独立于 sinceDays 窗口口径:窗口问「近 N 天统计了多少」,本函数问「这条流最后一次写入
|
|
232
|
+
* 是什么时候」——窗口缩小不该让「数据截至」跟着回退,故只收 runtimeDir 不收窗口参数。
|
|
233
|
+
* at 缺失/不可解析的记录不参与取最大(与 readKnowledgeHits 窗口过滤跳过同类);全部无效
|
|
234
|
+
* 或零记录 → null(消费方以此区分「有遥测」与「无遥测」两个展示分支)。
|
|
235
|
+
*
|
|
236
|
+
* @param {string} runtimeDir - .runtime 目录(knowledge-hits.jsonl 落点)
|
|
237
|
+
* @returns {string|null} 最新记录 at 的原样 ISO 字符串;无有效记录 null
|
|
238
|
+
*/
|
|
239
|
+
export function resolveLastEventAt(runtimeDir) {
|
|
240
|
+
let best = null
|
|
241
|
+
let bestTs = -Infinity
|
|
242
|
+
for (const record of readKnowledgeHits(runtimeDir)) {
|
|
243
|
+
const ts = Date.parse(record.at)
|
|
244
|
+
// >= 同刻取落盘更后者,与 buildHitMatrix touch 的 lastTs 口径一致
|
|
245
|
+
if (Number.isFinite(ts) && ts >= bestTs) {
|
|
246
|
+
bestTs = ts
|
|
247
|
+
best = String(record.at)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return best
|
|
251
|
+
}
|
|
252
|
+
|
|
203
253
|
// ── CLI 入口(形态对齐 src/stages/knowledge.js cmdSearch / src/knowledge-classify.js cmdKnowledgeClassify)──
|
|
204
254
|
|
|
205
255
|
function outputJson(ok, data, error) {
|
|
@@ -213,7 +263,7 @@ function outputJson(ok, data, error) {
|
|
|
213
263
|
*
|
|
214
264
|
* 用法:stats [--since-days N] [--json]
|
|
215
265
|
* 默认人类可读输出(命中矩阵表 + 从未命中清单 + 遥测计数);--json 输出结构化结果
|
|
216
|
-
* ({ ok, sinceDays, hasTelemetry, matrix, neverHit, totalInjects, totalClassifies })。
|
|
266
|
+
* ({ ok, sinceDays, hasTelemetry, lastEventAt, matrix, neverHit, totalInjects, totalClassifies })。
|
|
217
267
|
* 纯只读。
|
|
218
268
|
*
|
|
219
269
|
* @param {string} dir - 项目根目录
|
|
@@ -239,15 +289,18 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
239
289
|
}
|
|
240
290
|
sinceDays = n
|
|
241
291
|
}
|
|
242
|
-
|
|
292
|
+
// --json 双路判定(对齐 stages/knowledge.js digest 惯例):全局旗标被 index.js 顶层吞进
|
|
293
|
+
// opts.json(filteredArgs 不含 --json),args.includes 仅兜底直调/测试场景
|
|
294
|
+
const asJson = args.includes('--json') || opts.json === true
|
|
243
295
|
|
|
244
296
|
// 遥测在场判定用全量读取(不加窗口):文件缺失/只有坏行 → 无遥测
|
|
245
297
|
const hasTelemetry = readKnowledgeHits(runtimeDir).length > 0
|
|
298
|
+
const lastEventAt = resolveLastEventAt(runtimeDir)
|
|
246
299
|
const result = buildHitMatrix(knowledgeDir, runtimeDir, { sinceDays })
|
|
247
300
|
const frIndex = buildFrIndexStats(knowledgeDir, runtimeDir, { sinceDays })
|
|
248
301
|
|
|
249
302
|
if (asJson) {
|
|
250
|
-
outputJson(true, { sinceDays, hasTelemetry, ...result, frIndex })
|
|
303
|
+
outputJson(true, { sinceDays, hasTelemetry, lastEventAt, ...result, frIndex })
|
|
251
304
|
return
|
|
252
305
|
}
|
|
253
306
|
|
|
@@ -281,7 +334,7 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
281
334
|
lines.push('')
|
|
282
335
|
}
|
|
283
336
|
|
|
284
|
-
lines.push(`遥测计数(窗口内):注入 ${result.totalInjects} 次 | 归类 ${result.totalClassifies}
|
|
337
|
+
lines.push(`遥测计数(窗口内):注入 ${result.totalInjects} 次 | 归类 ${result.totalClassifies} 次${lastEventAt ? ` | 数据截至 ${lastEventAt.slice(0, 10)}` : ''}`)
|
|
285
338
|
|
|
286
339
|
// FR 索引实验(L3 裁决仪表盘——证伪条款出数面;裁决时建议 --since-days 大窗口读全口径)
|
|
287
340
|
if (frIndex.present) {
|
|
@@ -296,6 +349,15 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
296
349
|
}
|
|
297
350
|
lines.push(` 索引面:${i.entries} 条(active ${i.active} / superseded ${i.superseded})| 来源变更 ${i.sourceChanges} 个 | 域 ${i.domains.join(', ') || '—'}`)
|
|
298
351
|
lines.push(` 承接引用率:${frIndex.supersedeRate === null ? '—(索引空)' : `${(frIndex.supersedeRate * 100).toFixed(0)}%(${e.frSupersedeChanges}/${i.sourceChanges},分子窗口内/分母全量——裁决用大窗口)`}`)
|
|
352
|
+
// 裁决候选视图(2026-10-03-fr-governance-telemetry):条目级 id 聚合(事件 frIds/ids,帽 20 抽样
|
|
353
|
+
// 为下界)。候选非裁决——高 suspect 天然来自热文件;处置出口:确认无人承接 → 承接翻链或人工退休。
|
|
354
|
+
const cand = frIndex.adjudicationCandidates || []
|
|
355
|
+
if (cand.length > 0) {
|
|
356
|
+
lines.push(' 裁决候选 Top-10(suspect×unref 条目级聚合——候选非裁决,确认无人承接→承接翻链或人工退休):')
|
|
357
|
+
for (const c of cand.slice(0, 10)) {
|
|
358
|
+
lines.push(` - ${c.id} suspect×${c.suspect} unref×${c.unreferenced}${c.change ? `(来源 ${c.change})` : ''}`)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
299
361
|
}
|
|
300
362
|
|
|
301
363
|
console.log(lines.join('\n'))
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* knowledge-vector.js — 平台向量召回层(CLI 侧,2026-09-29-knowledge-vector-recall)
|
|
3
|
+
*
|
|
4
|
+
* 检索三层:路由 tag 命中 →(零命中)平台向量召回(本模块,已连接平台时)→ 本地词片
|
|
5
|
+
* 复现窗口(knowledge-match.js fallbackByQueryShingles)→ 空。
|
|
6
|
+
*
|
|
7
|
+
* 原则(用户裁决 2026-09-29):平台只做语义召回——返回「哪个文件哪个条目」的候选
|
|
8
|
+
* (spec_path+anchor+score);条目 status/deathPath/回显资格等全部策略面在本地解析
|
|
9
|
+
* (本地文件是真相源,向量结果不携带策略语义)。CLI 本地零模型约束不变。
|
|
10
|
+
*
|
|
11
|
+
* 端点契约(SillyHub 侧实现规格,测试夹具按此 mock):
|
|
12
|
+
* POST {platform.url}/api/spec/knowledge/vector-search
|
|
13
|
+
* Headers: Authorization: Bearer <token>; Content-Type: application/json
|
|
14
|
+
* Body: { "query": "<检索文本>", "limit": 10 }
|
|
15
|
+
* 200 → { "ok": true, "results": [
|
|
16
|
+
* { "spec_path": "knowledge/decisions/unmapped.md", "anchor": "D-001@v1",
|
|
17
|
+
* "change": "2026-09-26-thin-agent-tasks", "score": 0.83 } ] }
|
|
18
|
+
* anchor = 决策条目 id(## D-xxx@vN);change = 条目「变更:」字段——同号条目跨变更常见
|
|
19
|
+
* (runtime.md 七个 D-001@v1 先例),change 在场时消歧;缺省时同号条目全量带回(候选诚实)。
|
|
20
|
+
*
|
|
21
|
+
* 降级纪律:任何失败(未连接/开关关闭/网络/404/5xx/超时/响应形态不符)静默降级本地层
|
|
22
|
+
* ——检索是 advisory 面且高频(每方案步 --done),失败刷屏即狼来了;debug 排障可设
|
|
23
|
+
* SILLYSPEC_KNOWLEDGE_VECTOR_DEBUG=1 打单行stderr。
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync } from 'fs'
|
|
26
|
+
import { join, dirname } from 'path'
|
|
27
|
+
import { readPlatformConfig } from './sync.js'
|
|
28
|
+
import { matchByRouting, fallbackByQueryShingles, parseDecisionEntries } from './knowledge-match.js'
|
|
29
|
+
|
|
30
|
+
const VECTOR_TIMEOUT_MS = 3000
|
|
31
|
+
const ENDPOINT_PATH = '/api/spec/knowledge/vector-search'
|
|
32
|
+
|
|
33
|
+
/** local.yaml knowledge.vector_search 开关(行级扫描 knowledge: 段下 vector_search: off)。 */
|
|
34
|
+
function vectorSwitchFromLocalYaml(cwd) {
|
|
35
|
+
try {
|
|
36
|
+
const lines = readFileSync(join(cwd, '.sillyspec', 'local.yaml'), 'utf-8').split(/\r?\n/)
|
|
37
|
+
let inKnowledge = false
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
const top = line.match(/^([A-Za-z_][\w-]*):\s*$/)
|
|
40
|
+
if (top) { inKnowledge = top[1] === 'knowledge'; continue }
|
|
41
|
+
if (!inKnowledge) continue
|
|
42
|
+
const kv = line.match(/^\s+vector_search\s*:\s*(\S+)/)
|
|
43
|
+
if (kv) return kv[1].toLowerCase()
|
|
44
|
+
}
|
|
45
|
+
} catch { /* 无 local.yaml = 缺省 auto */ }
|
|
46
|
+
return 'auto'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function vectorEnabled(cwd) {
|
|
50
|
+
const env = String(process.env.SILLYSPEC_KNOWLEDGE_VECTOR || '').toLowerCase()
|
|
51
|
+
if (env === 'off' || env === 'auto') return env === 'auto'
|
|
52
|
+
return vectorSwitchFromLocalYaml(cwd) !== 'off'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 平台向量召回(纯 HTTP 层)。未连接/失败/形态不符 → null(调用方降级)。
|
|
57
|
+
* @returns {Promise<{results: Array<{spec_path: string, anchor: string, score: number}>}|null>}
|
|
58
|
+
*/
|
|
59
|
+
export async function platformVectorRecall({ cwd, query, limit = 10, timeoutMs = VECTOR_TIMEOUT_MS }) {
|
|
60
|
+
const cfg = readPlatformConfig(cwd)
|
|
61
|
+
if (!cfg || !cfg.url || !cfg.token) return null
|
|
62
|
+
const q = String(query || '').trim().slice(0, 4000)
|
|
63
|
+
if (!q) return null
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(`${String(cfg.url).replace(/\/+$/, '')}${ENDPOINT_PATH}`, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { Authorization: `Bearer ${cfg.token}`, 'Content-Type': 'application/json' },
|
|
68
|
+
body: JSON.stringify({ query: q, limit }),
|
|
69
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
70
|
+
})
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
if (process.env.SILLYSPEC_KNOWLEDGE_VECTOR_DEBUG) console.error(`[knowledge-vector] ${res.status} ${ENDPOINT_PATH}——降级本地`)
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
const body = await res.json().catch(() => null)
|
|
76
|
+
const results = body && Array.isArray(body.results)
|
|
77
|
+
? body.results.filter((r) => r && typeof r.spec_path === 'string' && typeof r.anchor === 'string')
|
|
78
|
+
: []
|
|
79
|
+
return results.length > 0 ? { results } : null
|
|
80
|
+
} catch {
|
|
81
|
+
if (process.env.SILLYSPEC_KNOWLEDGE_VECTOR_DEBUG) console.error(`[knowledge-vector] 请求失败/超时——降级本地`)
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** spec_path(knowledge/decisions/x.md)→ INDEX 相对 file(decisions/x.md)。 */
|
|
87
|
+
function specPathToFile(specPath) {
|
|
88
|
+
return String(specPath).replace(/^knowledge\//, '').replace(/\\/g, '/')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 平台候选 → matchKnowledge 同构结果(策略面全本地解析)。 */
|
|
92
|
+
function buildResultFromPlatform(indexDir, pvResults) {
|
|
93
|
+
const byFile = new Map()
|
|
94
|
+
for (const r of pvResults) {
|
|
95
|
+
const file = specPathToFile(r.spec_path)
|
|
96
|
+
if (!file.startsWith('decisions/')) continue
|
|
97
|
+
if (!byFile.has(file)) byFile.set(file, [])
|
|
98
|
+
byFile.get(file).push(r)
|
|
99
|
+
}
|
|
100
|
+
if (byFile.size === 0) return null
|
|
101
|
+
const all = parseDecisionEntries(indexDir)
|
|
102
|
+
const entries = []
|
|
103
|
+
const decisionHits = []
|
|
104
|
+
for (const [file, rs] of byFile) {
|
|
105
|
+
const wanted = new Set(rs.map((r) => r.anchor))
|
|
106
|
+
const fileEntries = all.filter((h) => h.file === file && wanted.has(h.id))
|
|
107
|
+
if (fileEntries.length === 0) continue
|
|
108
|
+
for (const r of rs) {
|
|
109
|
+
// 同号消歧:change 在场取精确条目;缺省同号全量带回(跨变更同号是已知形态)
|
|
110
|
+
const matched = r.change
|
|
111
|
+
? fileEntries.filter((e) => e.id === r.anchor && e.change === r.change)
|
|
112
|
+
: fileEntries.filter((e) => e.id === r.anchor)
|
|
113
|
+
for (const h of matched.length > 0 ? matched : []) {
|
|
114
|
+
decisionHits.push({ ...h, score: typeof r.score === 'number' ? r.score : 1 })
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
entries.push({
|
|
118
|
+
category: 'Decisions',
|
|
119
|
+
keywords: ['vector'],
|
|
120
|
+
file,
|
|
121
|
+
anchor: rs[0].anchor,
|
|
122
|
+
display: `${file}(平台向量召回)`,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
if (decisionHits.length === 0) return null
|
|
126
|
+
decisionHits.sort((a, b) =>
|
|
127
|
+
(b.score - a.score) ||
|
|
128
|
+
(((b.status === 'rejected' || b.deathPath) ? 1 : 0) - ((a.status === 'rejected' || a.deathPath) ? 1 : 0))
|
|
129
|
+
)
|
|
130
|
+
// 缺 change 的同号锚点全量带回可放大(unmapped 实测 65 个 D-001@v1)——score 序封顶 20
|
|
131
|
+
if (decisionHits.length > 20) decisionHits.length = 20
|
|
132
|
+
return {
|
|
133
|
+
matched: true,
|
|
134
|
+
entries: entries.slice(0, 3),
|
|
135
|
+
report: `Status: platform-vector matched (${decisionHits.length} entries)`,
|
|
136
|
+
json: {
|
|
137
|
+
matched: true, entry_count: Math.min(entries.length, 3), vector: true,
|
|
138
|
+
entries: entries.slice(0, 3).map((e) => ({ file: e.file, anchor: e.anchor, keywords: e.keywords, category: e.category })),
|
|
139
|
+
},
|
|
140
|
+
decisionHits,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 异步三层检索入口(四消费方使用:flow 注入段/complete 门/prompt {DECISION_HITS}/
|
|
146
|
+
* knowledge search CLI):路由 → 平台向量(已连接且开关开)→ 本地词片 → 空。
|
|
147
|
+
* opts.cwd = 仓根(平台配置与 local.yaml 定位用);缺省由 indexDir 上推两级。
|
|
148
|
+
*/
|
|
149
|
+
export async function matchKnowledgeHybrid(indexDir, taskContext, opts = {}) {
|
|
150
|
+
const r = matchByRouting(indexDir, taskContext)
|
|
151
|
+
if (r.matched) return r
|
|
152
|
+
const cwd = opts.cwd || dirname(dirname(String(indexDir)))
|
|
153
|
+
if (vectorEnabled(cwd)) {
|
|
154
|
+
const pv = await platformVectorRecall({ cwd, query: taskContext, limit: opts.limit || 10 })
|
|
155
|
+
const built = pv ? buildResultFromPlatform(indexDir, pv.results) : null
|
|
156
|
+
if (built) return built
|
|
157
|
+
}
|
|
158
|
+
const fb = fallbackByQueryShingles(indexDir, taskContext)
|
|
159
|
+
return fb || r
|
|
160
|
+
}
|
package/src/machine-interface.js
CHANGED
|
@@ -141,7 +141,7 @@ function greenCachedCheck({ kind, greenRoot, cwd, specBase, changeName }, runRea
|
|
|
141
141
|
return result;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, specDriftAnchor, ctx = null, full = false } = {}) {
|
|
144
|
+
export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, specDriftAnchor, ctx = null, full = false, docsOnly = false } = {}) {
|
|
145
145
|
const specRoot = specBase || resolveSpecDir(cwd);
|
|
146
146
|
// A4:pm 用 specRoot(而非默认 resolveSpecDir(cwd))——--spec-dir/平台模式下 specBase 是真实
|
|
147
147
|
// specDir,默认解析会指向本地孤儿库,与下方 planContent/changeDir 的 specRoot 两套事实源混拼。
|
|
@@ -312,7 +312,28 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
// ── d. verify 阶段追加 verify-test(CLI 实测 local.yaml commands.test)──
|
|
315
|
-
|
|
315
|
+
// docsOnly 预检档(2026-10-06-verify-docs-prefill task-01):纯文档契约缺项(visual-evidence/
|
|
316
|
+
// 结论槽/移交项等)毫秒级可查,却因 gate verify 含测试执行而整体变贵(postmortem 实证
|
|
317
|
+
// --json 超时转后台)——本档跳过 test/lint 执行(informational 占位明示),其余检查照跑;
|
|
318
|
+
// 测试客观核验仍以完整 gate verify / verify --done 为准。
|
|
319
|
+
if (stage === 'verify' && docsOnly) {
|
|
320
|
+
checks.push({
|
|
321
|
+
id: 'verify-test',
|
|
322
|
+
code: checkCode('verify-test'),
|
|
323
|
+
ok: true,
|
|
324
|
+
informational: true,
|
|
325
|
+
warnings: ['verify-test 跳过(--docs-only 预检档)——本档只核文档/契约面不执行测试;测试客观核验以完整 gate verify(不带 --docs-only)或 verify --done 为准'],
|
|
326
|
+
data: { status: 'docs-only-skip' },
|
|
327
|
+
});
|
|
328
|
+
checks.push({
|
|
329
|
+
id: 'verify-lint',
|
|
330
|
+
code: checkCode('verify-lint'),
|
|
331
|
+
ok: true,
|
|
332
|
+
informational: true,
|
|
333
|
+
warnings: ['verify-lint 跳过(--docs-only 预检档)——同上,lint 客观核验以完整 gate 或 verify --done 为准'],
|
|
334
|
+
data: { status: 'docs-only-skip' },
|
|
335
|
+
});
|
|
336
|
+
} else if (stage === 'verify') {
|
|
316
337
|
// 绿缓存根:与 runtimeRoot 解析同源(drift 锚主仓口径);解析失败 null → 不缓存不命中
|
|
317
338
|
let greenRoot = null;
|
|
318
339
|
try { greenRoot = resolveRuntimeRoot({ runtimeRoot, specDriftAnchor }, specRoot); } catch { greenRoot = null; }
|
|
@@ -473,6 +494,32 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
473
494
|
}
|
|
474
495
|
}
|
|
475
496
|
|
|
497
|
+
// ── e3. stage-review 默认档提示(2026-10-07-flow-friction-batch3 task-01)── postmortem
|
|
498
|
+
// (provider-model-list):gate execute 默认档不含 Stage Review 检查(在 --full 档),
|
|
499
|
+
// agent 跑 gate 见 ok、--done 却被 Stage Review Gate 拦。默认档补 informational 探测
|
|
500
|
+
// (同一 getLatestStageReviewRunId 只读 marker 存在性,秒级):缺 review → warning 带
|
|
501
|
+
// register 指引;在场 → 静默 ok。独立 check id 不与 --full 的 full-stage-review 碰撞,
|
|
502
|
+
// error 语义(--done 拦)仍归 --full 档与 --done 本体。
|
|
503
|
+
if (!full && (stage === 'verify' || stage === 'execute')) {
|
|
504
|
+
try {
|
|
505
|
+
const { getLatestStageReviewRunId } = await import('./stage-review.js');
|
|
506
|
+
const rrRoot = runtimeRoot || join(specRoot, '.runtime');
|
|
507
|
+
const runId = getLatestStageReviewRunId(rrRoot, stage, changeName);
|
|
508
|
+
checks.push({
|
|
509
|
+
id: 'stage-review-hint',
|
|
510
|
+
code: 'stage_review_hint',
|
|
511
|
+
ok: true,
|
|
512
|
+
informational: true,
|
|
513
|
+
warnings: runId ? [] : [
|
|
514
|
+
`缺 ${stage} 阶段的 stage review.json——verify/execute --done 的 Stage Review Gate 会拦(本提示不改变 gate 结论)。先产出评审再注册:sillyspec register-stage-review --change ${changeName} --stage ${stage}(骨架生成后填,或 --from review.json 过继独立评审产出)`,
|
|
515
|
+
],
|
|
516
|
+
data: { runId: runId || null },
|
|
517
|
+
});
|
|
518
|
+
} catch (e) {
|
|
519
|
+
// fail-open:探测装配失败不留痕(默认档零行为变化承诺)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
476
523
|
// ── 综合结论:所有非 informational check 均 ok ──
|
|
477
524
|
const ok = checks.filter((c) => !c.informational).every((c) => c.ok);
|
|
478
525
|
const exitCode = ok ? EXIT_OK : EXIT_BLOCKED;
|
|
@@ -854,3 +901,44 @@ export function writeGateResultArtifact({ specBase, stage, changeName, envelope
|
|
|
854
901
|
return null // fail-soft:落盘是审计附件,失败只缺附件不误门
|
|
855
902
|
}
|
|
856
903
|
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* gate last 的读取面(2026-10-06-verify-friction-fix task-02):writeVerifyGatePointer(gates.js
|
|
907
|
+
* quick-D)只写不读——看上轮 verify --done blocked 原因只能重跑全量 gate(超时转后台)或手翻
|
|
908
|
+
* 两层 JSON。本函数读稳定指针并带上其指向取证目录里的 reconcile/probe-consistency 明细。
|
|
909
|
+
* run 目录用指针内 latest_run_dir 的 basename 在当前根下重推导(指针里的绝对路径可能来自
|
|
910
|
+
* 另一机器/根,跨环境直接拼会静默指空)。fail-soft:指针缺失 → found:false;损坏 → unreadable。
|
|
911
|
+
* @param {{ specBase: string, changeName: string }} opts
|
|
912
|
+
* @returns {{ found: boolean, pointerPath: string, unreadable?: boolean, error?: string } &
|
|
913
|
+
* Partial<{ blocked: boolean, note: string, latest_run_dir: string, run_dir_resolved: string,
|
|
914
|
+
* written_at: string, reconcile: object, probeConsistency: object }>}
|
|
915
|
+
*/
|
|
916
|
+
export function summarizeVerifyGatePointer({ specBase, changeName }) {
|
|
917
|
+
const runsDir = join(specBase, '.runtime', 'verify-runs')
|
|
918
|
+
const pointerPath = join(runsDir, `gate-last-${changeName || 'change'}.json`)
|
|
919
|
+
if (!existsSync(pointerPath)) return { found: false, pointerPath }
|
|
920
|
+
let pointer
|
|
921
|
+
try {
|
|
922
|
+
pointer = JSON.parse(readFileSync(pointerPath, 'utf8'))
|
|
923
|
+
} catch (e) {
|
|
924
|
+
return { found: true, unreadable: true, pointerPath, error: e.message }
|
|
925
|
+
}
|
|
926
|
+
const out = { found: true, pointerPath, ...pointer }
|
|
927
|
+
const runDirName = pointer.latest_run_dir
|
|
928
|
+
? String(pointer.latest_run_dir).split(/[\\/]/).filter(Boolean).pop()
|
|
929
|
+
: null
|
|
930
|
+
if (runDirName) {
|
|
931
|
+
const runDir = join(runsDir, runDirName)
|
|
932
|
+
out.run_dir_resolved = runDir
|
|
933
|
+
for (const [key, file] of [['reconcile', 'reconcile-result.json'], ['probeConsistency', 'probe-consistency-result.json']]) {
|
|
934
|
+
const p = join(runDir, file)
|
|
935
|
+
if (!existsSync(p)) continue
|
|
936
|
+
try {
|
|
937
|
+
out[key] = JSON.parse(readFileSync(p, 'utf8'))
|
|
938
|
+
} catch (e) {
|
|
939
|
+
out[key] = { unreadable: true, error: e.message }
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return out
|
|
944
|
+
}
|
package/src/module-impact.js
CHANGED
|
@@ -26,9 +26,12 @@ import { resolveSpecDir } from './run/shared.js'
|
|
|
26
26
|
export function parseModuleMapPaths(yamlText) {
|
|
27
27
|
const map = new Map()
|
|
28
28
|
if (!yamlText) return map
|
|
29
|
+
// CRLF 归一(Windows 生成的 map:块式行靠 \s*$/.trim() 蒙混,内联数组正则恒失配——
|
|
30
|
+
// sillyhub-daemon 整图内联 CRLF 实证漏 343 条)
|
|
31
|
+
const text = String(yamlText).replace(/\r\n/g, '\n')
|
|
29
32
|
let currentModule = null
|
|
30
33
|
let inPaths = false
|
|
31
|
-
for (const line of
|
|
34
|
+
for (const line of text.split('\n')) {
|
|
32
35
|
if (!line.trim() || line.trim().startsWith('#')) continue
|
|
33
36
|
if (!/^\s/.test(line)) {
|
|
34
37
|
inPaths = line.startsWith('modules:')
|
|
@@ -41,6 +44,18 @@ export function parseModuleMapPaths(yamlText) {
|
|
|
41
44
|
inPaths = false
|
|
42
45
|
continue
|
|
43
46
|
}
|
|
47
|
+
// 内联 paths 数组(ground truth 审计实证:sillyhub-daemon 整图内联写法,本解析器此前
|
|
48
|
+
// 恒漏 343 条 paths=归属全盲——对齐 parseModulePathsSubset / parseModuleMapSimple 的既有内联支持)
|
|
49
|
+
const inlinePaths = currentModule && line.match(/^ paths: \[(.*)\]$/)
|
|
50
|
+
if (inlinePaths) {
|
|
51
|
+
if (!map.has(currentModule)) map.set(currentModule, [])
|
|
52
|
+
for (const v of inlinePaths[1].split(',').map(s => s.trim()).filter(Boolean)) {
|
|
53
|
+
const unquoted = v.replace(/^(['"])([\s\S]*)\1$/, '$2')
|
|
54
|
+
map.get(currentModule).push(unquoted.replace(/\\/g, '/'))
|
|
55
|
+
}
|
|
56
|
+
inPaths = false
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
44
59
|
if (currentModule && /^ paths:\s*$/.test(line)) {
|
|
45
60
|
inPaths = true
|
|
46
61
|
if (!map.has(currentModule)) map.set(currentModule, [])
|
|
@@ -56,8 +71,14 @@ export function parseModuleMapPaths(yamlText) {
|
|
|
56
71
|
const raw = item[1].trim()
|
|
57
72
|
const unquoted = raw.replace(/^(['"])([\s\S]*)\1$/, '$2')
|
|
58
73
|
map.get(currentModule).push(unquoted.replace(/\\/g, '/'))
|
|
74
|
+
continue
|
|
59
75
|
}
|
|
60
76
|
}
|
|
77
|
+
// 结构守卫(坑 module-map-list-leak,2026-10-06-wallclock-entry 实测发现):paths 收集只在
|
|
78
|
+
// 「6 缩进项行 / 空行 / 注释」上存活,其余任何行——字段头(字段名开放集不枚举)、顶层段
|
|
79
|
+
// (span_risk:/blast:)、异缩进项(2 缩进 "- prefixes:")——终结收集,防 tags/aliases/
|
|
80
|
+
// depends_on 字段项与顶层 blast prefixes 漏进 paths 污染 classifyFile 归属分类。
|
|
81
|
+
inPaths = false
|
|
61
82
|
}
|
|
62
83
|
return map
|
|
63
84
|
}
|