sillyspec 3.33.7 → 3.33.9

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.
Files changed (41) hide show
  1. package/AGENTS.md +3 -2
  2. package/eval/knowledge/README.md +119 -0
  3. package/eval/knowledge/baseline.json +907 -0
  4. package/eval/knowledge/datasets/organic.jsonl +79 -0
  5. package/eval/knowledge/fixtures/index.mjs +132 -0
  6. package/eval/knowledge/lib/dataset.mjs +171 -0
  7. package/eval/knowledge/lib/engine.mjs +136 -0
  8. package/eval/knowledge/lib/faces.mjs +88 -0
  9. package/eval/knowledge/lib/metrics.mjs +56 -0
  10. package/eval/knowledge/lib/probes.mjs +98 -0
  11. package/eval/knowledge/lib/report.mjs +82 -0
  12. package/eval/knowledge/lib/repos.mjs +41 -0
  13. package/eval/knowledge/lib/snapshot.mjs +55 -0
  14. package/eval/knowledge/run.mjs +206 -0
  15. package/package.json +3 -2
  16. package/src/change-delete.js +24 -1
  17. package/src/checkpoint.js +258 -0
  18. package/src/config-schema.js +36 -0
  19. package/src/flow-parity.js +10 -50
  20. package/src/flow-review.js +4 -0
  21. package/src/flow.js +44 -10
  22. package/src/foreign-declared.js +2 -1
  23. package/src/index.js +100 -46
  24. package/src/knowledge-graph.js +243 -25
  25. package/src/knowledge-match.js +130 -21
  26. package/src/knowledge-stats.js +3 -0
  27. package/src/knowledge-vector.js +29 -4
  28. package/src/overlap-detect.js +68 -0
  29. package/src/redomain-unmapped.js +163 -2
  30. package/src/run/complete.js +20 -1
  31. package/src/run/gate-snapshot.js +3 -1
  32. package/src/run/prompt.js +67 -37
  33. package/src/run/quick-audit.js +58 -10
  34. package/src/run/shared.js +7 -1
  35. package/src/run/verify-quality-scan.js +52 -7
  36. package/src/stages/execute.js +64 -16
  37. package/src/stages/knowledge.js +57 -28
  38. package/src/task-tick.js +16 -0
  39. package/src/verify-postcheck.js +330 -3
  40. package/templates/agents-instruction.md +1 -1
  41. package/src/knowledge-quicklog.js +0 -261
