sillyspec 3.33.2 → 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/design-facts.js +1 -1
- package/src/docs-check.js +8 -4
- package/src/doctor-diagnostics.js +89 -6
- package/src/flow-review-pack.js +35 -3
- package/src/flow-review.js +31 -8
- package/src/flow.js +6 -2
- 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 +31 -2
- 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/brainstorm.js +1 -0
- package/src/stages/knowledge.js +25 -3
- package/src/timing.js +55 -0
- package/src/ui-visual.js +3 -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/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
|
}
|
package/src/machine-interface.js
CHANGED
|
@@ -352,9 +352,24 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
352
352
|
// 绿缓存根:与 runtimeRoot 解析同源(drift 锚主仓口径);解析失败 null → 不缓存不命中
|
|
353
353
|
let greenRoot = null;
|
|
354
354
|
try { greenRoot = resolveRuntimeRoot({ runtimeRoot, specDriftAnchor }, specRoot); } catch { greenRoot = null; }
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
355
|
+
// 失败签名复用(2026-10-21-verify-reuse-large-repo FR-03):真跑前咨询上次质量扫描失败——
|
|
356
|
+
// 同签名(代码指纹×豁免面)+ 快照口径一致(CLI 门主仓口径跑,plannedSnapshot=false)+
|
|
357
|
+
// 30min 窗口 → 不重跑,合成 failed(♻️ 披露 + 四出路)照常进 check not-ok 语义。
|
|
358
|
+
// gate verify 预检每轮整跑同签名失败 10~13.5 分钟的浪费由此消除(dsh 0ff6f1c8 取证)。
|
|
359
|
+
let vt = null;
|
|
360
|
+
try {
|
|
361
|
+
const { consultFailedScanReuse, synthesizeReusedFailedCheck } = await import('./run/verify-quality-scan.js');
|
|
362
|
+
const consult = consultFailedScanReuse({
|
|
363
|
+
specBase: specRoot, cwd, changeName, plannedSnapshot: false,
|
|
364
|
+
forceRerun: Boolean(process.env.SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN),
|
|
365
|
+
});
|
|
366
|
+
if (consult.reuse) vt = synthesizeReusedFailedCheck(consult.record);
|
|
367
|
+
} catch { /* 咨询异常 → 照旧 green-cache/真跑 */ }
|
|
368
|
+
if (vt == null) {
|
|
369
|
+
vt = greenCachedCheck({ kind: 'test', greenRoot, cwd, specBase: specRoot, changeName },
|
|
370
|
+
() => runVerifyTestCheck({ cwd, specBase: specRoot, changeName, ctx }),
|
|
371
|
+
(hit) => ({ status: 'passed', exitCode: 0, durationMs: 0, resultPath: null, mode: null, fallbackReason: null, exemptedCount: 0, cached: true, reason: greenCacheNotice(hit, 'verify-test') }));
|
|
372
|
+
}
|
|
358
373
|
const vtWarnings = [];
|
|
359
374
|
if (vt.cached) vtWarnings.push(vt.reason);
|
|
360
375
|
if (vt.status === 'skipped') {
|
|
@@ -455,7 +470,7 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
455
470
|
// 回合):①target_files reconcile 只读跑(reconcileTargetFiles 纯计算不落盘,--done 的写结果
|
|
456
471
|
// 文件在其调用方);②stage review 在场探测(marker+review.json——缺则报预期路径,同 --done
|
|
457
472
|
// Stage Review Gate 口径,只报缺不生成)。全只读零副作用;未传 full 零行为变化。
|
|
458
|
-
if (full && (stage === 'verify' || stage === 'execute')) {
|
|
473
|
+
if (full && (stage === 'verify' || stage === 'execute' || stage === 'brainstorm' || stage === 'plan')) {
|
|
459
474
|
// e1. reconcile(verify --done 的 target_files 对账门)
|
|
460
475
|
if (stage === 'verify') {
|
|
461
476
|
try {
|
|
@@ -484,10 +499,13 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
484
499
|
});
|
|
485
500
|
}
|
|
486
501
|
}
|
|
487
|
-
// e2. stage review 在场探测(
|
|
488
|
-
//
|
|
489
|
-
// register 枚举亦无 verify,verify 阶段不产
|
|
490
|
-
|
|
502
|
+
// e2. stage review 在场探测(Stage Review Gate 拦截面的 --full 预检——brainstorm/plan/execute
|
|
503
|
+
// 三阶段与 gates.js Stage Review Gate 实拦范围逐字对齐;verify 不在其列,坑
|
|
504
|
+
// stage-review-verify-enum-gap 2026-10-11:register 枚举亦无 verify,verify 阶段不产
|
|
505
|
+
// stage review 期望;verify 的真实门面是 e1 对账。dsh z1 实证(2026-10-21-p0-p1-hardening
|
|
506
|
+
// FR-04):此前 execute 单值使 brainstorm Step8 --done 被 Stage Review Gate「伏击」——
|
|
507
|
+
// gate 预检全绿而 --done 被拦,46 分钟+3 轮子代理评审才过)
|
|
508
|
+
if (stage === 'execute' || stage === 'brainstorm' || stage === 'plan') try {
|
|
491
509
|
const { getLatestStageReviewRunId } = await import('./stage-review.js');
|
|
492
510
|
const rrRoot = runtimeRoot || join(specRoot, '.runtime');
|
|
493
511
|
const runId = getLatestStageReviewRunId(rrRoot, stage, changeName);
|
|
@@ -496,7 +514,7 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
496
514
|
code: checkCode('full-stage-review') || 'full_stage_review_missing',
|
|
497
515
|
ok: !!runId,
|
|
498
516
|
errors: !runId
|
|
499
|
-
? [`缺 ${stage} 阶段的 stage review.json(tier=independent
|
|
517
|
+
? [`缺 ${stage} 阶段的 stage review.json(tier=independent 要求独立审查产出)——${stage} --done 将被 Stage Review Gate 拦;可先 sillyspec register-stage-review --change ${changeName} --stage ${stage} 生成骨架再填(降级条款照 review.json reviewerNotes 首行留痕)`]
|
|
500
518
|
: [],
|
|
501
519
|
data: { runId: runId || null },
|
|
502
520
|
});
|
|
@@ -518,9 +536,12 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
518
536
|
// register 指引;在场 → 静默 ok。独立 check id 不与 --full 的 full-stage-review 碰撞,
|
|
519
537
|
// error 语义(--done 拦)仍归 --full 档与 --done 本体。
|
|
520
538
|
// stage 期望面对齐(坑 stage-review-verify-enum-gap,2026-10-11 实证):Stage Review Gate
|
|
521
|
-
// 实际只拦 brainstorm/plan/execute(gates.js),register-stage-review 枚举亦无 verify——
|
|
522
|
-
// verify 阶段不产 stage review 提示(提示命令不可执行 + 指称的门禁不存在,双重误导)。
|
|
523
|
-
|
|
539
|
+
// 实际只拦 brainstorm/plan/execute(gates.js),register-stage-review 枚举亦无 verify——
|
|
540
|
+
// verify 阶段不产 stage review 提示(提示命令不可执行 + 指称的门禁不存在,双重误导)。
|
|
541
|
+
// 覆盖面扩全(2026-10-21-p0-p1-hardening FR-04 / dsh z1 实证):此前 execute 单值——brainstorm
|
|
542
|
+
// Step8 --done 被 Stage Review Gate 伏击(预检绿灯 vs 收口拦截口径不一致,损失 46 分钟)。
|
|
543
|
+
// 现与 gates.js 拦截面三阶段逐字对齐;文案按 stage 参数化。
|
|
544
|
+
if (!full && (stage === 'execute' || stage === 'brainstorm' || stage === 'plan')) {
|
|
524
545
|
try {
|
|
525
546
|
const { getLatestStageReviewRunId } = await import('./stage-review.js');
|
|
526
547
|
const rrRoot = runtimeRoot || join(specRoot, '.runtime');
|
|
@@ -531,7 +552,7 @@ if (!full && stage === 'execute') {
|
|
|
531
552
|
ok: true,
|
|
532
553
|
informational: true,
|
|
533
554
|
warnings: runId ? [] : [
|
|
534
|
-
`缺 ${stage} 阶段的 stage review.json
|
|
555
|
+
`缺 ${stage} 阶段的 stage review.json——${stage} --done 的 Stage Review Gate 会拦(本提示不改变 gate 结论)。先产出评审再注册:sillyspec register-stage-review --change ${changeName} --stage ${stage}(骨架生成后填,或 --from review.json 过继独立评审产出)`,
|
|
535
556
|
],
|
|
536
557
|
data: { runId: runId || null },
|
|
537
558
|
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// pm._changePath(persistence-core 留 facade 本体)。无共享常量依赖。
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, renameSync } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
|
+
import { hostname } from 'os';
|
|
6
7
|
import { appendDeletedLedger } from '../deleted-ledger.js';
|
|
7
8
|
|
|
8
9
|
// 所有权活跃心跳窗缺省(2026-09-14-change-ownership-guards task-02 / FR-01):owner 非本会话
|
|
@@ -290,6 +291,7 @@ export class ChangeRegistry {
|
|
|
290
291
|
* - opts.forced(--takeover) → takeover-forced 放行(无条件接管,调用方重写 owner 留痕)
|
|
291
292
|
* - owner 为空(无行/NULL) → no-owner 放行(存量无主行,任何会话可接管)
|
|
292
293
|
* - owner === selfSession → self 放行(本会话自有 change 零路径变化)
|
|
294
|
+
* - owner 本机匿名占位 + selfSession 显式 → takeover-anon-upgrade 放行(同主机显式身份自动升级,FR-05)
|
|
293
295
|
* - owner 他人 且 now-last_active ≥ 窗口 → takeover-stale 放行(窗口外自动接管重写)
|
|
294
296
|
* - owner 他人 且活跃窗内 → blocked-active-owner 拒绝(allowed:false,调用方打结构化错误)
|
|
295
297
|
* last_active 缺失/不可解析按陈旧处理:无法证明新鲜即不拦——护栏拦「可证活跃」,不因时间戳
|
|
@@ -313,6 +315,19 @@ export class ChangeRegistry {
|
|
|
313
315
|
if (opts.forced) return { allowed: true, action: 'takeover-forced', owner, lastActive, heartbeatMs };
|
|
314
316
|
if (owner === null) return { allowed: true, action: 'no-owner', owner: null, lastActive, heartbeatMs };
|
|
315
317
|
if (owner === opts.selfSession) return { allowed: true, action: 'self', owner, lastActive, heartbeatMs };
|
|
318
|
+
// 匿名占位所有权自动升级(2026-10-21-p0-p1-hardening FR-05 / dsh z1 实证:首调未设
|
|
319
|
+
// SESSION_ID → owner 落 anon@<host>,同机 agent 数分钟后补设显式身份,被
|
|
320
|
+
// blocked-active-owner 拦 15 分钟活跃窗,被迫 --takeover——自己锁自己)。anon@ 是机器级
|
|
321
|
+
// 降级标识(缺省只拦他机),同主机的显式身份(--session/env 三级解析,非 anon 降级)严格
|
|
322
|
+
// 更具体:直接放行升级(调用方 setChangeOwner 重写留痕),不必 --takeover。跨主机匿名
|
|
323
|
+
// (owner 的 @ 后缀 ≠ 本机 hostname)与任何显式 owner 持有不在此列,照旧新鲜度判定。
|
|
324
|
+
if (
|
|
325
|
+
typeof owner === 'string' && owner.startsWith('anon@')
|
|
326
|
+
&& typeof opts.selfSession === 'string' && opts.selfSession && !opts.selfSession.startsWith('anon@')
|
|
327
|
+
&& owner === `anon@${hostname()}`
|
|
328
|
+
) {
|
|
329
|
+
return { allowed: true, action: 'takeover-anon-upgrade', owner, lastActive, heartbeatMs };
|
|
330
|
+
}
|
|
316
331
|
// owner 他人:新鲜度决定拒绝 / 自动接管
|
|
317
332
|
const lastMs = lastActive ? new Date(lastActive).getTime() : NaN;
|
|
318
333
|
const nowMs = Number.isFinite(opts.nowMs) ? opts.nowMs : Date.now();
|
package/src/progress/shared.js
CHANGED
|
@@ -16,6 +16,10 @@ export const STAGE_LABELS = {
|
|
|
16
16
|
quick: '⚡ 快速任务',
|
|
17
17
|
explore: '🧭 自由探索',
|
|
18
18
|
archive: '📦 归档变更',
|
|
19
|
+
// thin 展示键(2026-10-21-overview-thin-stage):轻量变更出身投影专用(overview
|
|
20
|
+
// stage_label 查找),不进 VALID_STAGES/STAGE_ORDER——thin 不走阶段机,加进阶段
|
|
21
|
+
// 清单会建空阶段行扰动级联。◈ 对齐 SillyHub 前端旁路徽标字形(◈ thin)。
|
|
22
|
+
thin: '◈ 轻量变更',
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
// 完整阶段顺序(含 scan),仅用于展示/迭代顺序
|
|
@@ -9,6 +9,9 @@ import { writeAtomicSync } from '../fs-atomic.js';
|
|
|
9
9
|
import { timeAgo } from '../datetime.js';
|
|
10
10
|
import { runValidators } from '../stage-contract.js';
|
|
11
11
|
import { QUICK_SID_RE } from '../run/shared.js';
|
|
12
|
+
// thin 出身投影读 flow-state(2026-10-21-overview-thin-stage):叶子模块,与 progress.js
|
|
13
|
+
// 2026-10-13-thin-sync-stage 同款 import 先例,零新依赖链。
|
|
14
|
+
import { readFlowState } from '../flow-state.js';
|
|
12
15
|
import { VALID_STAGES, STAGE_LABELS, STAGE_ORDER, MAIN_FLOW_ORDER, SPEC_DIR_NAME, CURRENT_VERSION, STALL_WARN_DAYS, emptyStage } from './shared.js';
|
|
13
16
|
|
|
14
17
|
// quick 会话行(quick-<8hex>)按设计无实体 changes/ 目录(progress.js initChange 跳过建目录),
|
|
@@ -314,12 +317,22 @@ export class StageMachine {
|
|
|
314
317
|
for (const cn of changes) {
|
|
315
318
|
const data = this.pm.read(cwd, cn);
|
|
316
319
|
const dirMissing = _isGhostChange(changesRoot, cn);
|
|
320
|
+
// thin 出身投影(2026-10-21-overview-thin-stage,对齐 2026-10-13-thin-sync-stage
|
|
321
|
+
// 平台上行同款覆写):本地行出生恒 'brainstorm'(thin 不走阶段机),总览按
|
|
322
|
+
// flow-state tier 覆写展示值——否则面板把轻量变更渲染成 brainstorm 主管线。仅覆写
|
|
323
|
+
// envelope 载荷,DB 行不动;listChanges 恒回 active 行(归档不进循环,sync 侧的
|
|
324
|
+
// status 守卫在此天然满足);flow-state 缺失/损坏 readFlowState→null、tier=thick
|
|
325
|
+
// 一律按行值(fail-open 同 sync 口径)。
|
|
326
|
+
let currentStage = data?.currentStage || null;
|
|
327
|
+
if (currentStage && readFlowState(this.pm._changePath(cwd, cn))?.tier === 'thin') {
|
|
328
|
+
currentStage = 'thin';
|
|
329
|
+
}
|
|
317
330
|
const entry = {
|
|
318
331
|
name: cn,
|
|
319
332
|
readable: !!data,
|
|
320
333
|
ghost: dirMissing,
|
|
321
|
-
current_stage:
|
|
322
|
-
stage_label:
|
|
334
|
+
current_stage: currentStage,
|
|
335
|
+
stage_label: currentStage ? (STAGE_LABELS[currentStage] || currentStage) : null,
|
|
323
336
|
last_active: this.pm._dumpIso(data?.lastActive) || null,
|
|
324
337
|
};
|
|
325
338
|
if (data) {
|
package/src/progress.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, appendFileSync, copyFileSync, readdirSync, statSync, realpathSync } from 'fs';
|
|
14
14
|
import { RUNTIME_TEMP_DIRNAME, runtimeTempPath } from './runtime-paths.js'
|
|
15
|
+
import { timedSync } from './timing.js';
|
|
15
16
|
import { join, basename, dirname, resolve, sep } from 'path';
|
|
16
17
|
import { tmpdir, hostname } from 'os';
|
|
17
18
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
@@ -959,6 +960,10 @@ export class ProgressManager {
|
|
|
959
960
|
}
|
|
960
961
|
|
|
961
962
|
_write(cwd, data, changeName = null) {
|
|
963
|
+
return timedSync('state-write', () => this._writeImpl(cwd, data, changeName))
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
_writeImpl(cwd, data, changeName = null) {
|
|
962
967
|
const cn = changeName || data.currentChange;
|
|
963
968
|
if (!cn) {
|
|
964
969
|
console.warn('⚠️ _write: 无变更名,跳过写入');
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* redomain-unmapped.js — unmapped 停车场存量批量清偿驱动(FR-04,2026-10-21-knowledge-tier-denoise)
|
|
3
|
+
*
|
|
4
|
+
* 决策/FR 条目落 unmapped 是域路由失败的兜底——源头匹配器已修(2026-10-15-module-path-glob-match),
|
|
5
|
+
* 存量需机器通道清偿(decision-redomain.js 头注:迁移原语已在,缺批量推断与编排)。本模块即编排:
|
|
6
|
+
* - decisions 条目:文件锚(「文件:」字段行)逐文件 matchedModuleIds 投票 → 目标真模块域
|
|
7
|
+
* - fr 条目:来源变更覆盖面(frCoverageFiles 三源:归档 design 交付表 ∪ change-patch ∪ git)→ 同款投票
|
|
8
|
+
* - 投票零命中/无锚 → 留 unmapped 并在报告列明残量原因(宁漏勿错,不裸删)
|
|
9
|
+
* 迁移原语复用 redomainDecisionEntries / redomainFrEntries:段字节整体搬运、迁域不换号、
|
|
10
|
+
* 目标域同 id 同变更冲突跳过、幂等;INDEX 路由行随原语内 sync 维护(FR-02 泛词已退役)。
|
|
11
|
+
* 只动 knowledge 活库,不回写归档件(D-unmapped-152 死路:归档是冻结审计面)。
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from 'fs'
|
|
14
|
+
import { join } from 'path'
|
|
15
|
+
import { splitKnowledgeSections, discoverModuleIndex } from './decision-distill.js'
|
|
16
|
+
import { matchedModuleIds, frCoverageFiles, FR_SECTION_RE } from './fr-index.js'
|
|
17
|
+
import { anchorFilePaths } from './knowledge-match.js'
|
|
18
|
+
import { redomainDecisionEntries, sectionMeta } from './decision-redomain.js'
|
|
19
|
+
import { redomainFrEntries } from './redomain.js'
|
|
20
|
+
|
|
21
|
+
const DECISION_FILE_LINE_RE = /^文件[::]\s*(.+)$/
|
|
22
|
+
const DECISION_ANCHOR_LINE_RE = /^锚点[::]\s*(.*)$/
|
|
23
|
+
const FR_HEADER_RE = /^## (FR-[\w.-]+-\d+|FR-\d+)\s*(.*)$/
|
|
24
|
+
|
|
25
|
+
/** 决策段 → 路径数组:双源——「文件:」行集(显式字段)∪「锚点:」行 anchorFilePaths 路径
|
|
26
|
+
* 提取(本仓实证 443 条全走锚点源:distill 的 文件: 字段极少发射,锚点是标准兜底
|
|
27
|
+
* 命中源——与 knowledge-match/knowledge-graph 的 anchors 边同判法)。 */
|
|
28
|
+
function filesFromDecisionSection(lines) {
|
|
29
|
+
const out = []
|
|
30
|
+
for (const l of lines) {
|
|
31
|
+
const line = String(l || '')
|
|
32
|
+
const m = line.match(DECISION_FILE_LINE_RE)
|
|
33
|
+
if (m) out.push(...String(m[1]).split(/[,,、\s]+/).map((s) => s.replace(/\\/g, '/')).filter(Boolean))
|
|
34
|
+
const a = line.match(DECISION_ANCHOR_LINE_RE)
|
|
35
|
+
if (a) out.push(...anchorFilePaths(a[1]))
|
|
36
|
+
}
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** FR 段「变更:」行(source change 迁移过滤键同款判法)。 */
|
|
41
|
+
function changeFromSection(lines) {
|
|
42
|
+
for (const l of lines) {
|
|
43
|
+
const m = String(l || '').match(/^变更[::]\s*(\S+)/)
|
|
44
|
+
if (m) return m[1]
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 文件集 → 目标域投票:逐文件 matchedModuleIds(修复后的 glob 匹配器,多命中并计),
|
|
50
|
+
* 最高票为域;平票字典序(确定性)。零命中 → null(留停车场)。 */
|
|
51
|
+
function voteDomain(files, moduleIndex) {
|
|
52
|
+
const tally = new Map()
|
|
53
|
+
for (const f of files) {
|
|
54
|
+
for (const mid of matchedModuleIds(moduleIndex, f)) tally.set(mid, (tally.get(mid) || 0) + 1)
|
|
55
|
+
}
|
|
56
|
+
if (tally.size === 0) return null
|
|
57
|
+
return [...tally.entries()].sort((a, b) => (b[1] - a[1]) || (a[0] < b[0] ? -1 : 1))[0][0]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function planDecisionsFace(knowledgeRoot, archiveRoot, moduleIndex) {
|
|
61
|
+
const fromPath = join(knowledgeRoot, 'decisions', 'unmapped.md')
|
|
62
|
+
if (!existsSync(fromPath)) return { absent: true, groups: [], leftover: [] }
|
|
63
|
+
const parsed = splitKnowledgeSections(readFileSync(fromPath, 'utf8'))
|
|
64
|
+
const groups = new Map()
|
|
65
|
+
const leftover = []
|
|
66
|
+
for (const s of parsed.sections) {
|
|
67
|
+
const meta = sectionMeta(s)
|
|
68
|
+
if (!meta) continue
|
|
69
|
+
// 三源:文件: 行 ∪ 锚点路径(显式锚优先)→ 都不中再走来源变更覆盖面(fr 面同款——
|
|
70
|
+
// 本仓实证 417/443 锚点「未记录」,436/443 有「变更:」行,覆盖面是主力推断源)
|
|
71
|
+
const files = filesFromDecisionSection(s.lines)
|
|
72
|
+
const change = changeFromSection(s.lines)
|
|
73
|
+
let domain = files.length > 0 ? voteDomain(files, moduleIndex) : null
|
|
74
|
+
if (!domain && change) {
|
|
75
|
+
const cov = [...frCoverageFiles({ archiveRoot, changeName: change })]
|
|
76
|
+
if (cov.length > 0) domain = voteDomain(cov, moduleIndex)
|
|
77
|
+
}
|
|
78
|
+
if (!domain) {
|
|
79
|
+
const reason = files.length === 0
|
|
80
|
+
? (change ? '锚点无路径且来源覆盖面不命中任何模块' : '无文件锚/锚点路径且无来源变更')
|
|
81
|
+
: '锚不命中任何模块'
|
|
82
|
+
leftover.push({ id: meta.id, title: meta.title, reason })
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
if (!groups.has(domain)) groups.set(domain, [])
|
|
86
|
+
groups.get(domain).push(meta.id)
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
groups: [...groups.entries()].map(([domain, ids]) => ({ domain, ids })),
|
|
90
|
+
leftover,
|
|
91
|
+
total: parsed.sections.filter((s) => sectionMeta(s)).length,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function planFrFace(knowledgeRoot, archiveRoot, moduleIndex) {
|
|
96
|
+
const fromPath = join(knowledgeRoot, 'fr', 'unmapped.md')
|
|
97
|
+
if (!existsSync(fromPath)) return { absent: true, groups: [], leftover: [] }
|
|
98
|
+
const parsed = splitKnowledgeSections(readFileSync(fromPath, 'utf8'), { sectionRegex: FR_SECTION_RE, buildId: (n) => n })
|
|
99
|
+
const groups = new Map()
|
|
100
|
+
const leftover = []
|
|
101
|
+
for (const s of parsed.sections) {
|
|
102
|
+
const h = FR_HEADER_RE.exec(s.lines[0] || '')
|
|
103
|
+
if (!h) continue
|
|
104
|
+
const id = h[1]
|
|
105
|
+
const title = (h[2] || '').trim()
|
|
106
|
+
const change = changeFromSection(s.lines)
|
|
107
|
+
const files = change ? [...frCoverageFiles({ archiveRoot, changeName: change })] : []
|
|
108
|
+
const domain = files.length > 0 ? voteDomain(files, moduleIndex) : null
|
|
109
|
+
if (!domain) {
|
|
110
|
+
leftover.push({ id, title, reason: !change ? '无来源变更' : '来源变更覆盖面不命中任何模块' })
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
if (!groups.has(domain)) groups.set(domain, [])
|
|
114
|
+
groups.get(domain).push(id)
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
groups: [...groups.entries()].map(([domain, ids]) => ({ domain, ids })),
|
|
118
|
+
leftover,
|
|
119
|
+
total: parsed.sections.filter((s) => FR_HEADER_RE.exec(s.lines[0] || '')).length,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 干跑计划:各面分组(目标域 → 条目 id 集)与残量(id/标题/不可推断原因)。
|
|
125
|
+
* @param {{knowledgeRoot: string, archiveRoot: string}} opts
|
|
126
|
+
* @returns {{decisions: object, fr: object}} 各面 {absent?, groups, leftover, total?}
|
|
127
|
+
*/
|
|
128
|
+
export function planUnmappedRedomain({ knowledgeRoot, archiveRoot }) {
|
|
129
|
+
const moduleIndex = discoverModuleIndex(knowledgeRoot) || { modules: {} }
|
|
130
|
+
return {
|
|
131
|
+
decisions: planDecisionsFace(knowledgeRoot, archiveRoot, moduleIndex),
|
|
132
|
+
fr: planFrFace(knowledgeRoot, archiveRoot, moduleIndex),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 执行清偿(先内部重算计划——apply 自带干跑等价性;幂等:重跑同批 0 迁移)。
|
|
138
|
+
* 单组失败不拦其余组(逐组收集结果与错误如实上报)。
|
|
139
|
+
* @param {{knowledgeRoot: string, archiveRoot: string, faces?: string[]}} opts faces 缺省双面
|
|
140
|
+
* @returns {{results: object, plan: object}} results 各面 {moved, movedByDomain, skippedConflicts, errors}
|
|
141
|
+
*/
|
|
142
|
+
export function applyUnmappedRedomain({ knowledgeRoot, archiveRoot, faces = ['decisions', 'fr'] }) {
|
|
143
|
+
const plan = planUnmappedRedomain({ knowledgeRoot, archiveRoot })
|
|
144
|
+
const empty = { moved: 0, movedByDomain: {}, skippedConflicts: [], errors: [] }
|
|
145
|
+
const results = {}
|
|
146
|
+
const run = (groups, migrate) => {
|
|
147
|
+
const acc = { moved: 0, movedByDomain: {}, skippedConflicts: [], errors: [] }
|
|
148
|
+
for (const g of groups) {
|
|
149
|
+
try {
|
|
150
|
+
const res = migrate(g)
|
|
151
|
+
acc.moved += res.moved.length
|
|
152
|
+
acc.movedByDomain[g.domain] = res.moved.length
|
|
153
|
+
if (Array.isArray(res.skippedConflicts)) acc.skippedConflicts.push(...res.skippedConflicts.map((id) => `${g.domain}:${id}`))
|
|
154
|
+
} catch (e) {
|
|
155
|
+
acc.errors.push({ domain: g.domain, error: e && e.message ? e.message : String(e) })
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return acc
|
|
159
|
+
}
|
|
160
|
+
results.decisions = faces.includes('decisions')
|
|
161
|
+
? run(plan.decisions.groups,
|
|
162
|
+
(g) => redomainDecisionEntries({ knowledgeRoot, from: 'unmapped', to: g.domain, anchors: g.ids }))
|
|
163
|
+
: { ...empty }
|
|
164
|
+
results.fr = faces.includes('fr')
|
|
165
|
+
? run(plan.fr.groups,
|
|
166
|
+
(g) => redomainFrEntries({ knowledgeRoot, from: 'unmapped', to: g.domain, anchors: g.ids }))
|
|
167
|
+
: { ...empty }
|
|
168
|
+
return { results, plan }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* CLI 子命令(stages/knowledge.js 经 tryImportSubcommandImpl 装载):
|
|
173
|
+
* sillyspec knowledge redomain-unmapped [--face decisions|fr] [--apply]
|
|
174
|
+
* 缺省 dry-run 只出报告(JSON 出口与 search/graph 同族);--apply 幂等落盘。
|
|
175
|
+
*/
|
|
176
|
+
export async function cmdRedomainUnmapped(dir, args, opts = {}) {
|
|
177
|
+
const specBase = opts.specDir || join(dir, '.sillyspec')
|
|
178
|
+
const knowledgeRoot = join(specBase, 'knowledge')
|
|
179
|
+
const archiveRoot = join(specBase, 'changes', 'archive')
|
|
180
|
+
const face = args.includes('--face')
|
|
181
|
+
? (args[args.indexOf('--face') + 1] === 'decisions' ? 'decisions' : args[args.indexOf('--face') + 1] === 'fr' ? 'fr' : 'all')
|
|
182
|
+
: 'all'
|
|
183
|
+
const apply = args.includes('--apply')
|
|
184
|
+
const emit = (ok, data, error) => console.log(JSON.stringify({ ok, ...data, ...(error ? { error } : {}) }, null, 2))
|
|
185
|
+
|
|
186
|
+
if (!existsSync(join(knowledgeRoot, 'INDEX.md'))) {
|
|
187
|
+
emit(false, {}, { code: 'knowledge_dir_missing', path: knowledgeRoot })
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
if (apply) {
|
|
191
|
+
const faces = face === 'all' ? ['decisions', 'fr'] : [face]
|
|
192
|
+
const { results, plan } = applyUnmappedRedomain({ knowledgeRoot, archiveRoot, faces })
|
|
193
|
+
const leftoverFace = (name) => (faces.includes(name) ? plan[name].leftover : [])
|
|
194
|
+
emit(true, {
|
|
195
|
+
mode: 'apply', face,
|
|
196
|
+
decisions: results.decisions,
|
|
197
|
+
fr: results.fr,
|
|
198
|
+
leftover: { decisions: leftoverFace('decisions'), fr: leftoverFace('fr') },
|
|
199
|
+
})
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
const plan = planUnmappedRedomain({ knowledgeRoot, archiveRoot })
|
|
203
|
+
emit(true, { mode: 'dry-run', face, decisions: plan.decisions, fr: plan.fr })
|
|
204
|
+
}
|
package/src/redomain.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs'
|
|
16
16
|
import { join } from 'path'
|
|
17
|
-
import { splitKnowledgeSections, joinKnowledgeFile, syncIndexRoutingLines } from './decision-distill.js'
|
|
17
|
+
import { splitKnowledgeSections, joinKnowledgeFile, syncIndexRoutingLines, FR_ROUTE_STRIP_TOKENS } from './decision-distill.js'
|
|
18
18
|
import { FR_SECTION_RE } from './fr-index.js'
|
|
19
19
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
20
20
|
|
|
@@ -141,9 +141,11 @@ export function redomainFrEntries({ knowledgeRoot, from, to, anchors = null, byC
|
|
|
141
141
|
syncIndexRoutingLines(knowledgeRoot, {
|
|
142
142
|
section: 'FR 需求索引',
|
|
143
143
|
subdir: 'fr',
|
|
144
|
+
// 泛词退役(FR-02,2026-10-21-knowledge-tier-denoise):与 fr-index.js 蒸馏侧同款模板
|
|
145
|
+
stripTokens: FR_ROUTE_STRIP_TOKENS,
|
|
144
146
|
makeLine: (d) => d.startsWith('auto-')
|
|
145
|
-
? `- ${d}|${d.slice(5)}
|
|
146
|
-
: `- ${d}
|
|
147
|
+
? `- ${d}|${d.slice(5)} → [fr/${d}.md](fr/${d}.md)`
|
|
148
|
+
: `- ${d} → [fr/${d}.md](fr/${d}.md)`,
|
|
147
149
|
})
|
|
148
150
|
} catch { /* INDEX 缺席/同步失败不拦迁移(主手术已成) */ }
|
|
149
151
|
const after = existsSync(indexPath) ? readFileSync(indexPath, 'utf8') : ''
|