sillyspec 3.33.3 → 3.33.5
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-config/SKILL.md +48 -0
- 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/config-schema.js +28 -0
- package/src/config-validate.js +180 -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/flow-parity.js +22 -2
- package/src/flow-preflight.js +67 -0
- package/src/flow.js +252 -55
- 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 +111 -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 +208 -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-handlers.js +38 -4
- package/src/run/complete.js +185 -14
- package/src/run/gates.js +30 -1
- package/src/run/prompt.js +9 -1
- package/src/run/quick-audit.js +4 -2
- 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 +153 -11
- 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
|
@@ -317,6 +317,57 @@ export function shouldReuseLastPassedScan({ lastRecord, currentPassedKey, planne
|
|
|
317
317
|
return { reuse: true, reason: null }
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
/**
|
|
321
|
+
* 收口门/gate verify 的失败签名复用咨询(2026-10-21-verify-reuse-large-repo FR-03)。
|
|
322
|
+
* 包装既有三件(loadLastQualityScanRecord + computeQualityScanDedupKey +
|
|
323
|
+
* shouldReuseLastFailedScan),供 gates.js 实测 else 分支与 machine-interface verify-test
|
|
324
|
+
* facet 在真跑前调用:上次 failed 且失败签名未变 → 不重跑,消费侧合成 failed 结果照常阻断
|
|
325
|
+
* (dsh 0ff6f1c8 取证:同签名失败在收口门前被完整重跑,每轮 10~13.5 分钟)。
|
|
326
|
+
*
|
|
327
|
+
* 与 noAI 步内 shouldReuseLastFailedScan 的两点刻意差异:
|
|
328
|
+
* 1. TTL(缺省 30min,对齐 green-cache 窗口)——dedupKey 不含测试面,agent 手动补测试
|
|
329
|
+
* 绑定后面漂移(记录面 X,本轮应跑面 X+Δ)在 noAI 步由「步骤不推进」自兜底,收口门
|
|
330
|
+
* 复用会把旧面失败压到新面收口上,窗口须有界;
|
|
331
|
+
* 2. RERUN 逃生阀由消费侧透传 forceRerun(与 noAI 失败签名闸同阀对称——环境确已修好等
|
|
332
|
+
* 场景不因复用层锁死)。
|
|
333
|
+
* fail-closed:任一判据不可得/失配 → { reuse: false, reason },消费侧照旧真跑。
|
|
334
|
+
*/
|
|
335
|
+
export function consultFailedScanReuse({ specBase, cwd, changeName, plannedSnapshot, maxAgeMs = 30 * 60 * 1000, forceRerun = false, now = Date.now() }) {
|
|
336
|
+
const lastRecord = loadLastQualityScanRecord({ specBase, changeName })
|
|
337
|
+
const verdict = shouldReuseLastFailedScan({
|
|
338
|
+
lastRecord,
|
|
339
|
+
currentDedupKey: computeQualityScanDedupKey({ cwd, specBase }),
|
|
340
|
+
plannedSnapshot,
|
|
341
|
+
forceRerun,
|
|
342
|
+
})
|
|
343
|
+
if (!verdict.reuse) return { reuse: false, reason: verdict.reason, record: null }
|
|
344
|
+
if (maxAgeMs != null) {
|
|
345
|
+
const ranAtMs = Date.parse(lastRecord.ranAt || '')
|
|
346
|
+
if (!Number.isFinite(ranAtMs) || now - ranAtMs > maxAgeMs) {
|
|
347
|
+
return { reuse: false, reason: `ttl-expired(上次实测 ${lastRecord.ranAt || '未知时间'} 超 ${Math.round(maxAgeMs / 60000)}min 窗口——面漂移防护,照旧真跑)`, record: null }
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return { reuse: true, reason: null, record: lastRecord }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** consultFailedScanReuse 命中后的合成 testCheck(消费侧共用——披露统一、字段齐全) */
|
|
354
|
+
export function synthesizeReusedFailedCheck(record) {
|
|
355
|
+
const t = (record && record.testResult) || {}
|
|
356
|
+
return {
|
|
357
|
+
status: 'failed',
|
|
358
|
+
reason: `♻️ 复用上次失败实测(失败签名未变:代码指纹/豁免面/快照口径全等${record && record.ranAt ? `,实测于 ${record.ranAt}` : ''},免重跑)——${(t.reason || '测试失败').split('\n')[0]}。出路:① 修代码 ② 补 known_failures 豁免(均即失配自动重测)③ 强制重跑:SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1(旁路本复用层直接重跑;=force 连 noAI 步失败签名闸一并旁路)`,
|
|
359
|
+
command: `${t.command || 'commands.test'} (failed-scan-reuse)`,
|
|
360
|
+
exitCode: typeof t.exitCode === 'number' ? t.exitCode : 1,
|
|
361
|
+
durationMs: 0,
|
|
362
|
+
outputTail: t.outputTail || null,
|
|
363
|
+
resultPath: t.resultPath || null,
|
|
364
|
+
strategy: t.strategy || null,
|
|
365
|
+
failureRemaining: t.failureRemaining || t.failure_remaining || null,
|
|
366
|
+
failureExempted: t.failureExempted || t.failure_exempted || null,
|
|
367
|
+
reusedFromRecord: true,
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
320
371
|
/**
|
|
321
372
|
* 伪影分诊编排(ql-20260920-010 修复一 a):测试失败 → classify 分类 → 命中且本次是
|
|
322
373
|
* 快照口径 → 主树口径单点复跑对照。复跑过 = 伪影坐实:返回主树结果 + artifactTriage
|
package/src/runtime-paths.js
CHANGED
|
@@ -74,6 +74,9 @@ export const RUNTIME_STATE_SINGLETONS = [
|
|
|
74
74
|
'spec-sync-last-success.json',
|
|
75
75
|
'spec-sync-fail-streak.json',
|
|
76
76
|
'spec-sync-follow-reported.json',
|
|
77
|
+
// 2026-10-21-cli-manifest-since-cache:spec-manifest 增量游标缓存(有读者、
|
|
78
|
+
// 不可再生前同步全量代价高——state/ 语义)
|
|
79
|
+
'spec-manifest-cache.json',
|
|
77
80
|
'sync-noise-self-heal.json',
|
|
78
81
|
'last-delta.json',
|
|
79
82
|
]
|
|
@@ -610,18 +610,21 @@ export class SillyHubMcpClient {
|
|
|
610
610
|
* artifacts 原样透传不裁剪:平台实返形态([{kind, content_ref, id}…] 或演进形态)整组
|
|
611
611
|
* 透传——kind 匹配 / JSON 提取等语义解析归消费方(review-dispatch 的
|
|
612
612
|
* extractReviewFromArtifacts),本封装保持传输层纯度(task-02 constraints)。
|
|
613
|
+
* answer 原文透传(2026-10-21-review-dispatch-answer-salvage / dsh 会话实证):worker 可能把
|
|
614
|
+
* review JSON 放回答正文而非 artifacts——取 tool 返回的 answer 字段(缺省兜底 result 字段)
|
|
615
|
+
* 字符串原文一并透传,非字符串/双缺 → null;解析仍归消费方,传输层只搬运。
|
|
613
616
|
* @param {object} p
|
|
614
617
|
* @param {string} p.missionId
|
|
615
618
|
* @param {string} p.workerId
|
|
616
|
-
* @returns {Promise<{workerId: string|null, status: string, artifacts: Array}>}
|
|
617
|
-
* 正常 → {workerId, status, artifacts} 原样解析(status 缺省 'unknown',artifacts 缺省 []);
|
|
618
|
-
* 未配置 / 调用失败(null) / tool isError → { workerId: null, status: 'unavailable', artifacts: [] }
|
|
619
|
+
* @returns {Promise<{workerId: string|null, status: string, artifacts: Array, answer: string|null}>}
|
|
620
|
+
* 正常 → {workerId, status, artifacts, answer} 原样解析(status 缺省 'unknown',artifacts 缺省 [],answer 缺省 null);
|
|
621
|
+
* 未配置 / 调用失败(null) / tool isError → { workerId: null, status: 'unavailable', artifacts: [], answer: null }
|
|
619
622
|
* (与 dispatchWorker/getDaemonStatus 同族 best-effort,绝不向上抛)
|
|
620
623
|
*/
|
|
621
624
|
async getWorkerResult({ missionId, workerId } = {}) {
|
|
622
|
-
if (!this._configured) return { workerId: null, status: 'unavailable', artifacts: [] };
|
|
625
|
+
if (!this._configured) return { workerId: null, status: 'unavailable', artifacts: [], answer: null };
|
|
623
626
|
const result = await this._callTool('get_worker_result', { mission_id: missionId, worker_id: workerId });
|
|
624
|
-
if (result === null || result.isError) return { workerId: null, status: 'unavailable', artifacts: [] };
|
|
627
|
+
if (result === null || result.isError) return { workerId: null, status: 'unavailable', artifacts: [], answer: null };
|
|
625
628
|
const value = this._parseToolReturnValue(result);
|
|
626
629
|
// workerId 兜底链与 dispatchWorker 同款(2026-09-10 平台实返 {id,…} 形态实证)
|
|
627
630
|
const wid = value && typeof value === 'object'
|
|
@@ -631,7 +634,9 @@ export class SillyHubMcpClient {
|
|
|
631
634
|
? (value.status ?? 'unknown')
|
|
632
635
|
: 'unknown';
|
|
633
636
|
const artifacts = value && Array.isArray(value.artifacts) ? value.artifacts : [];
|
|
634
|
-
|
|
637
|
+
const rawAnswer = value && typeof value === 'object' ? (value.answer ?? value.result) : null;
|
|
638
|
+
const answer = typeof rawAnswer === 'string' ? rawAnswer : null;
|
|
639
|
+
return { workerId: wid, status, artifacts, answer };
|
|
635
640
|
}
|
|
636
641
|
|
|
637
642
|
/**
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spawn-settle —— detached 子进程孵化稳定化(2026-10-21-p0-p1-hardening FR-08)
|
|
3
|
+
*
|
|
4
|
+
* 背景(dsh z1/z2 各实证一次):Windows 下父进程 spawn detached 子进程(watcher/bg-sync)后
|
|
5
|
+
* 立即退出,可触发 Node libuv 退出断言崩溃(`Assertion failed: !(handle->flags &
|
|
6
|
+
* UV_HANDLE_CLOSING) src\win\async.c`)——把本已成功的 CLI 调用污染成 exit 1。根因在 Node
|
|
7
|
+
* 运行时(父进程退出与子进程 async 句柄建立竞态),应用层无法根除;本 util 提供有界缓解:
|
|
8
|
+
* spawn 后等子进程 'spawn' 事件(孵化完成、句柄建立完毕)或 'error' 再放行,上限 timeoutMs
|
|
9
|
+
* 兜底——把「父进程在句柄建立中途退出」的竞态窗口压掉。best-effort:任何异常路径直接返回,
|
|
10
|
+
* 绝不影响命令功能。
|
|
11
|
+
*/
|
|
12
|
+
import { EventEmitter } from 'node:events';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 等待 detached 子进程孵化稳定。
|
|
16
|
+
* @param {import('node:child_process').ChildProcess} child
|
|
17
|
+
* @param {number} [timeoutMs=150] 兜底上限('spawn'/'error' 均未发出时按此时长放行)
|
|
18
|
+
* @returns {Promise<void>} 事件到达 / 兜底超时 / child 已退出 / child 非法形态——均 resolve(永不 reject)
|
|
19
|
+
*/
|
|
20
|
+
export async function awaitSpawnSettled(child, timeoutMs = 150) {
|
|
21
|
+
if (!child || typeof child.once !== 'function') return;
|
|
22
|
+
// 已退出(同步失败形态:exitCode/signalCode 已定)——无句柄在途,立即返回
|
|
23
|
+
if (child.exitCode !== null && child.exitCode !== undefined) return;
|
|
24
|
+
if (child.signalCode) return;
|
|
25
|
+
await new Promise((resolve) => {
|
|
26
|
+
let settled = false;
|
|
27
|
+
const fin = () => {
|
|
28
|
+
if (settled) return;
|
|
29
|
+
settled = true;
|
|
30
|
+
try { child.off('spawn', fin); child.off('error', fin); } catch { /* 已解绑/非 EventEmitter——best-effort */ }
|
|
31
|
+
resolve();
|
|
32
|
+
};
|
|
33
|
+
child.once('spawn', fin);
|
|
34
|
+
child.once('error', fin);
|
|
35
|
+
// 上限兜底:两事件都不发(异常宿主/极端调度)也不让调用方久等
|
|
36
|
+
const t = setTimeout(fin, timeoutMs);
|
|
37
|
+
t.unref?.();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 测试辅助:构造最小 child 形态(EventEmitter + exitCode/signalCode 字段)。 */
|
|
42
|
+
export function fakeChildForTest({ exitCode = null, signalCode = null } = {}) {
|
|
43
|
+
const c = new EventEmitter();
|
|
44
|
+
c.exitCode = exitCode;
|
|
45
|
+
c.signalCode = signalCode;
|
|
46
|
+
return c;
|
|
47
|
+
}
|
package/src/spec-sync.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { createHash } from 'crypto';
|
|
19
|
+
import { timedAsync } from './timing.js';
|
|
19
20
|
import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
|
|
20
21
|
import { join, relative, sep } from 'path';
|
|
21
22
|
import { bindSyncNoiseRoot, syncConnectionWarn, isConnectionClassStatus, noteSyncSuccess } from './sync-noise.js';
|
|
@@ -433,6 +434,48 @@ function writeBaseSnapshot(specRoot, localFiles) {
|
|
|
433
434
|
} catch { /* 快照失败只损失下次的内容级防回推,不影响本次同步结果 */ }
|
|
434
435
|
}
|
|
435
436
|
|
|
437
|
+
// ── spec-manifest 增量游标缓存(2026-10-21-cli-manifest-since-cache)──
|
|
438
|
+
// 平台 spec-manifest 支持 ?since=<spec_version> 增量返回(multi-agent-platform
|
|
439
|
+
// 2026-10-21-spec-manifest-incremental)。缓存 = 上次消费的整份清单 + 游标 +
|
|
440
|
+
// 连续增量计数:稳态拉取只传变化行(生产实测全量 2.2MB/13145 行、5.5s@~400KBps
|
|
441
|
+
// 链路——单请求吃掉 CLI 同步 8s 总预算七成;增量后 <10KB/亚秒)。
|
|
442
|
+
// 形态 {spec_version:int, files:{path:{hash,version,exists}}, inc_count:int};
|
|
443
|
+
// .runtime 在上传排除面,缓存不进同步树。fail-soft:读写任何失败都退化为
|
|
444
|
+
// 全量拉取(与无缓存同路径,行为不劣于现状)。
|
|
445
|
+
const MANIFEST_CACHE = 'spec-manifest-cache.json';
|
|
446
|
+
|
|
447
|
+
// 定期全量对账阈值:连续增量 N 轮后强制不带 since 全量(缓存与服务器脱钩的
|
|
448
|
+
// 幻影行防线,design 风险节)。env SILLYSPEC_SPEC_SYNC_RECONCILE_EVERY 可调。
|
|
449
|
+
function resolveReconcileEvery() {
|
|
450
|
+
const raw = Number(process.env.SILLYSPEC_SPEC_SYNC_RECONCILE_EVERY);
|
|
451
|
+
if (Number.isInteger(raw) && raw >= 1 && raw <= 10000) return raw;
|
|
452
|
+
return 50;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function readManifestCache(specRoot) {
|
|
456
|
+
try {
|
|
457
|
+
const c = JSON.parse(readFileSync(runtimeStatePath(join(specRoot, '.runtime'), MANIFEST_CACHE), 'utf8'));
|
|
458
|
+
if (
|
|
459
|
+
c && Number.isInteger(c.spec_version) && c.files && typeof c.files === 'object'
|
|
460
|
+
&& Number.isInteger(c.inc_count)
|
|
461
|
+
) return c;
|
|
462
|
+
return null;
|
|
463
|
+
} catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function writeManifestCache(specRoot, cache) {
|
|
469
|
+
try {
|
|
470
|
+
mkdirSync(join(specRoot, '.runtime', 'state'), { recursive: true });
|
|
471
|
+
writeAtomicSync(runtimeStatePath(join(specRoot, '.runtime'), MANIFEST_CACHE), JSON.stringify(cache));
|
|
472
|
+
} catch { /* 缓存写失败只损失增量优化,下轮全量(fail-soft) */ }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function clearManifestCache(specRoot) {
|
|
476
|
+
try { unlinkSync(runtimeStatePath(join(specRoot, '.runtime'), MANIFEST_CACHE)); } catch { /* 不存在即无事 */ }
|
|
477
|
+
}
|
|
478
|
+
|
|
436
479
|
/**
|
|
437
480
|
* 丢弃「跟随服务器」型 update op(坑 quicksync-conflict-granularity ①)。
|
|
438
481
|
*
|
|
@@ -682,6 +725,10 @@ function attributeChangeLabel(paths) {
|
|
|
682
725
|
* (syncSpecTreeOnly / resolve forcePush 等)不消费该字段,向后兼容。
|
|
683
726
|
*/
|
|
684
727
|
export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
728
|
+
return timedAsync('sync', () => syncSpecTreeImpl(specRoot, platform, changeName, opts))
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async function syncSpecTreeImpl(specRoot, platform, changeName, opts = {}) {
|
|
685
732
|
if (!platform || !platform.url || !platform.token) {
|
|
686
733
|
debugLog('[spec-sync] 未连接平台(本地合法状态);跳过 spec 树增量同步');
|
|
687
734
|
return { synced: 0, ok: true };
|
|
@@ -698,10 +745,15 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
698
745
|
const manifestUrl = `${platform.url.replace(/\/$/, '')}/api/changes/-/spec-manifest`;
|
|
699
746
|
const syncUrl = `${platform.url.replace(/\/$/, '')}/api/changes/-/spec-sync`;
|
|
700
747
|
|
|
701
|
-
// 1. GET
|
|
748
|
+
// 1. GET 服务器清单(2026-10-21-cli-manifest-since-cache:缓存感知——有可用
|
|
749
|
+
// 缓存带 ?since=<游标> 拉增量行合并;无缓存/对账轮/SILLYSPEC_SPEC_SYNC_FULL=1
|
|
750
|
+
// 走全量并落缓存;旧平台响应无 spec_version → 作废缓存回退现状行为;服务端
|
|
751
|
+
// spec_version 倒退(重建)→ 一次性全量对账)
|
|
702
752
|
let serverManifest = {};
|
|
753
|
+
const manifestCache = process.env.SILLYSPEC_SPEC_SYNC_FULL ? null : readManifestCache(specRoot);
|
|
754
|
+
const cacheUsable = manifestCache && manifestCache.inc_count < resolveReconcileEvery();
|
|
703
755
|
try {
|
|
704
|
-
const res = await fetch(manifestUrl, {
|
|
756
|
+
const res = await fetch(cacheUsable ? `${manifestUrl}?since=${manifestCache.spec_version}` : manifestUrl, {
|
|
705
757
|
headers: { Authorization: `Bearer ${platform.token}` },
|
|
706
758
|
signal: withSignal(10000),
|
|
707
759
|
});
|
|
@@ -724,7 +776,41 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
724
776
|
return { synced: 0, ok: false };
|
|
725
777
|
}
|
|
726
778
|
const body = await res.json().catch(() => ({}));
|
|
727
|
-
|
|
779
|
+
if (cacheUsable && typeof body.spec_version === 'number' && body.spec_version >= manifestCache.spec_version) {
|
|
780
|
+
// 增量合并:响应行(含 exists=false 墓碑行)逐行覆盖进缓存——与拉全量等价
|
|
781
|
+
const merged = { ...manifestCache.files, ...(body.files || {}) };
|
|
782
|
+
writeManifestCache(specRoot, { spec_version: body.spec_version, files: merged, inc_count: manifestCache.inc_count + 1 });
|
|
783
|
+
serverManifest = merged;
|
|
784
|
+
} else if (cacheUsable && typeof body.spec_version === 'number') {
|
|
785
|
+
// 服务端 spec_version 倒退(重建/回滚)——缓存脱离服务端纪元,一次性全量对账。
|
|
786
|
+
// 对账失败按拉取失败 fail-closed(ok:false):空锚会触发全量 add 风暴,禁用。
|
|
787
|
+
debugLog(`[spec-sync] spec_version 倒退(${manifestCache.spec_version} → ${body.spec_version},服务端重建?)——回退全量清单对账`);
|
|
788
|
+
const full = await fetch(manifestUrl, {
|
|
789
|
+
headers: { Authorization: `Bearer ${platform.token}` },
|
|
790
|
+
signal: withSignal(10000),
|
|
791
|
+
});
|
|
792
|
+
if (!full.ok) {
|
|
793
|
+
const failCount = bumpFailStreak(specRoot, logLabel, `倒退对账全量 HTTP ${full.status}`);
|
|
794
|
+
console.warn(`[spec-sync] 倒退对账全量拉取失败 HTTP ${full.status}(文件树本次未同步,下次自动重试): ${logLabel}`);
|
|
795
|
+
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
796
|
+
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
797
|
+
}
|
|
798
|
+
return { synced: 0, ok: false };
|
|
799
|
+
}
|
|
800
|
+
const fullBody = await full.json().catch(() => ({}));
|
|
801
|
+
serverManifest = fullBody.files || {};
|
|
802
|
+
if (typeof fullBody.spec_version === 'number') writeManifestCache(specRoot, { spec_version: fullBody.spec_version, files: serverManifest, inc_count: 0 });
|
|
803
|
+
else clearManifestCache(specRoot);
|
|
804
|
+
} else if (typeof body.spec_version === 'number') {
|
|
805
|
+
// 全量响应(首拉/对账轮/强制全量):整份落缓存
|
|
806
|
+
writeManifestCache(specRoot, { spec_version: body.spec_version, files: body.files || {}, inc_count: 0 });
|
|
807
|
+
serverManifest = body.files || {};
|
|
808
|
+
} else {
|
|
809
|
+
// 旧平台(响应无 spec_version 字段):忽略 since 参数恒回全量——作废缓存,
|
|
810
|
+
// 未部署窗口内行为与现状完全一致
|
|
811
|
+
serverManifest = body.files || {};
|
|
812
|
+
clearManifestCache(specRoot);
|
|
813
|
+
}
|
|
728
814
|
} catch (err) {
|
|
729
815
|
const failCount = bumpFailStreak(specRoot, logLabel, describeSyncError(err));
|
|
730
816
|
const line = `[spec-sync] 拉取清单异常(文件树本次未同步,下次自动重试): ${logLabel}: ${describeSyncError(err)}`;
|
package/src/stages/knowledge.js
CHANGED
|
@@ -118,8 +118,19 @@ export async function cmdSearch(dir, args, opts = {}) {
|
|
|
118
118
|
vectorHint = vectorSwitchHint(dir)
|
|
119
119
|
} catch { /* 提示 fail-open */ }
|
|
120
120
|
|
|
121
|
+
// 停车场过滤披露(FR-01,2026-10-21-knowledge-tier-denoise):查询命中过 unmapped 停车场
|
|
122
|
+
// 文件但被检索核心滤除且最终零结果时,披露被滤文件——去噪不丢可见性,人工按需直查。
|
|
123
|
+
const parkingFiltered = !result.matched && Array.isArray(result.json && result.json.parkingFiltered)
|
|
124
|
+
? result.json.parkingFiltered : []
|
|
125
|
+
|
|
121
126
|
if (!result.matched) {
|
|
122
|
-
output(true, {
|
|
127
|
+
output(true, {
|
|
128
|
+
query, matches: [],
|
|
129
|
+
...(vectorHint ? { hint: vectorHint } : {}),
|
|
130
|
+
...(parkingFiltered.length > 0
|
|
131
|
+
? { parkingFiltered, parkingNote: `停车场文件已过滤(${parkingFiltered.join('、')})——按需直查文件` }
|
|
132
|
+
: {}),
|
|
133
|
+
})
|
|
123
134
|
return
|
|
124
135
|
}
|
|
125
136
|
|
|
@@ -620,11 +631,12 @@ export async function cmdKnowledge(args, dir, opts = {}) {
|
|
|
620
631
|
' refresh 重扫 scan 文档重写 generated/ 区(**写盘**——先 validate 再用)',
|
|
621
632
|
' propose / classify / inbox / graph / stats / renumber',
|
|
622
633
|
' renumber [--dry-run|--apply] 存量决策号迁移为全局号(dry-run 先行;apply 写盘幂等)',
|
|
634
|
+
' redomain-unmapped [--face decisions|fr] [--apply] unmapped 停车场存量批量清偿(文件锚/覆盖面投票目标域;dry-run 先行)',
|
|
623
635
|
'',
|
|
624
636
|
'写盘面子命令(refresh/propose/classify)先跑同名只读命令确认现状;探索用法先看本帮助。',
|
|
625
637
|
]
|
|
626
638
|
for (const l of lines) console.log(l)
|
|
627
|
-
output(true, { help: true, subcommands: ['search', 'inspect', 'validate', 'digest', 'refresh', 'propose', 'classify', 'inbox', 'graph', 'stats'] })
|
|
639
|
+
output(true, { help: true, subcommands: ['search', 'inspect', 'validate', 'digest', 'refresh', 'propose', 'classify', 'inbox', 'graph', 'stats', 'redomain-unmapped'] })
|
|
628
640
|
return
|
|
629
641
|
}
|
|
630
642
|
const subCommand = args[0] || ''
|
|
@@ -640,6 +652,16 @@ export async function cmdKnowledge(args, dir, opts = {}) {
|
|
|
640
652
|
return cmdRefresh(dir, args.slice(1), opts)
|
|
641
653
|
case 'propose':
|
|
642
654
|
return cmdPropose(dir, args.slice(1), opts)
|
|
655
|
+
case 'redomain-unmapped': {
|
|
656
|
+
// unmapped 停车场存量批量清偿(FR-04,2026-10-21-knowledge-tier-denoise):decisions 按文件锚
|
|
657
|
+
// / fr 按来源变更覆盖面投票目标域;缺省 dry-run 报告先行,--apply 幂等迁移(不裸删、冲突跳过)。
|
|
658
|
+
const mod = await tryImportSubcommandImpl('../redomain-unmapped.js', 'redomain-unmapped.js')
|
|
659
|
+
if (!mod || typeof mod.cmdRedomainUnmapped !== 'function') {
|
|
660
|
+
output(false, {}, { code: 'not_implemented', subcommand: 'redomain-unmapped' })
|
|
661
|
+
return
|
|
662
|
+
}
|
|
663
|
+
return mod.cmdRedomainUnmapped(dir, args.slice(1), opts)
|
|
664
|
+
}
|
|
643
665
|
case 'graph': {
|
|
644
666
|
// 知识图谱查询面(2026-10-08-knowledge-graph):neighbors/path/impact/orphans/dangling,
|
|
645
667
|
// --edges 边型筛选;全 JSON 出口(与 search/validate 同族)。
|
|
@@ -748,7 +770,7 @@ export async function cmdKnowledge(args, dir, opts = {}) {
|
|
|
748
770
|
output(false, {}, {
|
|
749
771
|
code: 'unknown_subcommand',
|
|
750
772
|
subCommand: subCommand,
|
|
751
|
-
available: ['search', 'inspect', 'validate', 'refresh', 'propose', 'classify', 'inbox', 'stats', 'digest', 'graph', 'renumber'], // dump 随 graph 路由面(2026-10-09-graph-dump-layout)
|
|
773
|
+
available: ['search', 'inspect', 'validate', 'refresh', 'propose', 'classify', 'inbox', 'stats', 'digest', 'graph', 'renumber', 'redomain-unmapped'], // dump 随 graph 路由面(2026-10-09-graph-dump-layout)
|
|
752
774
|
})
|
|
753
775
|
}
|
|
754
776
|
}
|
package/src/timing.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 分段计时记录器(2026-10-21-protocol-tax FR-01)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:会话考古(2026-10-20-session-page-dsh-alignment z1/z2)发现 CLI 单命令耗时无从归因
|
|
5
|
+
* ——verify --done 挂 300s 靠会话死亡才暴露,status 6.3s 靠 CPU profile 考古才定位到平台同步。
|
|
6
|
+
* 本模块给命令提供四段计时(sync / gates / render / state-write),env DEBUG_SILLYSPEC_TIMING=1
|
|
7
|
+
* 时在进程退出前输出 `[timing] sync=…ms gates=…ms render=…ms state-write=…ms`。
|
|
8
|
+
*
|
|
9
|
+
* 契约(FR-01):
|
|
10
|
+
* - 未设 env:timedSync/timedAsync 原样透传、退出钩子不输出——命令输出与现状逐字节等价。
|
|
11
|
+
* - 设 env:未触达段输出 `n/a` 不省略段名;同名段多次进入累加;走 stderr(不污染 --json
|
|
12
|
+
* 的 stdout envelope)。
|
|
13
|
+
* - 观测不改变行为:fn 抛错照抛,耗时只记到抛错前;模块注册的 exit 钩子只写一行 stderr。
|
|
14
|
+
* - 覆盖面:exit 钩子挂 process 'exit' 事件(含 process.exit 硬退出路径),无需各命令埋发射点。
|
|
15
|
+
*/
|
|
16
|
+
const SEGMENTS = ['sync', 'gates', 'render', 'state-write']
|
|
17
|
+
const enabled = process.env.DEBUG_SILLYSPEC_TIMING === '1'
|
|
18
|
+
const acc = new Map()
|
|
19
|
+
|
|
20
|
+
function record(segment, ms) {
|
|
21
|
+
acc.set(segment, (acc.get(segment) || 0) + ms)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function timedSync(segment, fn) {
|
|
25
|
+
if (!enabled) return fn()
|
|
26
|
+
const t0 = performance.now()
|
|
27
|
+
try {
|
|
28
|
+
return fn()
|
|
29
|
+
} finally {
|
|
30
|
+
record(segment, performance.now() - t0)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function timedAsync(segment, fn) {
|
|
35
|
+
if (!enabled) return fn()
|
|
36
|
+
const t0 = performance.now()
|
|
37
|
+
try {
|
|
38
|
+
return await fn()
|
|
39
|
+
} finally {
|
|
40
|
+
record(segment, performance.now() - t0)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function emitTimingIfEnabled(stream = process.stderr) {
|
|
45
|
+
if (!enabled) return
|
|
46
|
+
const parts = SEGMENTS.map((s) => {
|
|
47
|
+
const ms = acc.get(s)
|
|
48
|
+
return `${s}=${ms === undefined ? 'n/a' : `${Math.round(ms)}ms`}`
|
|
49
|
+
})
|
|
50
|
+
stream.write(`[timing] ${parts.join(' ')}\n`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (enabled) {
|
|
54
|
+
process.on('exit', () => emitTimingIfEnabled())
|
|
55
|
+
}
|