@@ -1,261 +0,0 @@
1
- /**
2
- * knowledge-quicklog.js — quicklog 历史检索面(知识可见性导线)
3
- *
4
- * 缺口(2026-09-21 R5R 实证):quick 产出(quicklog 条目 + patch)不在任何检索面——
5
- * knowledge-match 只扫 INDEX 路由与 decisions;主仓唯一核验过键名的先例
6
- * (ql-20260920-007-d0dd)因 linked 变更未归档而 distill 缺席,FR 检索面零覆盖,
7
- * 后续三个独立会话全部猜错同一外部键。本模块把 quicklog 历史拉进注入面:
8
- * 纯读、fail-open、top-3 一行制——注入的是「发生过什么」的历史事实(条目说的是
9
- * 当时做了什么),不是权威断言;消费方要用契约细节按 ql-ID 回源读全文或读代码。
10
- *
11
- * 与 knowledge-match.js 分工:那边消费 INDEX/decisions(结构化知识),这边消费双信号源——
12
- * quicklog/QUICKLOG-*.md(append-only 历史)+ changes/archive/<变更>/flow-state.yaml(flow 族归档
13
- * 变更伪条目,2026-09-23 扩源:flow 变更不产生 quicklog 条目,扩源后检索面才看得见)。
14
- * 条目头与字段行是 allocateQuicklogEntry
15
- * 的机械解析契约:`## ql-<id> | <时间> | <标题>` + `文件:` 段(`- path(注记)` 行)。
16
- * run/prompt.js(execute 确认步 + quick step1)与 stages/execute.js(Wave 孪生)两处
17
- * 注入点共用本模块——格式等价由 test/knowledge-inject.test.mjs 锁定,改渲染必同步两处。
18
- */
19
-
20
- import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
21
- import { join } from 'path'
22
- import { readHotCache, writeHotCache, dirStamp } from './hot-cache.js'
23
-
24
- const QUICKLOG_ENTRY_RE = /^##\s+(ql-[\w-]+)\s*\|\s*([^|]+)\s*\|\s*(.+)$/
25
- // 文件注记行:`- path(中文括注)` / `- path(ascii)` / 裸 `- path`(allocateQuicklogEntry
26
- // 均可产出;path 本身不含空白,故止于首个空白或括号)
27
- const QUICKLOG_FILE_LINE_RE = /^-\s+([^\s((]+)/
28
- // 文件:段结束边界——出现其他字段标签行即收段(状态/需求/根因/方案/结果/审计/关联变更)
29
- const QUICKLOG_FIELD_RE = /^(状态|关联变更|需求|根因|方案|结果|审计)\s*[::]/
30
-
31
- /**
32
- * 解析 quicklog 全量条目——双信号源(资产三小件①,2026-09-23 扩源):
33
- * ① quicklog/QUICKLOG-*.md(append-only 历史条目,含按日归档的分片文件);
34
- * ② .sillyspec/changes/archive/<变更>/flow-state.yaml(flow 族归档变更)——flow 变更不产生
35
- * quicklog 条目,检索面此前看不见 flow 变更的知识(与 ql-011 治的病同族)。伪条目
36
- * 三料合成:qlId=变更目录名 / date=目录名日期前缀(缺前缀退 flow-state.yaml mtime)/
37
- * title=proposal.md 首个 `# ` 标题行(缺文件退变更名);solution 恒空、files 恒空。
38
- * 双源命中时 quicklog 条目排前(matchQuicklogContext 排序 source 键)。
39
- * @param {string} specBase - .sillyspec 根
40
- * @returns {{ qlId: string, date: string, title: string, solution: string, files: string[], source: 'quicklog'|'flow' }[]}
41
- * 两源都不存在 / 全部不可读 → [](调用方零注入零阻断)
42
- */
43
- export function parseQuicklogEntries(specBase) {
44
- return [...parseQuicklogFileEntries(specBase), ...parseFlowArchiveEntries(specBase)]
45
- }
46
-
47
- /** 源①:quicklog/QUICKLOG-*.md 条目解析(原 parseQuicklogEntries 主体,行为不变)。 */
48
- function parseQuicklogFileEntries(specBase) {
49
- const dir = join(specBase, 'quicklog')
50
- let files
51
- try {
52
- files = readdirSync(dir).filter((f) => /^QUICKLOG-.*\.md$/i.test(f))
53
- } catch {
54
- return []
55
- }
56
- const entries = []
57
- for (const f of files) {
58
- let content
59
- try {
60
- content = readFileSync(join(dir, f), 'utf8')
61
- } catch {
62
- continue
63
- }
64
- let cur = null
65
- let inFiles = false
66
- const flush = () => {
67
- if (cur) entries.push(cur)
68
- cur = null
69
- }
70
- for (const line of content.replace(/\r\n/g, '\n').split('\n')) {
71
- const h = line.match(QUICKLOG_ENTRY_RE)
72
- if (h) {
73
- flush()
74
- cur = { qlId: h[1].trim(), date: h[2].trim(), title: h[3].trim(), solution: '', files: [], source: 'quicklog' }
75
- inFiles = false
76
- continue
77
- }
78
- if (!cur) continue
79
- if (QUICKLOG_FIELD_RE.test(line)) {
80
- inFiles = false
81
- const m = line.match(/^方案\s*[::]\s*(.*)$/)
82
- if (m) cur.solution = m[1].trim()
83
- continue
84
- }
85
- if (/^文件\s*[::]/.test(line)) {
86
- inFiles = true
87
- continue
88
- }
89
- if (inFiles) {
90
- const m = line.match(QUICKLOG_FILE_LINE_RE)
91
- if (m) cur.files.push(m[1].replace(/\\/g, '/'))
92
- }
93
- }
94
- flush()
95
- }
96
- return entries
97
- }
98
-
99
- /** 伪条目扫描缓存版本(解析契约变化时 bump 即全量失效)。 */
100
- const FLOW_ARCHIVE_CACHE_VERSION = 1
101
-
102
- /** 源②:flow 归档变更伪条目——目录内 flow-state.yaml 在场即认(不校验 yaml 内容:伪条目
103
- * 只需「这曾是一个 flow 变更」的存在信号,字段全来自目录名与 proposal.md,缺件逐项降级)。
104
- * 跨进程缓存(FR-03,2026-10-21-knowledge-tier-denoise):指纹=归档根子项名全集+根 mtimeMs
105
- * (一次 readdir+一次 stat,廉价每次重算)——命中即免逐目录列举与逐变更 proposal.md 读。
106
- * 归档件是冻结审计面(无就地编辑形态),指纹对增删/改名完备;坏缓存/失配 fail-open 全量重建。
107
- * 导出供测试断言缓存三态(命中/失效/坏缓存)。 */
108
- export function parseFlowArchiveEntries(specBase) {
109
- const archiveDir = join(specBase, 'changes', 'archive')
110
- const fingerprint = dirStamp(archiveDir)
111
- if (fingerprint === null) return []
112
- const cachePath = join(specBase, '.runtime', 'cache', 'quicklog-archive-entries.json')
113
- const cached = readHotCache(cachePath, { version: FLOW_ARCHIVE_CACHE_VERSION, fingerprint })
114
- if (Array.isArray(cached)) return cached
115
-
116
- let dirs
117
- try {
118
- dirs = readdirSync(archiveDir, { withFileTypes: true })
119
- } catch {
120
- return []
121
- }
122
- const out = []
123
- for (const d of dirs) {
124
- if (!d.isDirectory()) continue
125
- try {
126
- const dir = join(archiveDir, d.name)
127
- if (!existsSync(join(dir, 'flow-state.yaml'))) continue
128
- const dm = d.name.match(/^(\d{4}-\d{2}-\d{2})-/)
129
- let date = dm ? dm[1] : ''
130
- if (!date) {
131
- try { date = new Date(statSync(join(dir, 'flow-state.yaml')).mtime).toISOString().slice(0, 10) } catch { /* mtime 不可得 → 空日期(排序自然靠后) */ }
132
- }
133
- let title = ''
134
- try {
135
- const prop = readFileSync(join(dir, 'proposal.md'), 'utf8')
136
- const h = prop.replace(/\r\n/g, '\n').split('\n').find((l) => /^#\s+/.test(l))
137
- if (h) title = h.replace(/^#\s+/, '').trim()
138
- } catch { /* 无 proposal.md → 标题退变更名 */ }
139
- out.push({ qlId: d.name, date, title: title || d.name, solution: '', files: [], source: 'flow' })
140
- } catch { /* 单目录读取异常 → 跳过该目录(fail-open,不影响其余源) */ }
141
- }
142
- writeHotCache(cachePath, { version: FLOW_ARCHIVE_CACHE_VERSION, fingerprint, payload: out })
143
- return out
144
- }
145
-
146
- // ── 匹配打分 ─────────────────────────────────────────────────────────────────
147
- // 查询侧 token:ASCII 词(≥3 字符,词边界匹配)+ CJK 连续段(≥2 字,子串匹配)——
148
- // 与 knowledge-match.keywordMatchesContext 同判法,方向相反(那边条目出关键词查上下文,
149
- // 这边上下文出 token 查条目——quicklog 条目没有 INDEX 关键词行,只能反向)。
150
- const ASCII_TOKEN_RE = /[a-z][a-z0-9._-]{2,}/g
151
- const CJK_RUN_RE = /[\u4e00-\u9fff]{2,}/g
152
-
153
- function contextTokens(taskContext) {
154
- const s = String(taskContext || '').toLowerCase()
155
- const out = new Set()
156
- for (const t of s.match(ASCII_TOKEN_RE) || []) out.add(t)
157
- for (const t of s.match(CJK_RUN_RE) || []) out.add(t)
158
- return [...out]
159
- }
160
-
161
- function escapeRegex(s) {
162
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
163
- }
164
-
165
- /** 文件命中权重:路径/文件名是强信号(同一文件被改过 ≈ 同域修补) */
166
- const FILE_HIT_WEIGHT = 5
167
- /** 命中门槛:≥1 文件命中,或 ≥2 个不同 token 命中(单 token 命中多为噪音词,不注入) */
168
- const MIN_TOKEN_HITS = 2
169
-
170
- /** 源排序权重:quicklog 条目 0(排前),flow 伪条目 1——双源命中时 quicklog 优先(更新鲜)。 */
171
- function sourceRank(entry) {
172
- return entry && entry.source === 'flow' ? 1 : 0
173
- }
174
-
175
- /**
176
- * 任务上下文 × quicklog 条目匹配(双源条目统一打分,排序 source 键优先)。
177
- * @param {string} specBase - .sillyspec 根
178
- * @param {string} taskContext - 任务描述串(与 matchKnowledge 的 query 同源)
179
- * @param {{ limit?: number }} [opts] - 注入上限(默认 3)
180
- * @returns {{ hits: Array<{ qlId: string, date: string, title: string, files: string[] }>, report: string }}
181
- * 无 quicklog / 无命中 / 任何异常 → { hits: [], report: '' }(fail-open,永不抛)
182
- */
183
- export function matchQuicklogContext(specBase, taskContext, { limit = 3 } = {}) {
184
- try {
185
- if (!specBase || !taskContext) return { hits: [], report: '' }
186
- const entries = parseQuicklogEntries(specBase)
187
- if (entries.length === 0) return { hits: [], report: '' }
188
- const tokens = contextTokens(taskContext)
189
- if (tokens.length === 0) return { hits: [], report: '' }
190
-
191
- const scored = []
192
- for (const e of entries) {
193
- const hay = [e.title, e.solution, e.files.join(' ')].join(' ').toLowerCase()
194
- const basenames = new Set(e.files.map((f) => f.split('/').pop().toLowerCase()))
195
- let score = 0
196
- let tokenHits = 0
197
- let fileHit = 0
198
- for (const t of tokens) {
199
- let hit = false
200
- if (/[^\x00-\x7f]/.test(t)) {
201
- hit = hay.includes(t)
202
- } else {
203
- hit = new RegExp(`(^|[^a-z0-9])${escapeRegex(t)}([^a-z0-9]|$)`).test(hay)
204
- if (!hit && t.length >= 4) {
205
- for (const b of basenames) {
206
- if (b.includes(t)) {
207
- hit = true
208
- fileHit++
209
- score += FILE_HIT_WEIGHT
210
- break
211
- }
212
- }
213
- }
214
- }
215
- if (hit) {
216
- score += 1
217
- tokenHits++
218
- }
219
- }
220
- if (fileHit >= 1 || tokenHits >= MIN_TOKEN_HITS) {
221
- scored.push({ entry: e, score, fileHit })
222
- }
223
- }
224
- scored.sort((a, b) =>
225
- (sourceRank(a.entry) - sourceRank(b.entry)) ||
226
- (b.fileHit - a.fileHit) ||
227
- (b.score - a.score) ||
228
- String(b.entry.date).localeCompare(String(a.entry.date))
229
- )
230
- const top = scored.slice(0, Math.max(1, limit)).map((s) => ({
231
- qlId: s.entry.qlId,
232
- date: String(s.entry.date).slice(0, 10),
233
- title: s.entry.title,
234
- files: s.entry.files,
235
- }))
236
- return { hits: top, report: renderQuicklogSection(top) }
237
- } catch {
238
- return { hits: [], report: '' }
239
- }
240
- }
241
-
242
- /**
243
- * 注入段渲染(prompt.js 与 execute.js 孪生共用——仅段头层级不同)。
244
- * 一行一条:ql-ID · 标题(截 60)· 日期 · 至多 2 个触碰文件。零正文注入——
245
- * quicklog 条目正文可能很长,注入面只做「知道有这回事 + 回源指针」。
246
- * @param {Array<{qlId,date,title,files}>} hits
247
- * @param {{ h3?: boolean }} [opts] - execute.js Wave prompt 内用 h3(### )对齐知识段层级
248
- */
249
- export function renderQuicklogSection(hits, { h3 = false } = {}) {
250
- if (!Array.isArray(hits) || hits.length === 0) return ''
251
- const lines = [
252
- `${h3 ? '### ' : ''}🕘 近期 quick 修补(quicklog 机械匹配,top-${hits.length}——历史记录非权威断言,细节按 ql-ID 回源 .sillyspec/quicklog/ 或读代码)`,
253
- ]
254
- for (const h of hits) {
255
- const files = (h.files || []).slice(0, 2).join(', ')
256
- const title = String(h.title || '').slice(0, 60)
257
- const date = String(h.date || '').slice(0, 10)
258
- lines.push(` - ${h.qlId} · ${title} · ${date}${files ? ` · ${files}` : ''}`)
259
- }
260
- return lines.join('\n')
261
- }