sillyspec 3.28.7 → 3.28.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.
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -0
- package/.husky/pre-push +3 -1
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
- package/package.json +1 -1
- package/src/agent-session-log.js +446 -35
- package/src/change-risk-profile.js +20 -0
- package/src/config-cat.js +6 -3
- package/src/config-schema.js +32 -0
- package/src/constants.js +3 -0
- package/src/db.js +23 -6
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +33 -3
- package/src/docs-check.js +137 -12
- package/src/docs-gate.js +108 -6
- package/src/doctor-diagnostics.js +243 -9
- package/src/friction-ledger.js +171 -0
- package/src/friction-tally.js +7 -2
- package/src/git-helper.js +6 -4
- package/src/hooks/worktree-guard.js +72 -30
- package/src/index.js +356 -81
- package/src/knowledge-classify.js +363 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/local-detect.js +30 -15
- package/src/local-register.js +13 -4
- package/src/mcp-server.js +38 -8
- package/src/progress/change-registry.js +224 -43
- package/src/progress/consistency-doctor.js +11 -4
- package/src/progress/shared.js +3 -2
- package/src/progress/stage-machine.js +3 -1
- package/src/progress/step-store.js +30 -17
- package/src/progress.js +123 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +199 -32
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +124 -15
- package/src/run/complete-handlers.js +514 -24
- package/src/run/complete.js +82 -25
- package/src/run/concurrent-detect.js +99 -1
- package/src/run/gate-snapshot.js +115 -5
- package/src/run/gates.js +85 -14
- package/src/run/prompt.js +143 -6
- package/src/run/quick-audit.js +136 -15
- package/src/run/shared.js +174 -20
- package/src/run/stage.js +6 -0
- package/src/run/verify-quality-scan.js +14 -3
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +365 -35
- package/src/spec-sync.js +19 -14
- package/src/stage-contract-spec.js +4 -3
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +29 -12
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/execute.js +91 -4
- package/src/stages/knowledge.js +52 -3
- package/src/stages/plan-postcheck.js +57 -12
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +114 -6
- package/src/sync.js +201 -11
- package/src/task-review.js +333 -27
- package/src/verify-facts-schema.js +10 -3
- package/src/verify-postcheck.js +41 -6
- package/src/verify-probes.js +348 -7
- package/src/worktree-apply.js +594 -62
- package/src/worktree-cross.js +12 -18
- package/src/worktree-deps.js +100 -2
- package/src/worktree.js +255 -30
- package/src/wt-commit.js +128 -0
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/sa-rename-probe/src/new.js +0 -4
|
@@ -840,10 +840,21 @@ export function validateTaskCommands(changeDir, projectRoot, modules = null) {
|
|
|
840
840
|
|
|
841
841
|
/**
|
|
842
842
|
* design §6 按仓分段段头识别(D-014):`## <repo> 仓变更`(如 `## sillyspec 仓变更`)。
|
|
843
|
-
* 段头为 h2,repo 名取段头首 token
|
|
843
|
+
* 段头为 h2/h3,repo 名取段头首 token(去空白/编号前缀)。容忍可选编号前缀、(…)备注后缀
|
|
844
|
+
* (`(跨仓,X1-X4)` 等,全/半角括号均可)与尾随空白/冒号——后缀不容忍时(用户实证
|
|
845
|
+
* 2026-08-29)`### sillyspec 仓变更(跨仓,X1-X4)` 整段判 main、跨仓文件全量误报未覆盖。
|
|
844
846
|
* 不命中(非仓变更段头)→ null。
|
|
845
847
|
*/
|
|
846
|
-
const REPO_SECTION_HEADER_RE = /^#{2,3}\s+(?:\d+[.)]\s*)?([A-Za-z0-9_.\-]+)\s
|
|
848
|
+
const REPO_SECTION_HEADER_RE = /^#{2,3}\s+(?:\d+[.)]\s*)?([A-Za-z0-9_.\-]+)\s*仓变更(?:\s*[((][^()()]*[))])?\s*[::]?\s*$/
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* 疑似段头宽匹配:标题行含「<token> 仓变更」字样(token 与仓变更间有空白)。
|
|
852
|
+
* 宽匹配命中而严格式(REPO_SECTION_HEADER_RE)不命中 = 段头意图明确但格式解析不了
|
|
853
|
+
* (repo-key 含中文等非法字符 / 仓变更后接自由文本)。此时该段文件会被错记到上一段仓
|
|
854
|
+
* (或 main)→ 覆盖对账整段错位,validateDesignFileCoverage 据此点名报错,
|
|
855
|
+
* 不让用户对着「未覆盖」清单反推是段头问题。
|
|
856
|
+
*/
|
|
857
|
+
const REPO_SECTION_HEADER_LOOSE_RE = /^#{2,3}\s+(?:\d+[.)]\s*)?\S+\s+仓变更/
|
|
847
858
|
|
|
848
859
|
/**
|
|
849
860
|
* design §6 文件清单章节标题(与 change-list.js FILE_LIST_SECTION_RE 同源,避免 import 私有常量)。
|
|
@@ -867,19 +878,20 @@ const FILE_LIST_SECTION_RE = /^#{2,3}\s*(?:\d+[.)]\s*)?(文件变更清单|变
|
|
|
867
878
|
* 外加 _hasSegmentHeader 标记(调用方据此决定是否走分段对账路径)
|
|
868
879
|
*
|
|
869
880
|
* @param {string} designPath - design.md 绝对路径
|
|
870
|
-
* @returns {{ byRepo: Map<string, Set<string>>, hasSegmentHeader: boolean, allFiles: string[] }}
|
|
881
|
+
* @returns {{ byRepo: Map<string, Set<string>>, hasSegmentHeader: boolean, allFiles: string[], malformedHeaders: string[] }}
|
|
871
882
|
*/
|
|
872
883
|
function parseDesignCoverageByRepo(designPath) {
|
|
873
884
|
const byRepo = new Map()
|
|
874
885
|
const allFiles = []
|
|
886
|
+
const malformedHeaders = []
|
|
875
887
|
if (!designPath || !existsSync(designPath)) {
|
|
876
|
-
return { byRepo, hasSegmentHeader: false, allFiles }
|
|
888
|
+
return { byRepo, hasSegmentHeader: false, allFiles, malformedHeaders }
|
|
877
889
|
}
|
|
878
890
|
const content = readFileSync(designPath, 'utf8')
|
|
879
891
|
|
|
880
892
|
const sectionMatch = content.match(FILE_LIST_SECTION_RE)
|
|
881
893
|
if (!sectionMatch) {
|
|
882
|
-
return { byRepo, hasSegmentHeader: false, allFiles }
|
|
894
|
+
return { byRepo, hasSegmentHeader: false, allFiles, malformedHeaders }
|
|
883
895
|
}
|
|
884
896
|
|
|
885
897
|
// 主章节起点(match index)→ 扫描到下一个非段头的 `## ` 标题或文件末尾。
|
|
@@ -891,10 +903,14 @@ function parseDesignCoverageByRepo(designPath) {
|
|
|
891
903
|
for (let i = 0; i < lines.length; i++) {
|
|
892
904
|
const line = lines[i]
|
|
893
905
|
if (i > 0 && /^##\s/.test(line) && !REPO_SECTION_HEADER_RE.test(line)) {
|
|
894
|
-
// 遇到非段头的 h2 标题(如 `## 7. 接口定义`)→
|
|
906
|
+
// 遇到非段头的 h2 标题(如 `## 7. 接口定义`)→ 主章节结束。
|
|
907
|
+
// 但若它疑似仓变更段头(宽匹配命中),先记格式错误再结束——静默截断会让
|
|
908
|
+
// 该段文件整段脱离对账基准,比报错更危险。
|
|
909
|
+
if (REPO_SECTION_HEADER_LOOSE_RE.test(line)) malformedHeaders.push(line.trim())
|
|
895
910
|
break
|
|
896
911
|
}
|
|
897
912
|
if (REPO_SECTION_HEADER_RE.test(line)) hasSegmentHeader = true
|
|
913
|
+
else if (REPO_SECTION_HEADER_LOOSE_RE.test(line)) malformedHeaders.push(line.trim())
|
|
898
914
|
sectionLines.push(line)
|
|
899
915
|
}
|
|
900
916
|
|
|
@@ -905,7 +921,7 @@ function parseDesignCoverageByRepo(designPath) {
|
|
|
905
921
|
const main = new Set(parseFileChangeList(designPath, { keepSillyspecDocs: true }))
|
|
906
922
|
if (main.size > 0) byRepo.set('main', main)
|
|
907
923
|
for (const p of main) allFiles.push(p)
|
|
908
|
-
return { byRepo, hasSegmentHeader: false, allFiles }
|
|
924
|
+
return { byRepo, hasSegmentHeader: false, allFiles, malformedHeaders }
|
|
909
925
|
}
|
|
910
926
|
|
|
911
927
|
// 有段头 → 按 repo 切片,每段构造临时 design 调 parseFileChangeList
|
|
@@ -951,7 +967,7 @@ function parseDesignCoverageByRepo(designPath) {
|
|
|
951
967
|
rmSync(tmpDir, { recursive: true, force: true })
|
|
952
968
|
}
|
|
953
969
|
|
|
954
|
-
return { byRepo, hasSegmentHeader: true, allFiles }
|
|
970
|
+
return { byRepo, hasSegmentHeader: true, allFiles, malformedHeaders }
|
|
955
971
|
}
|
|
956
972
|
|
|
957
973
|
/**
|
|
@@ -996,9 +1012,16 @@ export function validateDesignFileCoverage(changeDir) {
|
|
|
996
1012
|
}
|
|
997
1013
|
|
|
998
1014
|
// 按仓分段解析 design §6(约束③):段头 `## <repo> 仓变更` → 段内路径归该 repo;无段头 → 全 main
|
|
999
|
-
const {
|
|
1015
|
+
const {
|
|
1016
|
+
byRepo: designByRepo, hasSegmentHeader, allFiles: designFiles, malformedHeaders,
|
|
1017
|
+
} = parseDesignCoverageByRepo(designPath)
|
|
1000
1018
|
// 两种断裂文案(缺清单章节 / 文件未覆盖)从 manifest 同源(plan.design-file-coverage.data)。
|
|
1001
1019
|
const dcRule = getRule('plan.design-file-coverage')
|
|
1020
|
+
// 疑似仓变更段头但格式解析不了 → 点名报错(manifest 同源文案)。不拦的话该段文件被错记
|
|
1021
|
+
// 到别的仓,下方未覆盖对账整段错位,报错又不指向段头本身(用户实证 2026-08-29:排查靠猜)。
|
|
1022
|
+
for (const h of malformedHeaders) {
|
|
1023
|
+
errors.push(dcRule.data.messageMalformedHeader.replaceAll('${line}', h))
|
|
1024
|
+
}
|
|
1002
1025
|
if (designFiles.length === 0) {
|
|
1003
1026
|
// 走到 plan-postcheck 说明已生成 task 卡片(light/full),brainstorm 模板规定清单必填。
|
|
1004
1027
|
// 无清单 = design↔execute 偏差温床(覆盖对账无从对起),阻断,不让它静默放过。
|
|
@@ -1047,6 +1070,13 @@ export function validateDesignFileCoverage(changeDir) {
|
|
|
1047
1070
|
return { ok: errors.length === 0, errors, warnings, designFiles, uncovered }
|
|
1048
1071
|
}
|
|
1049
1072
|
|
|
1073
|
+
// acceptance 标识符 grep 停用词:工具域/格式名词——acceptance 行文高频出现但不是代码实体,
|
|
1074
|
+
// 不排除会给几乎每张卡稳定制造一条误报(宁漏不噪,漏一条提示的代价远低于满屏噪音淹没人)。
|
|
1075
|
+
const ACCEPT_IDENT_STOPWORDS = new Set([
|
|
1076
|
+
'snake_case', 'camelCase', 'kebab_case', // 格式名词(acceptance 常写「输出 snake_case 格式」)
|
|
1077
|
+
'allowed_paths', 'related_tests', 'depends_on', 'expects_from', 'title_zh', // TaskCard 字段名
|
|
1078
|
+
])
|
|
1079
|
+
|
|
1050
1080
|
/**
|
|
1051
1081
|
* 流程产物前缀(target_files 不该声明的路径域)。
|
|
1052
1082
|
* 与 worktree-apply filterDeliverableFiles 同口径硬编码——不 import 复用是因为
|
|
@@ -1189,6 +1219,16 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
|
|
|
1189
1219
|
const allTaskIds = []
|
|
1190
1220
|
const depMap = new Map()
|
|
1191
1221
|
|
|
1222
|
+
// acceptance grep 降噪依据:design.md 全文——标识符已在 design 声明 = 本次计划要新增的
|
|
1223
|
+
// 字段/函数(此刻自然不在源码里),不是噪音(用户实证 2026-08-29:15 卡 20+ 条 warning
|
|
1224
|
+
// 全是这类误报)。无 design.md(none/light)→ 空串退化为纯源码 grep。
|
|
1225
|
+
let designText = ''
|
|
1226
|
+
try {
|
|
1227
|
+
designText = readFileSync(pJoin(changeDir, 'design.md'), 'utf8')
|
|
1228
|
+
} catch {
|
|
1229
|
+
// design.md 不存在 → 跳过该降噪门,行为退化为只有源码 grep
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1192
1232
|
for (const file of taskFiles) {
|
|
1193
1233
|
const content = readFileSync(pJoin(tasksDir, file), 'utf8')
|
|
1194
1234
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
@@ -1274,8 +1314,11 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
|
|
|
1274
1314
|
|
|
1275
1315
|
// 6. acceptance best-effort 字段 grep(D-05 软约束,warning 不阻断)
|
|
1276
1316
|
// 从 acceptance 文本提取 snake_case/camelCase 标识符,grep allowed_paths 指向的源文件;
|
|
1277
|
-
// 找不到 → warning(给 LLM 审查提线索,不阻断 execute
|
|
1278
|
-
//
|
|
1317
|
+
// 找不到 → warning(给 LLM 审查提线索,不阻断 execute)。宁漏不噪,三道降噪门:
|
|
1318
|
+
// ① 停用词(格式名词/TaskCard 字段名,ACCEPT_IDENT_STOPWORDS);
|
|
1319
|
+
// ② design.md 已声明的标识符跳过——本次计划新增的字段/函数此刻不在源码里是正常的;
|
|
1320
|
+
// ③ 正则天然避开命令(无 _ 或大写)/路径(无 /)/中文;glob allowed_path / 目录 /
|
|
1321
|
+
// 不存在文件一律跳过,且至少读到一个源文件才比对(防全读不到 → 全标识符误报)。
|
|
1279
1322
|
if (projectRoot && hasAcceptance && allowedPaths.length > 0) {
|
|
1280
1323
|
let acceptanceText = ''
|
|
1281
1324
|
try {
|
|
@@ -1301,8 +1344,10 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
|
|
|
1301
1344
|
// 至少读到一个源文件才比对,避免「全没读到 → 全部标识符误报」噪声
|
|
1302
1345
|
if (readableFiles.length > 0) {
|
|
1303
1346
|
for (const ident of idents) {
|
|
1347
|
+
if (ACCEPT_IDENT_STOPWORDS.has(ident)) continue
|
|
1348
|
+
if (designText.includes(ident)) continue // design 已声明 → 本次计划新增,非噪音
|
|
1304
1349
|
if (!readableFiles.some(c => c.includes(ident))) {
|
|
1305
|
-
warnings.push(`${taskId}: acceptance 提到 ${ident} 但 allowed_paths
|
|
1350
|
+
warnings.push(`${taskId}: acceptance 提到 ${ident} 但 allowed_paths 源文件与 design.md 均未命中`)
|
|
1306
1351
|
}
|
|
1307
1352
|
}
|
|
1308
1353
|
}
|
package/src/stages/quick.js
CHANGED
|
@@ -25,7 +25,7 @@ step 3 --done 审计会把 src 核心文件(CLI / 状态机 / 注入框架等
|
|
|
25
25
|
4. 构建命令(CLI 自 local.yaml 注入,勿再读文件):
|
|
26
26
|
{LOCAL_COMMANDS}
|
|
27
27
|
5. 若有关联变更,加载每个变更的设计文档:\`cat {SPEC_ROOT}/changes/<c>/design.md 2>/dev/null\`(理解设计意图)
|
|
28
|
-
6.
|
|
28
|
+
6. 知识库:CLI 已按本会话任务描述(--input)机械匹配知识库——命中时本 prompt 末尾注入「📚 命中知识」段(top-3 文件正文截断),直接阅读即可,勿自行重跑 INDEX 匹配;段未出现(无命中或未带 --input)且确有需要时才查询:\`cat {SPEC_ROOT}/knowledge/INDEX.md 2>/dev/null\`
|
|
29
29
|
|
|
30
30
|
### 模块上下文(CLI 按任务描述匹配注入)
|
|
31
31
|
按本会话任务描述从 _module-map.yaml 匹配的模块已注入本 prompt 开头「📦 模块上下文」段(职责/核心文件/风险/最近变更)——**不要再 cat _module-map.yaml 挑卡**;开头无该段 = 无命中模块,需要跨模块全局视野时才读 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\` 自查。
|
package/src/stages/verify.js
CHANGED
|
@@ -160,6 +160,7 @@ export const definition = {
|
|
|
160
160
|
2. 如果存在:
|
|
161
161
|
- 逐个读取 tasks/task-NN.md,对照 frontmatter 的 \`acceptance:\` 列表逐条核验(TaskCard 协议的验收标准在 frontmatter YAML,正文无 checkbox)
|
|
162
162
|
- 每条 acceptance 对照实际实现/测试结果判定满足与否,未满足的项列为不通过
|
|
163
|
+
- 覆盖对账走探针 7 矩阵:verify-result.md「探针 7:验收×测试覆盖矩阵」段已由 CLI 预填归属测试文件与关键词提示(骨架未生成时先跑 \`sillyspec verify-probes --change <change-name> --init\`,幂等不覆盖已有正文);逐行填判定(covered/partial/uncovered/non-testable 四选一)与证据——covered/partial 附测试锚点(\`.test.\` 文件或 file:line),non-testable 写一句理由;判定/证据未填会被 verify \`--done\` 门禁阻断(fail-closed)
|
|
163
164
|
3. 如果不存在:跳过此步骤
|
|
164
165
|
|
|
165
166
|
### 输出
|
package/src/sync-noise.js
CHANGED
|
@@ -15,10 +15,13 @@
|
|
|
15
15
|
* - 任意一次同步成功清 marker,且此前在闸内时打一行恢复提示;
|
|
16
16
|
* - SILLYSPEC_DEBUG_SYNC=1 全程不静默。
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
* 4xx
|
|
20
|
-
* 且已有各自的冲突降噪机制(sync-conflict-banner-spam
|
|
21
|
-
*
|
|
18
|
+
* 边界:连接类闸只覆盖「连接类」失败(HTTP 404/5xx、网络错、超时/abort)。409 与其余
|
|
19
|
+
* 4xx 业务态(平台就绪但状态冲突/鉴权问题)不走连接类闸——那些是每轮都该看见的语义信号,
|
|
20
|
+
* 且已有各自的冲突降噪机制(sync-conflict-banner-spam 一族)。例外:409 code='change_deleted'
|
|
21
|
+
* 是终态幂等回执(平台侧已删该变更 key,重推永远同回执、永不自愈),走下方按变更名键控的
|
|
22
|
+
* change_deleted 闸(2026-09-14 用户反馈②):首报可见 + 跨进程窗口静默 + 推送成功清窗。
|
|
23
|
+
* 用户显式命令(platform connect 的 health ping / platform sync 手动推送)不应被静默——
|
|
24
|
+
* 调用方传 noMute 绕过。
|
|
22
25
|
*/
|
|
23
26
|
import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs'
|
|
24
27
|
import { join } from 'path'
|
|
@@ -130,13 +133,116 @@ export function syncConnectionWarn(msg) {
|
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
/**
|
|
133
|
-
* 判定 HTTP 状态码是否属连接类(可静默):404
|
|
136
|
+
* 判定 HTTP 状态码是否属连接类(可静默):404(端点未就绪/未部署)与 5xx(服务端故障)。
|
|
134
137
|
* 409 与其余 4xx 不在此列(见模块头注释边界)。
|
|
135
138
|
*/
|
|
136
139
|
export function isConnectionClassStatus(status) {
|
|
137
140
|
return status === 404 || (typeof status === 'number' && status >= 500)
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
// ── 变更级 change_deleted 回执噪音闸(2026-09-14 用户反馈②:--done 的真实报错被
|
|
144
|
+
// default 墓碑 409 回执刷屏淹没)──
|
|
145
|
+
// 与连接类闸同构但按变更名键控。409 code='change_deleted' 是终态幂等回执:平台侧已删
|
|
146
|
+
// 该变更 key 后,本地同名变更每次自动 push(每步 triggerSync/triggerStepStartSync)都收到
|
|
147
|
+
// 同样的回执——多步流程下逐条重打属零新信息刷屏;且与 base_ts 409 不同,它永远不会自愈
|
|
148
|
+
// (平台侧删除需人工介入)。语义边界(区别于连接类闸的「每轮都该看见」):
|
|
149
|
+
// - 首报完整可见(含 ⚠️ 本地仍 active 的可行动信号);
|
|
150
|
+
// - 跨进程窗口内同变更的回执静默(窗口过期后照常重报——保活信号,不静默死);
|
|
151
|
+
// - 开窗进程内不同回执行照打(诊断完整性,与连接类闸同款);
|
|
152
|
+
// - 该变更推送成功(平台侧重建/恢复)→ noteChangeDeletedResolved 清窗;
|
|
153
|
+
// - 手动 platform sync(opts.manual)与 SILLYSPEC_DEBUG_SYNC=1 旁路。
|
|
154
|
+
const DELETED_MARKER = 'sync-noise-change-deleted.json'
|
|
155
|
+
// 本进程开窗的变更名集合(区分「窗口是本进程开的」与「先前进程留的」,同 _openedByThisProcess)
|
|
156
|
+
let _openedDeletedByThisProcess = new Set()
|
|
157
|
+
// 本进程已见(打印/静默)过的 `${changeName}\u0000${msg}` 集合:同进程内逐字重复只打一次
|
|
158
|
+
const _seenDeletedInProcess = new Set()
|
|
159
|
+
|
|
160
|
+
function _readDeletedWindows() {
|
|
161
|
+
const p = _runtimeDir ? join(_runtimeDir, DELETED_MARKER) : null
|
|
162
|
+
if (!p) return null
|
|
163
|
+
try {
|
|
164
|
+
const s = JSON.parse(readFileSync(p, 'utf8'))
|
|
165
|
+
return s && typeof s === 'object' && !Array.isArray(s) ? s : null
|
|
166
|
+
} catch { return null }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function _writeDeletedWindows(map) {
|
|
170
|
+
const p = _runtimeDir ? join(_runtimeDir, DELETED_MARKER) : null
|
|
171
|
+
if (!p) return
|
|
172
|
+
try {
|
|
173
|
+
mkdirSync(_runtimeDir, { recursive: true })
|
|
174
|
+
const now = Date.now()
|
|
175
|
+
const pruned = {}
|
|
176
|
+
for (const [k, v] of Object.entries(map)) if (typeof v === 'number' && v > now) pruned[k] = v
|
|
177
|
+
if (Object.keys(pruned).length === 0) {
|
|
178
|
+
try { unlinkSync(p) } catch { /* 无文件/占用:留空 map 文件也等效 */ }
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
writeFileSync(p, JSON.stringify(pruned) + '\n', 'utf8')
|
|
182
|
+
} catch { /* marker 写失败 = 闸门失效退化为直通,仅损失降噪 */ }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function _deletedLineVisible(changeName, msg, noMute) {
|
|
186
|
+
if (!changeName || noMute || process.env.SILLYSPEC_DEBUG_SYNC) return true
|
|
187
|
+
const key = changeName + '\u0000' + msg
|
|
188
|
+
if (_seenDeletedInProcess.has(key)) return false
|
|
189
|
+
const now = Date.now()
|
|
190
|
+
const windows = _readDeletedWindows()
|
|
191
|
+
const until = windows ? windows[changeName] : undefined
|
|
192
|
+
if (typeof until === 'number' && now < until) {
|
|
193
|
+
_seenDeletedInProcess.add(key)
|
|
194
|
+
// 开窗进程内不同回执行可见(诊断完整性),先前进程留的窗口静默
|
|
195
|
+
return _openedDeletedByThisProcess.has(changeName)
|
|
196
|
+
}
|
|
197
|
+
_seenDeletedInProcess.add(key)
|
|
198
|
+
const next = windows || {}
|
|
199
|
+
next[changeName] = now + MUTE_WINDOW_MS
|
|
200
|
+
_writeDeletedWindows(next)
|
|
201
|
+
_openedDeletedByThisProcess.add(changeName)
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* change_deleted 回执行(info 口吻:本地已注销的墓碑上行 409 属预期回执)。
|
|
207
|
+
* @returns {boolean} true=已输出
|
|
208
|
+
*/
|
|
209
|
+
export function syncChangeDeletedLog(changeName, msg, { noMute = false } = {}) {
|
|
210
|
+
if (_deletedLineVisible(changeName, msg, noMute)) {
|
|
211
|
+
console.log(msg)
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* change_deleted 回执行(warn 口吻:本地仍 active,上行被拒收的可行动信号)。
|
|
219
|
+
* @returns {boolean} true=已输出
|
|
220
|
+
*/
|
|
221
|
+
export function syncChangeDeletedWarn(changeName, msg, { noMute = false } = {}) {
|
|
222
|
+
if (_deletedLineVisible(changeName, msg, noMute)) {
|
|
223
|
+
console.warn(msg)
|
|
224
|
+
return true
|
|
225
|
+
}
|
|
226
|
+
return false
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 该变更推送成功(平台侧未删/已重建):清其回执噪音窗与进程内状态。
|
|
231
|
+
* 与 noteSyncSuccess 的「任意成功清连接闸」不同——本闸按变更名精清(他变更的窗口不受影响)。
|
|
232
|
+
*/
|
|
233
|
+
export function noteChangeDeletedResolved(changeName) {
|
|
234
|
+
if (!changeName) return
|
|
235
|
+
_openedDeletedByThisProcess.delete(changeName)
|
|
236
|
+
for (const k of _seenDeletedInProcess) {
|
|
237
|
+
if (k.startsWith(changeName + '\u0000')) _seenDeletedInProcess.delete(k)
|
|
238
|
+
}
|
|
239
|
+
const windows = _readDeletedWindows()
|
|
240
|
+
if (windows && windows[changeName] !== undefined) {
|
|
241
|
+
delete windows[changeName]
|
|
242
|
+
_writeDeletedWindows(windows)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
140
246
|
/**
|
|
141
247
|
* 任意一次同步成功:清 marker。此前处于闸内(marker 存在)时打一行恢复提示——
|
|
142
248
|
* 平台恢复的第一个信号应该可见,之后的成功照常静默(成功不打扰)。
|
|
@@ -150,9 +256,11 @@ export function noteSyncSuccess() {
|
|
|
150
256
|
console.log('[sync] 平台连接已恢复(此前静默的同步失败已停止)')
|
|
151
257
|
}
|
|
152
258
|
|
|
153
|
-
/**
|
|
259
|
+
/** 测试复位:清进程内绑定与开窗状态(连接类闸 + change_deleted 变更级闸)。 */
|
|
154
260
|
export function _resetSyncNoiseForTest() {
|
|
155
261
|
_runtimeDir = null
|
|
156
262
|
_openedByThisProcess = false
|
|
157
263
|
_seenInProcess.clear()
|
|
264
|
+
_openedDeletedByThisProcess = new Set()
|
|
265
|
+
_seenDeletedInProcess.clear()
|
|
158
266
|
}
|