sillyspec 3.33.3 → 3.33.4
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-flow/SKILL.md +4 -0
- package/AGENTS.md +1 -1
- package/package.json +1 -1
- package/src/config-known-failures.js +258 -0
- package/src/decision-distill.js +26 -5
- package/src/decision-redomain.js +5 -4
- package/src/docs-check.js +8 -4
- package/src/doctor-diagnostics.js +89 -6
- package/src/fr-index.js +7 -3
- package/src/git-helper.js +82 -0
- package/src/handoff.js +4 -0
- package/src/hooks/worktree-guard.js +12 -3
- package/src/hot-cache.js +60 -0
- package/src/index.js +99 -7
- package/src/knowledge-graph.js +82 -2
- package/src/knowledge-match.js +18 -2
- package/src/knowledge-quicklog.js +17 -2
- package/src/knowledge-vector.js +5 -3
- package/src/machine-interface.js +34 -13
- package/src/progress/change-registry.js +15 -0
- package/src/progress/shared.js +4 -0
- package/src/progress/stage-machine.js +15 -2
- package/src/progress.js +5 -0
- package/src/redomain-unmapped.js +204 -0
- package/src/redomain.js +5 -3
- package/src/review-dispatch.js +109 -47
- package/src/run/bg-sync.js +4 -0
- package/src/run/code-face-key.js +4 -1
- package/src/run/command.js +20 -5
- package/src/run/complete.js +185 -14
- package/src/run/gates.js +30 -1
- package/src/run/prompt.js +9 -1
- package/src/run/shared.js +18 -1
- package/src/run/stage.js +22 -19
- package/src/run/verify-quality-scan.js +51 -0
- package/src/runtime-paths.js +3 -0
- package/src/sillyhub-mcp/client.js +11 -6
- package/src/spawn-settle.js +47 -0
- package/src/spec-sync.js +89 -3
- package/src/stages/knowledge.js +25 -3
- package/src/timing.js +55 -0
- package/src/verify-postcheck.js +73 -4
- package/src/watcher.js +4 -0
- package/src/worktree-apply.js +67 -4
- package/src/worktree.js +80 -11
- package/src/yaml-scalar-unquote.js +43 -0
- package/templates/agents-instruction.md +1 -1
package/src/git-helper.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { execFileSync } from 'node:child_process'
|
|
16
16
|
import { existsSync, readdirSync } from 'node:fs'
|
|
17
17
|
import { join } from 'node:path'
|
|
18
|
+
import { createHash } from 'node:crypto'
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* git C 风格引号路径解码:`\\` `\"` `\n` `\t` 与 `\NNN` 八进制**原始字节**。
|
|
@@ -146,6 +147,87 @@ export function git(cwd, args, opts = {}) {
|
|
|
146
147
|
return trim ? value.trim() : value
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
/**
|
|
151
|
+
* 批量对象存在性判定(2026-10-21-protocol-tax task-07 / FR-07)。
|
|
152
|
+
*
|
|
153
|
+
* 单次 `git cat-file --batch-check`(SHA 列表走 stdin)替代逐 SHA `cat-file -e`——
|
|
154
|
+
* Windows 单次 git spawn ~100-150ms,归档锚点校验在 681 归档仓实测串行 25s+,批量化后
|
|
155
|
+
* 一次 spawn 判完全部。探针形态 `<sha>^{commit}` 与旧逐查同构(tag/tree 对象不算 commit)。
|
|
156
|
+
*
|
|
157
|
+
* @param {string} cwd
|
|
158
|
+
* @param {string[]} shas 40-hex SHA 列表(大小写不敏感,返回键统一小写)
|
|
159
|
+
* @returns {Map<string, boolean>} sha → 是否为可解析 commit;整批失败返回空 Map
|
|
160
|
+
* (调用方查表 miss = false = 不可解析 = 悬空——与逐查失败同向 fail,不静默洗白)
|
|
161
|
+
*/
|
|
162
|
+
export function batchCatFileExists(cwd, shas) {
|
|
163
|
+
const map = new Map()
|
|
164
|
+
if (!Array.isArray(shas) || shas.length === 0) return map
|
|
165
|
+
const probes = shas.map((s) => `${s}^{commit}`)
|
|
166
|
+
try {
|
|
167
|
+
const out = execFileSync('git', buildFullArgs(cwd, ['cat-file', '--batch-check']), {
|
|
168
|
+
encoding: 'utf8', timeout: 30000, maxBuffer: GIT_MAX_BUFFER,
|
|
169
|
+
input: probes.join('\n') + '\n',
|
|
170
|
+
env: gitChildEnv(), windowsHide: true,
|
|
171
|
+
})
|
|
172
|
+
for (const line of String(out).split('\n')) {
|
|
173
|
+
// 行形态:可解析 → git 回显**解析后裸 SHA**(不回显输入串)`<sha> commit <size>`;
|
|
174
|
+
// 不可解析 → 回显输入串 `<sha>^{commit} missing`。统一剥后缀取裸 SHA 作键。
|
|
175
|
+
const m = line.match(/^([0-9a-f]{40})(?:\^\{commit\})?\s+(\S+)/i)
|
|
176
|
+
if (m) map.set(m[1].toLowerCase(), m[2] === 'commit')
|
|
177
|
+
}
|
|
178
|
+
} catch { /* 整批失败:空表全 miss=悬空(同逐查失败语义) */ }
|
|
179
|
+
return map
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 批量暂存区 blob 内容哈希(2026-10-21-protocol-tax task-07 / FR-07 配套)。
|
|
184
|
+
*
|
|
185
|
+
* 单次 `git cat-file --batch`(stdin 逐行 `:path`)替代逐文件 `git show :path`——apply
|
|
186
|
+
* 交付指纹漂移维度对最近 5 份 manifest × 全部文件逐个起进程,Windows spawn 税可观。
|
|
187
|
+
* 响应按输入顺序回流:成功 `<oid> blob <size>\n<content>\n`、缺失 `<ref> missing\n`;
|
|
188
|
+
* 内容与写侧同基(staged blob 即 LF 规范态),sha256 直算 content 字节。
|
|
189
|
+
*
|
|
190
|
+
* @param {string} cwd
|
|
191
|
+
* @param {string[]} paths 仓库相对路径列表
|
|
192
|
+
* @returns {Map<string, string|null>} path → staged 内容 sha256(null=未暂存/不可解析);
|
|
193
|
+
* 整批失败返回空 Map(调用方查表 miss=null=未暂存,与逐查 gitQuiet null 同语义)
|
|
194
|
+
*/
|
|
195
|
+
export function batchStageBlobSha256(cwd, paths) {
|
|
196
|
+
const map = new Map()
|
|
197
|
+
if (!Array.isArray(paths) || paths.length === 0) return map
|
|
198
|
+
let out
|
|
199
|
+
try {
|
|
200
|
+
out = execFileSync('git', buildFullArgs(cwd, ['cat-file', '--batch']), {
|
|
201
|
+
encoding: 'buffer', timeout: 30000, maxBuffer: GIT_MAX_BUFFER,
|
|
202
|
+
input: paths.map((p) => `:${p}`).join('\n') + '\n',
|
|
203
|
+
env: gitChildEnv(), windowsHide: true,
|
|
204
|
+
})
|
|
205
|
+
} catch { return map }
|
|
206
|
+
const buf = Buffer.isBuffer(out) ? out : Buffer.from(String(out), 'binary')
|
|
207
|
+
let pos = 0
|
|
208
|
+
let idx = 0
|
|
209
|
+
while (pos < buf.length && idx < paths.length) {
|
|
210
|
+
const nl = buf.indexOf(0x0a, pos)
|
|
211
|
+
if (nl === -1) break
|
|
212
|
+
const header = buf.toString('utf8', pos, nl).trim()
|
|
213
|
+
pos = nl + 1
|
|
214
|
+
const parts = header.split(/\s+/)
|
|
215
|
+
if (parts[parts.length - 1] === 'missing') {
|
|
216
|
+
map.set(paths[idx], null)
|
|
217
|
+
idx++
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
const size = Number.parseInt(parts[parts.length - 1], 10)
|
|
221
|
+
if (!Number.isFinite(size) || pos + size > buf.length) break // 形态意外:截断防错读
|
|
222
|
+
map.set(paths[idx], createHash('sha256').update(buf.subarray(pos, pos + size)).digest('hex'))
|
|
223
|
+
pos += size + 1 // 内容 + 尾随换行
|
|
224
|
+
idx++
|
|
225
|
+
}
|
|
226
|
+
// 尾部未覆盖的路径(流截断等)按未暂存处理——与逐查失败同向
|
|
227
|
+
for (; idx < paths.length; idx++) map.set(paths[idx], null)
|
|
228
|
+
return map
|
|
229
|
+
}
|
|
230
|
+
|
|
149
231
|
/**
|
|
150
232
|
* 静默版 git:内部调 git,失败 catch 返回 null(对齐 worktree 本地 gitQuiet() 语义)。
|
|
151
233
|
* ENOBUFS(输出超 GIT_MAX_BUFFER 上限)不是"无输出",静默成 null 会让调用方把大输出误当空清单——
|
package/src/handoff.js
CHANGED
|
@@ -164,6 +164,10 @@ export async function buildHandoff({ cwd, specBase, changeName } = {}) {
|
|
|
164
164
|
} else {
|
|
165
165
|
lines.push('# (流程终态,无续跑命令)');
|
|
166
166
|
}
|
|
167
|
+
// 所有权接缝预告(2026-10-21-p0-p1-hardening FR-05 / dsh z2 实证:新会话自拟新 SESSION_ID
|
|
168
|
+
// 而非沿用交接块导出的旧 ID 时,首个写操作必撞 15 分钟活跃窗被迫 --takeover):粘贴块内显式
|
|
169
|
+
// 给出出路,消「交接块没预告接管」的设计缺口。
|
|
170
|
+
lines.push('# ⚠️ 若换用新的 SILLYSPEC_SESSION_ID(不推荐,保持上方导出值即可):首个写操作(--done/--answer 等)会撞所有权活跃窗——等窗过期自动接管,或首写带 --takeover 显式接管留痕');
|
|
167
171
|
lines.push('# ── 交接块结束 ──');
|
|
168
172
|
lines.push('');
|
|
169
173
|
lines.push(`📋 ${target}:当前 ${STAGE_LABELS[currentStage] || currentStage}${steps.length > 0 ? `(步骤 ${doneSteps}/${steps.length})` : ''}${stageCompleted ? ' ✅ 已完成' : ''}`);
|
|
@@ -67,11 +67,14 @@ const STAGE_HINTS = {
|
|
|
67
67
|
' - <可验证标准>"',
|
|
68
68
|
' 逻辑变更(skill sillyspec-brainstorm):sillyspec run brainstorm → plan → execute → verify → archive',
|
|
69
69
|
' 全自动模式(skill sillyspec-auto):sillyspec run auto "任务描述"',
|
|
70
|
+
'',
|
|
71
|
+
'若在头脑风暴阶段做真原型(变更 key 已出):sillyspec worktree create <变更名> 进隔离树写原型,',
|
|
72
|
+
'已在 worktree 内则复用当前目录不重复创建;文档照常主树(worktree 内运行 sillyspec 自动锚回主仓根)。',
|
|
70
73
|
],
|
|
71
74
|
'brainstorm': [
|
|
72
|
-
'当前在 brainstorm
|
|
73
|
-
'
|
|
74
|
-
'execute
|
|
75
|
+
'当前在 brainstorm(需求分析)阶段:spec 文档写主树;做真原型须先进隔离 worktree——变更 key 已出,运行 sillyspec worktree create <变更名>(已在 worktree 内则复用当前目录,不重复创建),原型代码写 worktree 内。',
|
|
76
|
+
'文档不受影响:worktree 内运行 sillyspec 会自动锚回主仓根,文档照常落主树。',
|
|
77
|
+
'完成 brainstorm 后,流程会自动推进到 plan → execute;execute 会复用已建的 worktree。',
|
|
75
78
|
],
|
|
76
79
|
'plan': [
|
|
77
80
|
'当前在 plan(计划制定)阶段,这个阶段只写计划文档和任务蓝图,不写代码。',
|
|
@@ -805,6 +808,12 @@ export function shouldBlockWrite(filePath, cwd) {
|
|
|
805
808
|
// 2. 文件门禁:文档类/配置类始终放行,但 worktree 存储区内的源码必须继续走登记校验。
|
|
806
809
|
if (!isInsideWorktreeStorage(absPath, projectRoot) && matchFileWhitelist(absPath)) return { blocked: false }
|
|
807
810
|
|
|
811
|
+
// 2.5 位置豁免(2026-10-21-brainstorm-proto-worktree):已注册 worktree(meta.json 登记)
|
|
812
|
+
// 是隔离区——路径落在其内即放行,不问阶段,与 Bash 侧 worktree-cwd 全放行对称。
|
|
813
|
+
// 头脑风暴期做真原型的协议出口:变更 key 已出 → sillyspec worktree create → 进树写原型。
|
|
814
|
+
// 未登记的 worktrees 存储区子目录不沾此豁免(下方阶段门照拦)。
|
|
815
|
+
if (isInsideRegisteredWorktree(absPath, projectRoot)) return { blocked: false }
|
|
816
|
+
|
|
808
817
|
if (!['execute', 'quick'].includes(stage)) {
|
|
809
818
|
return {
|
|
810
819
|
blocked: true,
|
package/src/hot-cache.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hot-cache.js — 跨进程热路径缓存小工具(2026-10-21-knowledge-tier-denoise FR-03)
|
|
3
|
+
*
|
|
4
|
+
* 服务对象:纯读派生面(quicklog flow 归档伪条目扫描、knowledge-graph 构建)——输入是
|
|
5
|
+
* 冻结/低频变更面(归档目录、知识库、模块图),派生结果跨进程可复用。落点
|
|
6
|
+
* .sillyspec/.runtime/cache/(gitignored 且被平台 spec 上传排除——与 vector-capability
|
|
7
|
+
* 同款纪律;writeAtomicSync 自建父目录)。
|
|
8
|
+
*
|
|
9
|
+
* 契约:
|
|
10
|
+
* - 读:SILLYSPEC_HOT_CACHE=off 逃生阀直 null;version/fingerprint 任一不匹配 → null;
|
|
11
|
+
* ttlMs>0 且 savedAt 超窗 → null;坏 JSON/缺文件 → null(fail-open,缓存非真相源)
|
|
12
|
+
* - 写:原子;写失败 fail-soft 返回 false(加速面失败不阻断本体)
|
|
13
|
+
* - fingerprint 须可 JSON 稳定序列化且键序/元素序确定(数组按序构造,dirStamp 已排序)
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, readdirSync, statSync } from 'fs'
|
|
16
|
+
import { join } from 'path'
|
|
17
|
+
import { writeAtomicSync } from './fs-atomic.js'
|
|
18
|
+
|
|
19
|
+
/** 读缓存:命中返回 payload,任何不匹配/损坏/关闭 → null。 */
|
|
20
|
+
export function readHotCache(cachePath, { version, fingerprint, ttlMs = 0, now = Date.now() }) {
|
|
21
|
+
if (process.env.SILLYSPEC_HOT_CACHE === 'off') return null
|
|
22
|
+
try {
|
|
23
|
+
const obj = JSON.parse(readFileSync(cachePath, 'utf-8'))
|
|
24
|
+
if (!obj || obj.version !== version) return null
|
|
25
|
+
if (JSON.stringify(obj.fingerprint) !== JSON.stringify(fingerprint)) return null
|
|
26
|
+
if (ttlMs > 0 && !(typeof obj.savedAt === 'number' && now - obj.savedAt <= ttlMs)) return null
|
|
27
|
+
return obj.payload
|
|
28
|
+
} catch { return null }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 写缓存:原子落盘 {version, fingerprint, savedAt, payload};失败 fail-soft。 */
|
|
32
|
+
export function writeHotCache(cachePath, { version, fingerprint, payload, now = Date.now() }) {
|
|
33
|
+
try {
|
|
34
|
+
writeAtomicSync(cachePath, JSON.stringify({ version, fingerprint, savedAt: now, payload }))
|
|
35
|
+
return true
|
|
36
|
+
} catch { return false }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 目录级指纹:子项名全集 + 目录 mtimeMs——子项增删/改名必变;不感知深层内容变化
|
|
40
|
+
* (配冻结输入面用:归档目录、或配 TTL 硬上限)。目录缺席 → null。 */
|
|
41
|
+
export function dirStamp(dir) {
|
|
42
|
+
try {
|
|
43
|
+
return { names: readdirSync(dir).sort(), mtimeMs: statSync(dir).mtimeMs }
|
|
44
|
+
} catch { return null }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 文件级指纹:目录内全部文件(含 _module-map.yaml 等非 md)名 → mtimeMs:size,按名排序。
|
|
48
|
+
* 感知本目录文件的新增/删除/就地重写(模块卡归档重写、知识文件蒸馏都在此列);
|
|
49
|
+
* 不递归子目录(子目录增删由上层逐目录 fileDirStamp 覆盖)。目录缺席 → null。 */
|
|
50
|
+
export function fileDirStamp(dir) {
|
|
51
|
+
try {
|
|
52
|
+
const out = []
|
|
53
|
+
for (const f of readdirSync(dir).sort()) {
|
|
54
|
+
const st = statSync(join(dir, f))
|
|
55
|
+
if (st.isDirectory()) continue
|
|
56
|
+
out.push([f, `${st.mtimeMs}:${st.size}`])
|
|
57
|
+
}
|
|
58
|
+
return out
|
|
59
|
+
} catch { return null }
|
|
60
|
+
}
|
package/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 只负责两件事:init(安装命令模板)和 setup(安装 MCP 工具)。
|
|
7
7
|
* 状态管理通过 sillyspec.db(SQLite)完成,使用 `sillyspec progress` 命令。
|
|
8
8
|
*/
|
|
9
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, unlinkSync } from 'fs';
|
|
9
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, writeSync, unlinkSync } from 'fs';
|
|
10
10
|
import { runtimeStatePath } from './runtime-paths.js'
|
|
11
11
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
12
12
|
import { basename, dirname, extname, join, resolve, isAbsolute, sep, relative } from 'path';
|
|
@@ -237,6 +237,47 @@ function refuseRetiredQuickFreshStart(stageArgs) {
|
|
|
237
237
|
process.exit(1)
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* 全局输出收集器(2026-10-21-protocol-tax FR-03)。
|
|
242
|
+
*
|
|
243
|
+
* 收编会话考古(z1)发现的 agent 自发模式:44/48 次把 CLI 输出重定向进日志再 tail 尾部——
|
|
244
|
+
* 工具精心生成的规则大半没进上下文,且 shell 包装会掩蔽真实退出码。内建后:全文落
|
|
245
|
+
* --out-file 文件、stdout 只回提示行+尾 N 行(--tail,默认 40),退出码零拦截(失败信息
|
|
246
|
+
* 进尾行或全文件,不丢)。
|
|
247
|
+
*
|
|
248
|
+
* 实现要点:
|
|
249
|
+
* - 拦截点选 process.stdout.write/process.stderr.write(console.log/error 底层同路,
|
|
250
|
+
* JSON envelope 直写 process.stdout 的命令也被覆盖;stdio:'inherit' 的子进程直写 fd
|
|
251
|
+
* 不经此层——已知豁免面,退出码不受影响)。
|
|
252
|
+
* - exit 钩子同步落盘+尾行输出用 fs.writeSync 直写 fd(exit 期 pipe 异步写会丢)。
|
|
253
|
+
* - 观测面优先、绝不改变命令成败:写文件失败退化为全量打印;收集器任何异常不影响退出码。
|
|
254
|
+
*/
|
|
255
|
+
function installOutFileRedirect(outFile, tailN) {
|
|
256
|
+
const entries = [];
|
|
257
|
+
const push = (chunk) => { entries.push(typeof chunk === 'string' ? chunk : chunk.toString()); return true; };
|
|
258
|
+
process.stdout.write = push;
|
|
259
|
+
process.stderr.write = push;
|
|
260
|
+
process.on('exit', () => {
|
|
261
|
+
const full = entries.join('');
|
|
262
|
+
let wrote = true;
|
|
263
|
+
try { writeFileSync(outFile, full); } catch { wrote = false; }
|
|
264
|
+
if (!wrote) {
|
|
265
|
+
try {
|
|
266
|
+
writeSync(2, `⚠️ --out-file 写入失败(${outFile})——改为全量打印:\n`);
|
|
267
|
+
writeSync(1, full);
|
|
268
|
+
} catch { /* exit 期 best-effort */ }
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const lines = full.split('\n');
|
|
272
|
+
if (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
273
|
+
const tail = lines.slice(-tailN);
|
|
274
|
+
try {
|
|
275
|
+
writeSync(1, `📄 全文已写 ${outFile}(${lines.length} 行,尾 ${tail.length} 行):\n`);
|
|
276
|
+
if (tail.length) writeSync(1, tail.join('\n') + '\n');
|
|
277
|
+
} catch { /* exit 期 best-effort */ }
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
240
281
|
async function main() {
|
|
241
282
|
const args = process.argv.slice(2);
|
|
242
283
|
|
|
@@ -269,6 +310,9 @@ async function main() {
|
|
|
269
310
|
// 平台模式 flag(init 落平台指针用;非 init 命令时忽略——runCommand 自行解析 filteredArgs)
|
|
270
311
|
let platformWorkspaceId = null;
|
|
271
312
|
let platformRuntimeRoot = null;
|
|
313
|
+
// 全局输出收集器(2026-10-21-protocol-tax FR-03):--out-file <路径> / --tail N
|
|
314
|
+
let outFileRedirect = null;
|
|
315
|
+
let outFileTailLines = null;
|
|
272
316
|
const filteredArgs = [];
|
|
273
317
|
|
|
274
318
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -305,6 +349,13 @@ async function main() {
|
|
|
305
349
|
platformRuntimeRoot = resolve(args[i + 1]);
|
|
306
350
|
filteredArgs.push(args[i], args[i + 1]);
|
|
307
351
|
i++;
|
|
352
|
+
} else if (args[i] === '--out-file' && args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
353
|
+
// 全局输出收集器(2026-10-21-protocol-tax FR-03):全文落文件、stdout 只回尾 N 行
|
|
354
|
+
outFileRedirect = resolve(args[i + 1]);
|
|
355
|
+
i++;
|
|
356
|
+
} else if (args[i] === '--tail' && args[i + 1] && /^\d+$/.test(args[i + 1]) && parseInt(args[i + 1], 10) > 0) {
|
|
357
|
+
outFileTailLines = parseInt(args[i + 1], 10);
|
|
358
|
+
i++;
|
|
308
359
|
} else if (args[i] === '--interactive' || args[i] === '-i') {
|
|
309
360
|
interactive = true;
|
|
310
361
|
} else if (args[i] === '--allow-worktree-cwd') {
|
|
@@ -324,6 +375,13 @@ async function main() {
|
|
|
324
375
|
process.exit(2);
|
|
325
376
|
}
|
|
326
377
|
|
|
378
|
+
// --out-file/--tail 校验与安装(2026-10-21-protocol-tax FR-03):必须在任何命令输出之前装好
|
|
379
|
+
if (outFileTailLines !== null && outFileRedirect === null) {
|
|
380
|
+
console.error('❌ --tail 需与 --out-file 同用(控制 --out-file 模式下 stdout 尾部行数,默认 40)。');
|
|
381
|
+
process.exit(2);
|
|
382
|
+
}
|
|
383
|
+
if (outFileRedirect) installOutFileRedirect(outFileRedirect, outFileTailLines ?? 40);
|
|
384
|
+
|
|
327
385
|
// ── 旧布局迁移(2026-10-18 runtime-relayout):per-change 散文件入 state/、可删产物入 temp/。
|
|
328
386
|
// 每次入口跑一遍(幂等:已收敛后 readdir 一轮零写入);fail-open 绝不阻断命令。
|
|
329
387
|
// init 除外——目标仓尚无 .sillyspec 可迁。resolveEffectiveDir 是函数声明(提升后可调)。
|
|
@@ -1683,12 +1741,18 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
1683
1741
|
const rdStage = rdStageIdx >= 0 && args[rdStageIdx + 1] && !String(args[rdStageIdx + 1]).startsWith("--") ? args[rdStageIdx + 1] : null;
|
|
1684
1742
|
const rdStatus = args.includes('--status');
|
|
1685
1743
|
const rdKill = args.includes('--kill');
|
|
1744
|
+
const rdSalvage = args.includes('--salvage');
|
|
1686
1745
|
const rdMode = rdKill ? 'kill' : rdStatus ? 'status' : 'create';
|
|
1687
|
-
const rdUsage = '用法: sillyspec review-dispatch --change <名> --stage <brainstorm|plan|execute> [--status] [--kill]\n' +
|
|
1746
|
+
const rdUsage = '用法: sillyspec review-dispatch --change <名> --stage <brainstorm|plan|execute> [--status] [--kill] [--status --salvage]\n' +
|
|
1688
1747
|
' 缺省=派发(probe 前置→create_mission(external)→dispatch_worker(read_only),异步返回 missionId)\n' +
|
|
1689
1748
|
' --status=轮询状态+停滞提示;worker completed 自动回收落盘 review.json(reviewer.channel=platform)\n' +
|
|
1749
|
+
' --status --salvage=终态原文兜底:worker-answer.md 落盘 + register-stage-review --from 回收指引(提取失败时用)\n' +
|
|
1690
1750
|
' --kill=清本地在途记录+平台处置指引(不自动 kill,处置权在人)';
|
|
1691
1751
|
if (!rdChange) { console.error(rdUsage); process.exit(2); }
|
|
1752
|
+
if (rdSalvage && !rdStatus) {
|
|
1753
|
+
console.error(`❌ --salvage 仅与 --status 组合使用(completed 提取失败后的终态原文兜底回收)\n${rdUsage}`);
|
|
1754
|
+
process.exit(2);
|
|
1755
|
+
}
|
|
1692
1756
|
assertSafeChangeName(rdChange, '--change 变更名');
|
|
1693
1757
|
if (rdMode === 'create' && !['brainstorm', 'plan', 'execute'].includes(rdStage)) {
|
|
1694
1758
|
console.error(`❌ create 形态必须 --stage ∈ brainstorm|plan|execute(实际:${rdStage || '缺'})\n${rdUsage}`);
|
|
@@ -1706,7 +1770,7 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
1706
1770
|
const rdSpecBase = resolvePlatformSpecDir(dir, specDir);
|
|
1707
1771
|
const r = await runReviewDispatch({
|
|
1708
1772
|
mode: rdMode, cwd: dir, specBase: rdSpecBase, changeName: rdChange, stage: rdStage,
|
|
1709
|
-
client: rdClient, budgetUsd: rdCfg.budget_usd, stallMs: rdCfg.stall_ms,
|
|
1773
|
+
client: rdClient, budgetUsd: rdCfg.budget_usd, stallMs: rdCfg.stall_ms, salvage: rdSalvage,
|
|
1710
1774
|
});
|
|
1711
1775
|
if (r.ok) {
|
|
1712
1776
|
if (r.statusLine) console.log(`🛰️ ${r.statusLine}`);
|
|
@@ -1721,6 +1785,7 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
1721
1785
|
} else {
|
|
1722
1786
|
console.error(`❌ review-dispatch(${rdMode}): ${r.reason}`);
|
|
1723
1787
|
if (r.statusLine) console.error(` ${r.statusLine}`);
|
|
1788
|
+
if (r.salvagePath) console.error(` 📄 worker 终态原文:${r.salvagePath}`);
|
|
1724
1789
|
if (r.errors) for (const e of r.errors) console.error(` - ${e}`);
|
|
1725
1790
|
if (r.guidance) console.error(` 下一步:\n${r.guidance}`);
|
|
1726
1791
|
process.exitCode = 1;
|
|
@@ -3264,8 +3329,13 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
3264
3329
|
const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
|
|
3265
3330
|
// 下行 pull:stage 命令(run/--done,含 archive)启动时拉一次(task-10 / D-009 / FR-04)
|
|
3266
3331
|
// 不在每步 pull,仅低频边界点;未连接平台静默跳过
|
|
3267
|
-
|
|
3268
|
-
|
|
3332
|
+
// 例外(2026-10-21-protocol-tax task-06 / FR-06):status 只读查看不 pull——消费仓实测
|
|
3333
|
+
// pull 占 status 6.3s 的大头(profile 71% idle 等网络);本地 DB 是查看态事实源
|
|
3334
|
+
//(progress show 同款语义——它从不 pull),新鲜度交给写命令与 daemon/bg-sync。
|
|
3335
|
+
if (command !== 'status') {
|
|
3336
|
+
const { triggerPullActiveChange } = await import('./run/shared.js')
|
|
3337
|
+
await triggerPullActiveChange(effectiveDir)
|
|
3338
|
+
}
|
|
3269
3339
|
await runCommand(stageArgs, effectiveDir, specDir, { json })
|
|
3270
3340
|
break
|
|
3271
3341
|
}
|
|
@@ -5322,6 +5392,9 @@ SillySpec config — local.yaml 配置键速查
|
|
|
5322
5392
|
git common-dir 主仓根;worktree 内 .sillyspec 是 checkout
|
|
5323
5393
|
副本无此文件,cat 自动解析到主仓)
|
|
5324
5394
|
sillyspec config cat --json 机读 JSON(dir/path/source/content;未找到退出 1)
|
|
5395
|
+
sillyspec config known-failures list 列 known_failures 豁免键
|
|
5396
|
+
sillyspec config known-failures add <key…> 增豁免键(行级手术保注释、原子写;坏 YAML 拒改)
|
|
5397
|
+
sillyspec config known-failures remove <key…> 删豁免键(未命中静默忽略)
|
|
5325
5398
|
|
|
5326
5399
|
说明:
|
|
5327
5400
|
- 数据源:src/config-schema.js(唯一真相;reader 见各键「读取点」)
|
|
@@ -5363,11 +5436,30 @@ SillySpec config — local.yaml 配置键速查
|
|
|
5363
5436
|
if (!content.endsWith('\n')) process.stdout.write('\n')
|
|
5364
5437
|
break;
|
|
5365
5438
|
}
|
|
5439
|
+
// config known-failures(2026-10-21-p0-p1-hardening FR-07 / dsh z2 实证:known_failures
|
|
5440
|
+
// 无 CLI 管理面 → agent 手改 local.yaml,PS5.1 编码把 50KB 文件写成 mojibake 等用户
|
|
5441
|
+
// 2h13m)。行级增删查:保注释、原子写、坏 YAML 拒改——消灭手改场景。
|
|
5442
|
+
if (configSub === 'known-failures') {
|
|
5443
|
+
let specBase = null
|
|
5444
|
+
try { specBase = resolvePlatformSpecDir(dir, specDir) } catch { /* 同 cat:pointer 异常走祖先链 */ }
|
|
5445
|
+
const { resolveLocalYaml } = await import('./config-cat.js')
|
|
5446
|
+
const located = resolveLocalYaml(dir, { specBase })
|
|
5447
|
+
const { runKnownFailuresCommand } = await import('./config-known-failures.js')
|
|
5448
|
+
const verb = filteredArgs[2];
|
|
5449
|
+
const keys = filteredArgs.slice(3).filter((a) => a && !a.startsWith('--'));
|
|
5450
|
+
const exitCode = runKnownFailuresCommand({
|
|
5451
|
+
yamlPath: located.exists ? located.path : join(dir, '.sillyspec', 'local.yaml'),
|
|
5452
|
+
exists: located.exists,
|
|
5453
|
+
verb, keys, json,
|
|
5454
|
+
});
|
|
5455
|
+
process.exitCode = exitCode;
|
|
5456
|
+
break;
|
|
5457
|
+
}
|
|
5366
5458
|
if (!isSchema) {
|
|
5367
|
-
const sug = didYouMean(configSub, ['schema', 'cat']);
|
|
5459
|
+
const sug = didYouMean(configSub, ['schema', 'cat', 'known-failures']);
|
|
5368
5460
|
console.error(`❌ 未知子命令: config ${configSub}`);
|
|
5369
5461
|
if (sug) console.error(` 你是想输入「config ${sug}」吗?`);
|
|
5370
|
-
console.error('用法: sillyspec config [schema|cat] [--json]
|
|
5462
|
+
console.error('用法: sillyspec config [schema|cat|known-failures] [--json] 打印配置键 / 读内容 / 管理 known_failures 键');
|
|
5371
5463
|
process.exit(2);
|
|
5372
5464
|
}
|
|
5373
5465
|
const { renderSchemaHuman, renderSchemaJson } = await import('./config-schema.js');
|
package/src/knowledge-graph.js
CHANGED
|
@@ -18,10 +18,11 @@
|
|
|
18
18
|
* 模块域内解析(沿 fr-index renderFrLines / module-changelog 行形态的机械契约)。
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
21
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
|
|
22
22
|
import { join } from 'path'
|
|
23
23
|
import { parseKnowledgeIndex, parseDecisionEntries, anchorFilePaths } from './knowledge-match.js'
|
|
24
24
|
import { modulePathMatches, parseModuleMapSimple } from './modules.js'
|
|
25
|
+
import { readHotCache, writeHotCache, dirStamp, fileDirStamp } from './hot-cache.js'
|
|
25
26
|
|
|
26
27
|
// ── 本体常量(design.md 总体方案 §1 的代码化) ──
|
|
27
28
|
|
|
@@ -60,7 +61,7 @@ function extractFilePaths(text, cap = 15) {
|
|
|
60
61
|
|
|
61
62
|
// ── 构建器 ──
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
function buildKnowledgeGraphFresh(specRoot) {
|
|
64
65
|
const nodes = new Map()
|
|
65
66
|
const edges = []
|
|
66
67
|
const byEdgeType = new Map()
|
|
@@ -340,6 +341,85 @@ export function buildKnowledgeGraph(specRoot) {
|
|
|
340
341
|
return { root: specRoot, nodes, edges, byEdgeType, stats }
|
|
341
342
|
}
|
|
342
343
|
|
|
344
|
+
// ── 跨进程缓存(FR-03,2026-10-21-knowledge-tier-denoise)──
|
|
345
|
+
// 图召回是四层检索的第三层 advisory 保底(fail-soft);构建成本随归档线性(逐变更读
|
|
346
|
+
// design.md+change-patch.json),flow start/知识门/doctor 热路径上无缓存重付。D-003 墓碑
|
|
347
|
+
// 切割原则不变:md/yaml 仍是唯一真相源——缓存只存派生图、键在输入指纹上,不构成第二
|
|
348
|
+
// 真相源;任何失配全量重建。
|
|
349
|
+
// 陈旧边界(有意接受):指纹感知本目录文件级变化与目录增删,但 docs 树深层就地重写若
|
|
350
|
+
// 未触及被 stat 的文件面则漏——TTL 10min 硬上限兜底,advisory 面有界陈旧可接受。
|
|
351
|
+
|
|
352
|
+
const GRAPH_CACHE_VERSION = 1
|
|
353
|
+
const GRAPH_CACHE_TTL_MS = 10 * 60 * 1000
|
|
354
|
+
|
|
355
|
+
function statSignature(p) {
|
|
356
|
+
try { const s = statSync(p); return `${s.mtimeMs}:${s.size}` } catch { return null }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** 输入面指纹:knowledge(INDEX+decisions+fr)+ docs 各项目(modules+scan 文件级)+
|
|
360
|
+
* 归档根(dirStamp)+ quicklog test-bindings。键序固定、数组排序——JSON 稳定比较。
|
|
361
|
+
* 各面缺席记 null(合法形态:无 docs/ 的小仓、无 fr 库)——缺席本身是指纹的一部分。 */
|
|
362
|
+
function knowledgeGraphFingerprint(specRoot) {
|
|
363
|
+
const knowledgeDir = join(specRoot, 'knowledge')
|
|
364
|
+
const docsDir = join(specRoot, 'docs')
|
|
365
|
+
let projects = []
|
|
366
|
+
try {
|
|
367
|
+
projects = readdirSync(docsDir, { withFileTypes: true })
|
|
368
|
+
.filter((d) => d.isDirectory())
|
|
369
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
370
|
+
.map((d) => [
|
|
371
|
+
d.name,
|
|
372
|
+
fileDirStamp(join(docsDir, d.name, 'modules')),
|
|
373
|
+
fileDirStamp(join(docsDir, d.name, 'scan')),
|
|
374
|
+
])
|
|
375
|
+
} catch { /* docs/ 缺席 = 无项目面(合法形态)*/ }
|
|
376
|
+
return {
|
|
377
|
+
index: statSignature(join(knowledgeDir, 'INDEX.md')),
|
|
378
|
+
decisions: fileDirStamp(join(knowledgeDir, 'decisions')),
|
|
379
|
+
fr: fileDirStamp(join(knowledgeDir, 'fr')),
|
|
380
|
+
projects,
|
|
381
|
+
archive: dirStamp(join(specRoot, 'changes', 'archive')),
|
|
382
|
+
testBindings: statSignature(join(specRoot, 'quicklog', 'test-bindings.json')),
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** 缓存载荷 → 图对象(nodes Map 按 payload 序重建保迭代序一致;byEdgeType/stats 复算)。 */
|
|
387
|
+
function reviveGraph(payload, specRoot) {
|
|
388
|
+
const nodes = new Map(payload.nodes.map((n) => [String(n.id), n]))
|
|
389
|
+
const edges = payload.edges.map((e) => ({ s: String(e.s), t: String(e.t), type: e.type, note: String(e.note || '') }))
|
|
390
|
+
const byEdgeType = new Map()
|
|
391
|
+
for (const e of edges) {
|
|
392
|
+
if (!byEdgeType.has(e.type)) byEdgeType.set(e.type, [])
|
|
393
|
+
byEdgeType.get(e.type).push(e)
|
|
394
|
+
}
|
|
395
|
+
const stats = {
|
|
396
|
+
nodeCount: nodes.size,
|
|
397
|
+
edgeCount: edges.length,
|
|
398
|
+
byNodeType: [...nodes.values()].reduce((a, n) => (a[n.type] = (a[n.type] || 0) + 1, a), {}),
|
|
399
|
+
byEdgeType: edges.reduce((a, e) => (a[e.type] = (a[e.type] || 0) + 1, a), {}),
|
|
400
|
+
}
|
|
401
|
+
return { root: payload.root || specRoot, nodes, edges, byEdgeType, stats }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** 构建入口(缓存包装,签名与返回结构不变):指纹+TTL+版本键三闸,命中零全量重扫。 */
|
|
405
|
+
export function buildKnowledgeGraph(specRoot) {
|
|
406
|
+
const fingerprint = knowledgeGraphFingerprint(specRoot)
|
|
407
|
+
const cachePath = join(specRoot, '.runtime', 'cache', 'knowledge-graph.json')
|
|
408
|
+
if (fingerprint) {
|
|
409
|
+
const cached = readHotCache(cachePath, { version: GRAPH_CACHE_VERSION, fingerprint, ttlMs: GRAPH_CACHE_TTL_MS })
|
|
410
|
+
if (cached && Array.isArray(cached.nodes) && Array.isArray(cached.edges)) return reviveGraph(cached, specRoot)
|
|
411
|
+
}
|
|
412
|
+
const graph = buildKnowledgeGraphFresh(specRoot)
|
|
413
|
+
if (fingerprint) {
|
|
414
|
+
writeHotCache(cachePath, {
|
|
415
|
+
version: GRAPH_CACHE_VERSION,
|
|
416
|
+
fingerprint,
|
|
417
|
+
payload: { root: graph.root, nodes: [...graph.nodes.values()], edges: graph.edges },
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
return graph
|
|
421
|
+
}
|
|
422
|
+
|
|
343
423
|
/** changelog 三态行解析(表格行 ∪ 列表行 ∪ 标题态——backend 侧主形态;坏行跳过 fail-soft)。 */
|
|
344
424
|
export function parseChangelogEntries(content) {
|
|
345
425
|
const out = []
|
package/src/knowledge-match.js
CHANGED
|
@@ -292,6 +292,16 @@ function asciiWordHit(sh, text) {
|
|
|
292
292
|
return new RegExp(`(^|[^a-z0-9-])${sh.replace(/[.*+?^${}()|[\]\\-]/g, '\\$&')}([^a-z0-9-]|$)`, 'i').test(text)
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
/** 停车场文件(未归类 unmapped 域——decisions 与 fr 两面):检索核心零返回,文件本身
|
|
296
|
+
* 保留在盘上供人工直查(search 面以 parkingFiltered 披露指路)。2026-10-21-knowledge-tier-denoise
|
|
297
|
+
* FR-01,对齐 FR 注入面 2026-09-29-rot-retire-inject-cap 的 unmapped 过滤口径(停车场非行为域)。 */
|
|
298
|
+
const PARKING_FILES = new Set(['decisions/unmapped.md', 'fr/unmapped.md'])
|
|
299
|
+
|
|
300
|
+
/** file 是否停车场文件(decisions/fr unmapped;Windows 反斜杠归一后判等)。 */
|
|
301
|
+
export function isParkingFile(file) {
|
|
302
|
+
return PARKING_FILES.has(String(file || '').replace(/\\/g, '/'))
|
|
303
|
+
}
|
|
304
|
+
|
|
295
305
|
/**
|
|
296
306
|
* 零命中查询侧词片回退(2026-09-29-decision-route-vocab):路由行零命中时,只测「查询自己的
|
|
297
307
|
* 词片」——逐 decisions 域文件数词片在条目文本(标题∪理由)的出现次数,落在「跨条目复现但
|
|
@@ -309,6 +319,7 @@ export function fallbackByQueryShingles(indexDir, taskContext) {
|
|
|
309
319
|
if (all.length === 0) return null
|
|
310
320
|
const byFile = new Map()
|
|
311
321
|
for (const h of all) {
|
|
322
|
+
if (isParkingFile(h.file)) continue // 停车场文件不参与词片回退(FR-01)
|
|
312
323
|
let st = byFile.get(h.file)
|
|
313
324
|
if (!st) { st = { entries: [], texts: [] }; byFile.set(h.file, st) }
|
|
314
325
|
st.entries.push(h)
|
|
@@ -395,16 +406,21 @@ export function matchByRouting(indexDir, taskContext) {
|
|
|
395
406
|
}
|
|
396
407
|
|
|
397
408
|
const contextLower = taskContext.toLowerCase()
|
|
398
|
-
const
|
|
409
|
+
const matchedRaw = allEntries.filter(entry => {
|
|
399
410
|
return entry.keywords.some(kw => keywordMatchesContext(kw, contextLower))
|
|
400
411
|
})
|
|
412
|
+
// 停车场过滤(FR-01,2026-10-21-knowledge-tier-denoise):unmapped 路由行不进命中集;
|
|
413
|
+
// 只命中停车场行 → 按零命中处理(hybrid 链继续走向量/图/词片层),json.parkingFiltered
|
|
414
|
+
// 披露被滤文件供 search 面提示人工直查(可见性不因去噪丢失)。
|
|
415
|
+
const parkingFiltered = [...new Set(matchedRaw.filter((e) => isParkingFile(e.file)).map((e) => e.file))]
|
|
416
|
+
const matched = matchedRaw.filter((e) => !isParkingFile(e.file))
|
|
401
417
|
|
|
402
418
|
if (matched.length === 0) {
|
|
403
419
|
return {
|
|
404
420
|
matched: false,
|
|
405
421
|
entries: [],
|
|
406
422
|
report: 'Status: no matches',
|
|
407
|
-
json: { matched: false, entry_count: 0, entries: [] },
|
|
423
|
+
json: { matched: false, entry_count: 0, entries: [], ...(parkingFiltered.length > 0 ? { parkingFiltered } : {}) },
|
|
408
424
|
decisionHits: []
|
|
409
425
|
}
|
|
410
426
|
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
|
|
21
21
|
import { join } from 'path'
|
|
22
|
+
import { readHotCache, writeHotCache, dirStamp } from './hot-cache.js'
|
|
22
23
|
|
|
23
24
|
const QUICKLOG_ENTRY_RE = /^##\s+(ql-[\w-]+)\s*\|\s*([^|]+)\s*\|\s*(.+)$/
|
|
24
25
|
// 文件注记行:`- path(中文括注)` / `- path(ascii)` / 裸 `- path`(allocateQuicklogEntry
|
|
@@ -95,10 +96,23 @@ function parseQuicklogFileEntries(specBase) {
|
|
|
95
96
|
return entries
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
/** 伪条目扫描缓存版本(解析契约变化时 bump 即全量失效)。 */
|
|
100
|
+
const FLOW_ARCHIVE_CACHE_VERSION = 1
|
|
101
|
+
|
|
98
102
|
/** 源②:flow 归档变更伪条目——目录内 flow-state.yaml 在场即认(不校验 yaml 内容:伪条目
|
|
99
|
-
* 只需「这曾是一个 flow 变更」的存在信号,字段全来自目录名与 proposal.md,缺件逐项降级)。
|
|
100
|
-
|
|
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) {
|
|
101
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
|
+
|
|
102
116
|
let dirs
|
|
103
117
|
try {
|
|
104
118
|
dirs = readdirSync(archiveDir, { withFileTypes: true })
|
|
@@ -125,6 +139,7 @@ function parseFlowArchiveEntries(specBase) {
|
|
|
125
139
|
out.push({ qlId: d.name, date, title: title || d.name, solution: '', files: [], source: 'flow' })
|
|
126
140
|
} catch { /* 单目录读取异常 → 跳过该目录(fail-open,不影响其余源) */ }
|
|
127
141
|
}
|
|
142
|
+
writeHotCache(cachePath, { version: FLOW_ARCHIVE_CACHE_VERSION, fingerprint, payload: out })
|
|
128
143
|
return out
|
|
129
144
|
}
|
|
130
145
|
|
package/src/knowledge-vector.js
CHANGED
|
@@ -38,7 +38,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
|
38
38
|
import { runtimeStatePath } from './runtime-paths.js'
|
|
39
39
|
import { join, dirname } from 'path'
|
|
40
40
|
import { readPlatformConfig } from './sync.js'
|
|
41
|
-
import { matchByRouting, fallbackByQueryShingles, parseDecisionEntries } from './knowledge-match.js'
|
|
41
|
+
import { matchByRouting, fallbackByQueryShingles, parseDecisionEntries, isParkingFile } from './knowledge-match.js'
|
|
42
42
|
|
|
43
43
|
const VECTOR_TIMEOUT_MS = 3000
|
|
44
44
|
const ENDPOINT_PATH = '/api/spec/knowledge/vector-search'
|
|
@@ -181,12 +181,14 @@ function specPathToFile(specPath) {
|
|
|
181
181
|
return String(specPath).replace(/^knowledge\//, '').replace(/\\/g, '/')
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
/** 平台候选 → matchKnowledge
|
|
185
|
-
|
|
184
|
+
/** 平台候选 → matchKnowledge 同构结果(策略面全本地解析)。导出供测试(FR-01 停车场
|
|
185
|
+
* 候选丢弃断言;策略面全在本地解析的契约不变)。 */
|
|
186
|
+
export function buildResultFromPlatform(indexDir, pvResults) {
|
|
186
187
|
const byFile = new Map()
|
|
187
188
|
for (const r of pvResults) {
|
|
188
189
|
const file = specPathToFile(r.spec_path)
|
|
189
190
|
if (!file.startsWith('decisions/')) continue
|
|
191
|
+
if (isParkingFile(file)) continue // 停车场候选丢弃(FR-01)——平台向量语料含 unmapped,策略面本地滤
|
|
190
192
|
if (!byFile.has(file)) byFile.set(file, [])
|
|
191
193
|
byFile.get(file).push(r)
|
|
192
194
|
}
|