sillyspec 3.28.3 → 3.28.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-auto/SKILL.md +54 -54
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
- package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
- package/.claude/skills/sillyspec-state/SKILL.md +25 -25
- package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
- package/AGENTS.md +24 -0
- package/INSTRUCTIONS.md +12 -0
- package/package.json +1 -1
- package/src/archive-delta.js +82 -0
- package/src/change-delete.js +2 -1
- package/src/change-list.js +7 -2
- package/src/change-risk-profile.js +77 -6
- package/src/config-cat.js +12 -1
- package/src/constants.js +3 -1
- package/src/docs-check.js +34 -0
- package/src/doctor-diagnostics.js +441 -5
- package/src/foreign-declared.js +53 -2
- package/src/index.js +181 -6
- package/src/module-impact.js +77 -12
- package/src/plan-adopt-waves.js +13 -5
- package/src/progress/change-registry.js +16 -0
- package/src/progress.js +62 -1
- package/src/quick-recommend.js +6 -1
- package/src/quicklog.js +10 -2
- package/src/run/command.js +30 -9
- package/src/run/complete-handlers.js +152 -6
- package/src/run/complete.js +45 -0
- package/src/run/gates.js +94 -6
- package/src/run/prompt.js +96 -5
- package/src/run/shared.js +74 -6
- package/src/run/stage.js +35 -0
- package/src/runtime-hygiene.js +84 -0
- package/src/spec-sync.js +63 -8
- package/src/stage-contract.js +83 -3
- package/src/stage-review.js +3 -0
- package/src/stages/archive.js +4 -4
- package/src/stages/brainstorm.js +2 -8
- package/src/stages/doctor.js +41 -515
- package/src/stages/plan-postcheck.js +191 -7
- package/src/stages/plan.js +5 -26
- package/src/stages/quick.js +11 -11
- package/src/stages/verify.js +10 -6
- package/src/sync-noise.js +158 -0
- package/src/sync.js +42 -17
- package/src/task-review.js +25 -1
- package/src/verify-facts-schema.js +141 -0
- package/src/verify-postcheck.js +257 -19
- package/src/verify-probes.js +142 -13
- package/src/workflow.js +6 -0
- package/src/worktree-apply.js +51 -13
- package/src/worktree.js +5 -1
|
@@ -1379,6 +1379,148 @@ export function validatePlanArtifacts(changeDir) {
|
|
|
1379
1379
|
// 执行入口(从 run.js 迁移)
|
|
1380
1380
|
// ═══════════════════════════════════════════════════════════════
|
|
1381
1381
|
|
|
1382
|
+
/**
|
|
1383
|
+
* plan --done 自动生成 module-impact.md 首版(刀②,2026-09-08 轮次经济学)。
|
|
1384
|
+
*
|
|
1385
|
+
* 文件×模块归属是机械分类(module-map 前缀匹配),此前 plan 审查步 prompt 让 agent 手写
|
|
1386
|
+
* 首版——章节标题写变体会被 archive contains_sections 硬拦返工(prompt 自认「agent 只能从
|
|
1387
|
+
* gate 报错反推格式」)。改为 --done 时 CLI 用 design 文件变更清单(main 段,代码未写、
|
|
1388
|
+
* 无 diff 可取)生成骨架,影响类型列留 <!--TODO--> 由 execute/verify 按实际 diff 回填。
|
|
1389
|
+
*
|
|
1390
|
+
* 边界:已存在不覆盖;scale=small 豁免(与 stage-contract plan.module-impact.exists 的
|
|
1391
|
+
* condition 同判法);生成失败(无 module-map / 清单为空)不阻断——与旧手写路径行为一致,
|
|
1392
|
+
* 由 check 1e 的 validatePlanOutputs 报缺失,agent 按 gate 提示兜底手写。
|
|
1393
|
+
*
|
|
1394
|
+
* @param {string} changeDir - 变更目录
|
|
1395
|
+
* @param {string} cwd - 仓库根
|
|
1396
|
+
* @param {string|null} [specRoot] - 规范根(平台模式显式传入)
|
|
1397
|
+
* @returns {Promise<{status: 'generated'|'skipped', reason?: string, matchedCount?: number, unmatchedCount?: number}>}
|
|
1398
|
+
*/
|
|
1399
|
+
export async function generatePlanModuleImpactFirstVersion(changeDir, cwd, specRoot = null) {
|
|
1400
|
+
const designPath = pJoin(changeDir, 'design.md')
|
|
1401
|
+
const impactPath = pJoin(changeDir, 'module-impact.md')
|
|
1402
|
+
if (!existsSync(designPath)) return { status: 'skipped', reason: 'no-design' }
|
|
1403
|
+
if (existsSync(impactPath)) return { status: 'skipped', reason: 'exists' }
|
|
1404
|
+
|
|
1405
|
+
const scale = parseFrontmatterScalar(readFileSync(designPath, 'utf8'), 'scale')
|
|
1406
|
+
if (scale === 'small') return { status: 'skipped', reason: 'scale-small' }
|
|
1407
|
+
|
|
1408
|
+
// 声明来源:design 文件变更清单 main 段(跨仓其他段的路径相对各仓根,不入主仓 module-map 归类)
|
|
1409
|
+
const { byRepo } = parseDesignCoverageByRepo(designPath)
|
|
1410
|
+
const mainFiles = [...(byRepo.get('main') || [])]
|
|
1411
|
+
if (mainFiles.length === 0) return { status: 'skipped', reason: 'no-declared-files' }
|
|
1412
|
+
|
|
1413
|
+
const { generateModuleImpactSkeleton } = await import('../module-impact.js')
|
|
1414
|
+
const result = generateModuleImpactSkeleton({
|
|
1415
|
+
cwd,
|
|
1416
|
+
changeName: basename(changeDir),
|
|
1417
|
+
specDir: specRoot,
|
|
1418
|
+
sourceFiles: mainFiles,
|
|
1419
|
+
origin: 'plan --done CLI(design 声明清单 × module-map 前缀匹配)',
|
|
1420
|
+
})
|
|
1421
|
+
if (!result) return { status: 'skipped', reason: 'no-module-map' }
|
|
1422
|
+
|
|
1423
|
+
writeFileSync(impactPath, result.markdown)
|
|
1424
|
+
return { status: 'generated', matchedCount: result.matchedCount, unmatchedCount: result.unmatchedCount }
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* Wave 提案验证(2026-09-09-plan-derived FR-01,提案-验证-落盘三段式的验证段):
|
|
1429
|
+
* ①蓝图一致性(validateBlueprintConsistency 经 opts.waves 注入提案分组——读侧注入不改 plan.md)
|
|
1430
|
+
* ②逐 Wave allowed_paths 两两交集(topo 只看依赖看不见文件重叠;合并手工安全串行可能收敛出
|
|
1431
|
+
* 同 Wave 共享路径冲突——提案必须比它要替换的违规布局更干净才可落盘)。
|
|
1432
|
+
* @returns {string[]} 冲突明细(空数组 = 提案干净可写)
|
|
1433
|
+
*/
|
|
1434
|
+
function validateWaveProposal(changeDir, waves, repoRegistry = null) {
|
|
1435
|
+
const conflicts = []
|
|
1436
|
+
try {
|
|
1437
|
+
const consistency = validateBlueprintConsistency(changeDir, { repoRegistry, waves })
|
|
1438
|
+
for (const e of consistency.errors || []) conflicts.push(`蓝图一致性:${e}`)
|
|
1439
|
+
} catch (e) {
|
|
1440
|
+
conflicts.push(`蓝图一致性复跑异常:${e?.message || e}`)
|
|
1441
|
+
}
|
|
1442
|
+
// 逐 Wave allowed_paths 两两交集
|
|
1443
|
+
try {
|
|
1444
|
+
const tasksDir = pJoin(changeDir, 'tasks')
|
|
1445
|
+
if (existsSync(tasksDir)) {
|
|
1446
|
+
for (const taskFiles of (waves || [])) {
|
|
1447
|
+
const pathsPerTask = []
|
|
1448
|
+
for (const tf of taskFiles) {
|
|
1449
|
+
const card = readFileSync(pJoin(tasksDir, `${tf}.md`), 'utf8')
|
|
1450
|
+
pathsPerTask.push(new Set(parseAllowedPaths(card)))
|
|
1451
|
+
}
|
|
1452
|
+
for (let i = 0; i < pathsPerTask.length; i++) {
|
|
1453
|
+
for (let j = i + 1; j < pathsPerTask.length; j++) {
|
|
1454
|
+
const overlap = [...pathsPerTask[i]].filter(x => pathsPerTask[j].has(x))
|
|
1455
|
+
if (overlap.length > 0) conflicts.push(`同 Wave ${taskFiles[i]} × ${taskFiles[j]} 共享路径:${overlap.slice(0, 3).join('、')}`)
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
} catch { /* 卡片读取失败按无重叠证据 → 冲突留一致性段兜底 */ }
|
|
1461
|
+
return conflicts
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/** plan_level 客观复核阈值单点(2026-09-09-plan-derived FR-03,D-003@v1) */
|
|
1465
|
+
const PLAN_LEVEL_SIGNALS = { files: 8, modules: 2 }
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* plan_level 第二把尺子(不接管,仅 warning):design 文件清单数 / 模块跨度 / task 数
|
|
1469
|
+
* 与声明档位比对。fail-open(design/map 读取失败 → null 零输出)。
|
|
1470
|
+
* @returns {string|null} warning 文案
|
|
1471
|
+
*/
|
|
1472
|
+
async function reviewPlanLevelSignal(changeDir, planLevel) {
|
|
1473
|
+
try {
|
|
1474
|
+
const designPath = pJoin(changeDir, 'design.md')
|
|
1475
|
+
if (!existsSync(designPath) || !planLevel) return null
|
|
1476
|
+
const { byRepo } = parseDesignCoverageByRepo(designPath)
|
|
1477
|
+
const fileList = [...(byRepo.get('main') || [])]
|
|
1478
|
+
// 模块跨度:module-map 前缀命中去重(未命中文件不计——防新模块假信号)。
|
|
1479
|
+
// map 来源 = parseModuleMapPaths(module-impact.js 同款扫描 docs/<p>/modules/_module-map.yaml,
|
|
1480
|
+
// 免 project 名——loadModuleMap(specRoot, null) 会因第二参 null 短路恒返 null,勿用)。
|
|
1481
|
+
let moduleIds = new Set()
|
|
1482
|
+
try {
|
|
1483
|
+
const { parseModuleMapPaths } = await import('../module-impact.js')
|
|
1484
|
+
const specRoot = pJoin(changeDir, '..', '..')
|
|
1485
|
+
let mapText = null
|
|
1486
|
+
try {
|
|
1487
|
+
for (const d of readdirSync(pJoin(specRoot, 'docs'), { withFileTypes: true })) {
|
|
1488
|
+
if (!d.isDirectory()) continue
|
|
1489
|
+
const cand = pJoin(specRoot, 'docs', d.name, 'modules', '_module-map.yaml')
|
|
1490
|
+
if (existsSync(cand)) { mapText = readFileSync(cand, 'utf8'); break }
|
|
1491
|
+
}
|
|
1492
|
+
} catch { /* 无 docs/ → 只用文件数信号 */ }
|
|
1493
|
+
if (mapText) {
|
|
1494
|
+
const prefixMap = parseModuleMapPaths(mapText)
|
|
1495
|
+
for (const f of fileList) {
|
|
1496
|
+
const hit = deriveModuleIdFor(f, prefixMap)
|
|
1497
|
+
if (hit) moduleIds.add(hit)
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
} catch { /* map 不可得 → 只用文件数信号 */ }
|
|
1501
|
+
const tasksDir = pJoin(changeDir, 'tasks')
|
|
1502
|
+
const taskCount = existsSync(tasksDir) ? readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f)).length : 0
|
|
1503
|
+
const big = fileList.length > PLAN_LEVEL_SIGNALS.files || moduleIds.size > PLAN_LEVEL_SIGNALS.modules
|
|
1504
|
+
const small = fileList.length <= 2 && moduleIds.size <= 1
|
|
1505
|
+
if (planLevel !== 'full' && big) {
|
|
1506
|
+
return `plan_level 声明 ${planLevel} 但客观规模信号偏大(design 清单 ${fileList.length} 文件 / ${moduleIds.size} 模块 / ${taskCount} task)——若确属轻量请在 plan.md 附一行理由,否则建议 full(tier=independent 独立审查)`
|
|
1507
|
+
}
|
|
1508
|
+
if (planLevel === 'full' && small && taskCount <= 2) {
|
|
1509
|
+
return `plan_level 声明 full 但信号偏小(${fileList.length} 文件 / ${moduleIds.size} 模块)——可考虑降档 light/none 省审查仪式(advisory)`
|
|
1510
|
+
}
|
|
1511
|
+
return null
|
|
1512
|
+
} catch { return null }
|
|
1513
|
+
}
|
|
1514
|
+
function deriveModuleIdFor(filePath, mapYaml) {
|
|
1515
|
+
const posix = String(filePath).replace(/\\/g, '/')
|
|
1516
|
+
for (const [id, paths] of mapYaml) {
|
|
1517
|
+
for (const p of paths) {
|
|
1518
|
+
if (p.endsWith('/') ? posix.startsWith(p) : (posix === p || posix.startsWith(p + '/'))) return id
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return null
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1382
1524
|
/**
|
|
1383
1525
|
* Plan postcheck 主函数:Wave 重排 + 一致性校验 + 产物确认
|
|
1384
1526
|
*
|
|
@@ -1490,6 +1632,14 @@ export async function executePlanPostcheck(context) {
|
|
|
1490
1632
|
console.log(` ✅ design.md ${coverage.designFiles.length} 个文件全部被 task allowed_paths 覆盖`)
|
|
1491
1633
|
}
|
|
1492
1634
|
|
|
1635
|
+
// ── 1e-0. module-impact.md 首版 CLI 自动生成(刀②)──
|
|
1636
|
+
// 在 1e 契约校验(plan.module-impact.exists)之前生成,使 scale≠small 的变更
|
|
1637
|
+
// 天然通过;生成失败不报错(缺失由 1e 统一报,口径不变)。
|
|
1638
|
+
const miGen = await generatePlanModuleImpactFirstVersion(changeDir, cwd, specRoot)
|
|
1639
|
+
if (miGen.status === 'generated') {
|
|
1640
|
+
console.log(` ✅ module-impact.md 首版已由 CLI 自动生成(归类 ${miGen.matchedCount} 个文件,未匹配 ${miGen.unmatchedCount} 个;影响类型列由 execute/verify 回填)`)
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1493
1643
|
// ── 1e. 阶段完成产物校验(plan stage contract)──
|
|
1494
1644
|
// plan 完成 gate 还会跑 stage-contract.validatePlanOutputs;为免「postcheck 通过 →
|
|
1495
1645
|
// stage gate 又报 module-impact 缺失/entry-point-wiring 未覆盖」的修一层撞一层,
|
|
@@ -1526,6 +1676,16 @@ export async function executePlanPostcheck(context) {
|
|
|
1526
1676
|
}
|
|
1527
1677
|
printSectionWarnings('target_files 声明', targetFiles.warnings)
|
|
1528
1678
|
|
|
1679
|
+
// ── 1g. plan_level 客观复核(第二把尺子,warning 不阻断——2026-09-09-plan-derived FR-03)──
|
|
1680
|
+
try {
|
|
1681
|
+
const planFm = readFileSync(pJoin(changeDir, 'plan.md'), 'utf8').match(/^plan_level:\s*(\w+)/m)
|
|
1682
|
+
if (planFm) {
|
|
1683
|
+
const levelWarn = await reviewPlanLevelSignal(changeDir, planFm[1])
|
|
1684
|
+
if (levelWarn) console.warn(`
|
|
1685
|
+
⚠️ ${levelWarn}`)
|
|
1686
|
+
}
|
|
1687
|
+
} catch { /* fail-open */ }
|
|
1688
|
+
|
|
1529
1689
|
// ── 聚合输出:一轮 --done 暴露全部失败项(坑6③)──
|
|
1530
1690
|
if (failures.length > 0) {
|
|
1531
1691
|
console.error(`\n❌ plan postcheck 失败(${failures.length} 类问题,已全部列出——一次修复后重跑,无需逐个迭代):`)
|
|
@@ -1607,10 +1767,35 @@ export async function executePlanPostcheck(context) {
|
|
|
1607
1767
|
}
|
|
1608
1768
|
}
|
|
1609
1769
|
if (directionViolations.length > 0) {
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1770
|
+
// ── 2026-09-09-plan-derived FR-01:违规自动修复(提案-验证-落盘三段式)──
|
|
1771
|
+
// 拓扑布局方向必然合法 → 产提案(不落盘)→ 双验证(蓝图一致性含 repoRegistry +
|
|
1772
|
+
// 逐 Wave allowed_paths 两两交集——topo 只看依赖看不见文件重叠)→ 干净才写。
|
|
1773
|
+
// 不干净保留手排原文照旧 throw(真冲突需人裁决:拆 Wave 或补 depends_on)。
|
|
1774
|
+
// 提案任何异常回落原 throw 路径(零新阻断面)。
|
|
1775
|
+
try {
|
|
1776
|
+
const { adoptPlanWaves } = await import('../plan-adopt-waves.js')
|
|
1777
|
+
const proposal = adoptPlanWaves({ changeDir, mode: 'proposal' })
|
|
1778
|
+
const conflicts = validateWaveProposal(changeDir, proposal.waves, repoRegistry)
|
|
1779
|
+
if (proposal.ok && proposal.rewritten && conflicts.length === 0) {
|
|
1780
|
+
writeFileSync(planPath, proposal.planMdDraft, 'utf8')
|
|
1781
|
+
console.log(`\n 🔧 Wave 依赖方向违规已按 depends_on 拓扑自动修复(${directionViolations.length} 处违规 → ${proposal.waves.length} 段重排,W 列同步 ${proposal.tableRowsUpdated} 行):`)
|
|
1782
|
+
for (const v of directionViolations) console.log(` · ${v}`)
|
|
1783
|
+
console.log(' 提案验证通过(蓝图一致性 + 同 Wave 文件面无交集);原文未被采纳的手工分组已按拓扑归一')
|
|
1784
|
+
} else {
|
|
1785
|
+
console.error(`\n❌ Wave 依赖方向违规(${directionViolations.length} 处),拓扑提案不可自动采纳(${!proposal.ok ? proposal.error : conflicts.length > 0 ? '同 Wave 文件面重叠' : '提案与现状等价'}):`)
|
|
1786
|
+
for (const v of directionViolations) console.error(` ${v}`)
|
|
1787
|
+
if (conflicts.length > 0) for (const c of conflicts) console.error(` · 提案冲突:${c}`)
|
|
1788
|
+
console.error(' 解法:把共享文件的任务拆到不同 Wave,或补 depends_on 使拓扑合法;sillyspec plan-adopt-waves 可预览拓扑分组')
|
|
1789
|
+
throw new Error('planPostcheck: Wave 依赖方向违规(depends_on 同 Wave / 后置 Wave)——' + directionViolations.join(';'))
|
|
1790
|
+
}
|
|
1791
|
+
} catch (e) {
|
|
1792
|
+
if (e && e.message && e.message.startsWith('planPostcheck:')) throw e
|
|
1793
|
+
// 提案链路异常 → 回落原硬拦(零新阻断面)
|
|
1794
|
+
console.error(`\n❌ Wave 依赖方向违规(${directionViolations.length} 处,自动修复提案链路异常:${e?.message || e}):`)
|
|
1795
|
+
for (const v of directionViolations) console.error(` ${v}`)
|
|
1796
|
+
console.error(' 解法:sillyspec plan-adopt-waves --change <变更名> 一键按 depends_on 拓扑重排 plan.md Wave 段,或手动调整分组')
|
|
1797
|
+
throw new Error('planPostcheck: Wave 依赖方向违规(depends_on 同 Wave / 后置 Wave)——' + directionViolations.join(';'))
|
|
1798
|
+
}
|
|
1614
1799
|
}
|
|
1615
1800
|
|
|
1616
1801
|
const sameStructure = waves.length === existingWaves.length &&
|
|
@@ -1622,10 +1807,9 @@ export async function executePlanPostcheck(context) {
|
|
|
1622
1807
|
|
|
1623
1808
|
if (sameStructure) {
|
|
1624
1809
|
console.log(' ✅ Wave 分组与拓扑排序一致,无需更新 plan.md')
|
|
1625
|
-
} else {
|
|
1626
|
-
console.log(' ⚠️ Wave 分组与拓扑排序不一致(依赖方向已校验合法——手工比拓扑更细的串行化是安全做法,可保持现状)')
|
|
1627
|
-
console.log(' 标准分组见上方 📊;如需对齐:sillyspec plan-adopt-waves --change <变更名>(重排 plan.md Wave 段并同步任务总表 W 列)')
|
|
1628
1810
|
}
|
|
1811
|
+
// 结构不一致但方向合法(手工保守串行)→ 静默放行(2026-09-09-plan-derived:
|
|
1812
|
+
// 合法安全模式消每轮提示噪音;手动对齐出口保留 sillyspec plan-adopt-waves)
|
|
1629
1813
|
}
|
|
1630
1814
|
}
|
|
1631
1815
|
}
|
package/src/stages/plan.js
CHANGED
|
@@ -340,6 +340,7 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
|
|
|
340
340
|
- [ ] 不存在 P0/P1 unresolved blocker 残留
|
|
341
341
|
- [ ] 没有实现细节泄漏到 plan.md(接口签名/代码示例应在 tasks/task-NN.md)
|
|
342
342
|
- [ ] 关键路径与 Wave 依赖合理(无循环依赖、无遗漏前置)
|
|
343
|
+
- [ ] Wave 分组说明:依赖方向违规(depends_on 同 Wave / 后置 Wave)在 --done 时会按拓扑**自动修复**(提案经一致性+文件面验证后落盘);共享文件的手工分 Wave 串行是合法安全模式,不会被自动改写
|
|
343
344
|
- [ ] 连带测试归属:本批改动是否会导致既有测试断言失效(改共享/被多 task 依赖源文件、改被测试精确匹配的值如 UI 文案/按钮文本/错误信息/常量/枚举字面量、改函数签名或返回结构等单文件场景)?此类 task 是否在 related_tests 声明了失效测试、且路径在 allowed_paths 内(或由独立测试 task 覆盖)?(漏声明 = execute 阶段测试债、主代理事后兜底)
|
|
344
345
|
- [ ] acceptance 字段对照实际 schema/类型源文件核验存在性与形态,不凭 design.md 文字臆断(plan-postcheck best-effort grep 会给 allowed_paths 源文件未命中的 snake_case/camelCase 标识符提 warning,此处是语义层复查;臆断 = execute 阶段返工)
|
|
345
346
|
|
|
@@ -351,37 +352,15 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
|
|
|
351
352
|
{REVIEW_JSON_CONTRACT}
|
|
352
353
|
4. verdict=fail 时在 reviewerNotes 写明阻断项
|
|
353
354
|
|
|
354
|
-
###
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
输入(此时 TaskCard/allowed_paths 尚未生成——在下一步「生成 TaskCard」才产出,故用以下两项作输入,粒度与 archive 现状一致):
|
|
358
|
-
- design.md 的「文件变更清单」(本次计划改哪些源码文件)
|
|
359
|
-
- plan.md 的任务列表(每个 task 改的范围)
|
|
360
|
-
|
|
361
|
-
步骤(module-impact 的生成口径与 archive 终审一致):
|
|
362
|
-
1. 读 {SPEC_ROOT}/docs/<project>/modules/_module-map.yaml(模块→文件路径映射)。**不存在 → 降级**:生成只含 unmapped 部分的 module-impact.md + 提示「建议运行 scan 生成模块映射」,不阻断
|
|
363
|
-
2. 对照 design 文件变更清单 + plan 任务列表,逐文件匹配所属模块(命中的归 mapped,未命中的归「未匹配文件」章节)
|
|
364
|
-
3. 落盘 {SPEC_ROOT}/changes/<change>/module-impact.md,**两个章节标题逐字固定**(archive-impact.yaml contains_sections 机械校验,写成「影响矩阵」等变体会被 archive 硬拦返工):
|
|
365
|
-
- 「## 模块影响矩阵」:模块 × 影响类型[新增/修改/删除/依赖变更] × 说明
|
|
366
|
-
- 「## 未匹配文件」:unmatched 文件 × 处置说明(无未匹配文件也保留空章节,写「无」)
|
|
367
|
-
4. 首行标题必须用中文:# 模块影响分析(Module Impact)— <变更简述>
|
|
368
|
-
5. **必须含「更新结果」表骨架**(本表是 verify/archive 死信门控的收口目标——CLI 硬校验表内无 pending/待办行,漏写此表则 agent 只能从 gate 报错反推格式):
|
|
369
|
-
\`\`\`markdown
|
|
370
|
-
## 更新结果
|
|
371
|
-
|
|
372
|
-
| 目标 | 操作 | 状态 |
|
|
373
|
-
|------|------|------|
|
|
374
|
-
| \`modules/<id>.md\` | 更新<模块名>模块卡(本次变更涉及) | pending |
|
|
375
|
-
| \`_module-map.yaml\` | 无变化(未增删模块) | skipped |
|
|
376
|
-
\`\`\`
|
|
377
|
-
规则:每个受影响模块文档一行,状态列初始化 pending;无变化的行直接写 skipped。execute/verify 完成文档同步后把对应行回填 done;确定不同步的行改 skipped 并在操作列写明原因。**末列是状态列**(done/skipped/pending),不要在表尾追加其他列。
|
|
355
|
+
### module-impact.md 首版(scale≠small 时:CLI 自动生成,审查步勿手写)
|
|
356
|
+
module-impact.md 首版**由 CLI 在本阶段 --done 时自动生成**——文件×模块归属按 _module-map.yaml 前缀匹配机械预填,章节含「## 模块影响矩阵」「## 未匹配文件」「## 更新结果」表骨架(每受影响模块一行 pending),影响类型列留 <!--TODO--> 由 execute/verify 按实际 diff 回填。已存在不覆盖。手写整份首版是历史返工根源(章节标题变体会被 archive contains_sections 硬拦),**本步不要手写**。
|
|
357
|
+
仅当 --done 报 module-impact 缺失时(如无 module-map 的绿地项目生成降级失败),按 gate 报错提示手写兜底:两章节标题逐字用「## 模块影响矩阵」「## 未匹配文件」(未命中的文件归「未匹配文件」章节),并含「## 更新结果」表骨架(行:\`modules/<id>.md\`、\`_module-map.yaml\`,状态列 pending/skipped,末列是状态列,勿追加其他列)。
|
|
378
358
|
|
|
379
359
|
execute/verify 阶段会按实际代码变更更新此文档;archive 阶段会最终确认它。
|
|
380
360
|
|
|
381
361
|
### 输出
|
|
382
362
|
- tier=self:审查清单结果(每条状态 + 偏差说明)
|
|
383
|
-
- tier=independent:子代理产出的 review.json 路径 + verdict
|
|
384
|
-
- scale≠small:附 module-impact.md 路径 + 影响摘要`,
|
|
363
|
+
- tier=independent:子代理产出的 review.json 路径 + verdict 摘要`,
|
|
385
364
|
outputHint: 'plan 审查结果(self=清单 / independent=review.json 路径+verdict)',
|
|
386
365
|
optional: false
|
|
387
366
|
}
|
package/src/stages/quick.js
CHANGED
|
@@ -20,18 +20,18 @@ step 3 --done 审计会把 src 核心文件(CLI / 状态机 / 注入框架等
|
|
|
20
20
|
|
|
21
21
|
### 操作
|
|
22
22
|
1. 检查关联变更(\`<linked-changes>\`,逗号分隔的变更名列表;显示「(无)」= 不关联变更),确定记录方式
|
|
23
|
-
2.
|
|
24
|
-
3.
|
|
25
|
-
4.
|
|
26
|
-
5. 构建命令(CLI 自 local.yaml 注入,勿再读文件):
|
|
23
|
+
2. 理解任务:模糊则问一个问题确认(项目/约定/模块上下文已由 CLI 注入下方与开头,勿再 cat)
|
|
24
|
+
3. 阅读「项目上下文注入」段(CLI 已代读 projects/*.yaml 摘要 + CONVENTIONS.md 开头)——注入标注截断且确需全文时才读原文件
|
|
25
|
+
4. 构建命令(CLI 自 local.yaml 注入,勿再读文件):
|
|
27
26
|
{LOCAL_COMMANDS}
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
5. 若有关联变更,加载每个变更的设计文档:\`cat {SPEC_ROOT}/changes/<c>/design.md 2>/dev/null\`(理解设计意图)
|
|
28
|
+
6. 如有需要,查询知识库:\`cat {SPEC_ROOT}/knowledge/INDEX.md 2>/dev/null\`
|
|
30
29
|
|
|
31
|
-
###
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
### 模块上下文(CLI 按任务描述匹配注入)
|
|
31
|
+
按本会话任务描述从 _module-map.yaml 匹配的模块已注入本 prompt 开头「📦 模块上下文」段(职责/核心文件/风险/最近变更)——**不要再 cat _module-map.yaml 挑卡**;开头无该段 = 无命中模块,需要跨模块全局视野时才读 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\` 自查。
|
|
32
|
+
|
|
33
|
+
### 项目上下文注入(CLI 代读,刀①)
|
|
34
|
+
{QUICK_CONTEXT_DIGEST}
|
|
35
35
|
|
|
36
36
|
### QUICKLOG 记录(CLI 已接管,无需你手动创建)
|
|
37
37
|
本 quick 会话的 ql-ID 由 CLI 在启动时分配,已注入为 \`<quicklog-id>\`。CLI 同时已自动:
|
|
@@ -101,7 +101,7 @@ sillyspec run quick --done --change <id> --req "一句话语义化短标题(
|
|
|
101
101
|
结果:验证情况(测试数 / lint / typecheck / 部署状态)
|
|
102
102
|
\`\`\`
|
|
103
103
|
|
|
104
|
-
⚠️ **「需求:」(= --req)就是 QUICKLOG 条目标题**——CLI
|
|
104
|
+
⚠️ **「需求:」(= --req)就是 QUICKLOG 条目标题**——CLI 提取该字段原样作标题(标点随意,仅超 80 字才在就近标点/空格处截断,2026-09-08 起;旧口径「截到首个标点」已废)。仍建议一句话说清改了什么(如「登录限流修复——INCR 计数误清」),需求背景放「根因:/方案:」字段——标题行太长影响 QUICKLOG 可扫读性。单行旧形式的风险仅在「正文里再次出现顶层标签字样」——需求:/根因:/方案:/结果: 字样前导恰是空白或句末标点(。;!?)时,会被边界扫描误当真实字段边界(错位/缺字段被拒)——这正是四参数形式消灭的事故面。注意区分:根因块内以列表行形态出现的嵌套子字段(\`- 现象:\` \`- 根因:\` \`- 护栏:\` \`- 证据:\`)是**合法形态**——「- 」前缀不构成顶层标签、不参与拆分判定,顶层四字段边界不受影响(postmortem 用法见下方可选提示)。
|
|
105
105
|
|
|
106
106
|
> 💡 **可选(非必填)**——根因字段属 postmortem 场景(踩坑修复、值得留档复盘)时,可改用多行 \`--output\` 形式,在根因块内按列表行补四子字段:\`根因:\` 行后换行接 \`- 现象:…\`(出了什么问题)、\`- 根因:…\`(深层原因)、\`- 护栏:…\`(防再犯措施)、\`- 证据:…\`(可追溯路径,如 sillyspec agent-log --json 输出的会话日志 jsonl 路径、review.json、verify-result.md)。该列表行形态为合法格式(顶层标签边界不受影响,详见上方说明);常规场景仍推荐四参数形式。
|
|
107
107
|
|
package/src/stages/verify.js
CHANGED
|
@@ -75,10 +75,12 @@ export const definition = {
|
|
|
75
75
|
- 路径:变更目录下的 verify-required-evidence.json
|
|
76
76
|
- 文件 schema:\`{ items: [{ task, verdict, evidence: string[] }] }\`——顶层是 \`items\`,每项的 \`evidence\` 是字符串数组(不是 \`requiredEvidence\` 键)
|
|
77
77
|
- 如果存在 → 逐项读取 \`items\`,对每个 cannot_verify 任务逐条核对其 \`evidence\` 数组是否已满足
|
|
78
|
-
- 每条 evidence
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
78
|
+
- 每条 evidence 的结论写进 verify-result.md「## 证据账(cannot_verify 任务)」槽段(2026-09-08-ir-verify-facts v2 起为硬门):逐 task 一行 \`- task-NN: <satisfied|missing|partial> | verifiedFiles: <精确路径,逗号分隔>\`
|
|
79
|
+
- verifiedFiles 是 CLI 核验对象:代码/测试类路径核 存在×mtime×git diff 交集(不在本变更 diff 内的文件会被拦);日志/文档类(.runtime/、*.log、docs/)豁免 diff 只核存在×mtime
|
|
80
|
+
- satisfied 必填 verifiedFiles;确实无法验证 → 填 missing 并加(豁免:<一句话理由>)后缀;部分满足 → partial + 已核验路径
|
|
81
|
+
- 存在未豁免 missing 或 satisfied 核验不过 → CLI 阻断 verify 完成(rollback,明细逐文件列出)
|
|
82
|
+
- 如果文件不存在 → 表示 execute 阶段无 cannot_verify 任务,证据账槽段写「无」,正常继续
|
|
83
|
+
- 旧格式(无槽段)报告走 legacy 子串对账(warning 不阻断),新变更一律用 --init 骨架的槽格式
|
|
82
84
|
|
|
83
85
|
### 模块文档加载
|
|
84
86
|
8. 读取 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
|
|
@@ -215,12 +217,14 @@ export const definition = {
|
|
|
215
217
|
3. **生成 verify-result.md 骨架(勿从零手写)**:先跑 \`sillyspec verify-probes --change <change-name> --init\`——一条命令生成十章节骨架(已存在不覆盖),其中**探针结果章节已机械预填**(探针 1 的 TODO/FIXME 命中清单、探针 3 的测试覆盖、探针 5 的 API 契约对账表、探针 6 的删除对账三态判定),文件落 \`{SPEC_ROOT}/changes/<change-name>/verify-result.md\`(CLI 替换出的**主仓绝对路径**;若当前 cwd 在 worktree 内也绝不落 worktree 副本——CLI 校验读主仓,副本随 worktree 清理蒸发)。你只需把各 \`<!--TODO-->\` 占位替换为语义结论;半语义探针(2 关键词覆盖 / 4 决策追踪)与断言抽查、集成盲区标注由你补在对应 TODO 处
|
|
216
218
|
4. **预填探针段不可篡改或删除**:verify \`--done\` gate 会重跑探针对比 verify-result.md 正文,不符即拦(ERROR);对探针结果有异议只能在预填段旁追加说明,不得改写原文
|
|
217
219
|
5. **verify-facts.json 为机器底稿,勿手改**:防篡改对比的基准是正文,改底稿无意义
|
|
218
|
-
6. 给出结论:PASS / PASS WITH NOTES / FAIL
|
|
220
|
+
6. 给出结论:PASS / PASS WITH NOTES / FAIL(受风险门控约束)——**结论只认骨架「结论枚举:」槽行(行首锚定,把 <待填:三选一> 整体替换为枚举值);槽留待填会被 gate 判不过,正文其他位置的 PASS/FAIL 字样不参与判定**
|
|
219
221
|
7. **核对 module-impact.md**(若 \`{SPEC_ROOT}/changes/<change>/module-impact.md\` 存在):对照本次实际代码变更(git diff)与 module-impact.md 的模块影响矩阵,发现不一致(漏标受影响模块 / 影响类型错误 / 实际未触碰的模块被误标)则在 verify-result.md 标注。module-impact 由 plan 首版生成、execute 各 Wave 更新,verify 是最后一次核对机会(archive 仅终审不再生成)。这是 advisory 核对(不阻断 verify 完成),但 module-impact 与实际严重背离应记为风险。
|
|
220
222
|
|
|
221
223
|
### verify-result.md 章节结构(骨架已含,占位替换即可)
|
|
222
224
|
|
|
223
|
-
结论(PASS / PASS WITH NOTES / FAIL)→ 任务完成度 → 设计一致性 → 探针结果(已预填)→ 测试结果 → 决策追踪矩阵(\`| 决策 ID | FR | Task | Evidence | 状态 |\`,存在 decisions.md 才留)→ 技术债务 → 变更风险等级 → Runtime Evidence → 代码审查。
|
|
225
|
+
结论(PASS / PASS WITH NOTES / FAIL)→ 证据账(cannot_verify 任务,槽段)→ 集成验证回执(槽段)→ 任务完成度 → 设计一致性 → 探针结果(已预填)→ 测试结果 → 决策追踪矩阵(\`| 决策 ID | FR | Task | Evidence | 状态 |\`,存在 decisions.md 才留)→ 技术债务 → 变更风险等级 → Runtime Evidence → 代码审查。
|
|
226
|
+
|
|
227
|
+
**「## 集成验证回执」槽行结构**(integration/deployment-critical 变更必填;其余写「无」):\`- claim: <一句话> | command: <命令> | exit: <0 或非 0> | log: <日志路径>\`——CLI 一致性校验四条件:log 存在 × mtime 在 verify 窗口内 × 日志尾无失败签名(error/exception/traceback/fatal 行首,剔除「0 errors」类良性行)× exit 0;全绿才算在场证据,字面措辞不再参与判定(v2 起 literals 仅存量回退)。CLI 不代跑集成进程——回执必须来自你真实执行过的命令。
|
|
224
228
|
|
|
225
229
|
**Runtime Evidence 行结构**(integration/deployment-critical 必填;按实际触碰的运行时组件写,未涉及的行写「不涉及」勿堆关键词):长驻进程启动命令 / 触碰的服务端点 / 触发核心路径的请求(附关键响应)/ 进程日志关键片段(证明走了新路径)/ 生命周期终态断言(初始态→运行态→终态)/ 失败模式排除(逐条说明为何未触发)。
|
|
226
230
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sync-noise.js — 平台连接类失败的跨进程噪音闸(2026-09-08 用户反馈③)。
|
|
3
|
+
*
|
|
4
|
+
* 场景:平台 sillyhub 未就绪(端点 404 / 断连 / 超时)期间,每条 CLI 命令的自动同步
|
|
5
|
+
* (triggerSync → sync() / syncSpecTree)都会打同一批 `[sync] … → 404` / `[spec-sync] …`
|
|
6
|
+
* 失败 warn,淹没真正的门禁输出。CLI 是短命进程,进程内去重无效——必须跨进程落 marker
|
|
7
|
+
* (对齐 run/shared.js 自动 pull 的 AUTO_PULL_THROTTLE_MS marker 先例)。
|
|
8
|
+
*
|
|
9
|
+
* 语义:
|
|
10
|
+
* - 首个失败进程(开窗者)完整展示本轮所有失败行:首行打 warn 并写 mutedUntil 窗口,
|
|
11
|
+
* 同轮**不同**失败行照打(诊断完整性),逐字重复只打一次;
|
|
12
|
+
* - 窗口内**后续进程**的同类失败静默(SILLYSPEC_DEBUG_SYNC=1 时降级为可见输出,排障不瞎)
|
|
13
|
+
* ——每步 CLI 命令重刷的正是这种;
|
|
14
|
+
* - 窗口过期后下一个失败进程再完整报一轮并重开窗——保活信号约每窗口一轮,不静默死;
|
|
15
|
+
* - 任意一次同步成功清 marker,且此前在闸内时打一行恢复提示;
|
|
16
|
+
* - SILLYSPEC_DEBUG_SYNC=1 全程不静默。
|
|
17
|
+
*
|
|
18
|
+
* 边界:只覆盖「连接类」失败(HTTP 404/5xx、网络错、超时/abort)。409 冲突与其余
|
|
19
|
+
* 4xx 业务态(平台就绪但状态冲突/鉴权问题)不走闸——那些是每轮都该看见的语义信号,
|
|
20
|
+
* 且已有各自的冲突降噪机制(sync-conflict-banner-spam 一族)。用户显式命令
|
|
21
|
+
* (platform connect 的 health ping)不应被静默——调用方传 noMute 绕过本闸。
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs'
|
|
24
|
+
import { join } from 'path'
|
|
25
|
+
|
|
26
|
+
const NOISE_MARKER = 'sync-noise-mute.json'
|
|
27
|
+
// 静默窗口:10 分钟 ≈ 一个平台重启/发布窗口;期间每步命令的重复失败全部静默。
|
|
28
|
+
const MUTE_WINDOW_MS = 10 * 60_000
|
|
29
|
+
|
|
30
|
+
// 进程内绑定的 marker 目录(.runtime 绝对路径)。CLI 单用途短进程,last-wins 足够;
|
|
31
|
+
// 未绑定(库形态调用)时闸门退化为直通——宁可多一行 warn 不可丢信号。
|
|
32
|
+
let _runtimeDir = null
|
|
33
|
+
// 本进程是否已开窗(打印过失败并写了 marker)。区分「窗口是本进程开的」与「窗口是先前
|
|
34
|
+
// 进程留下的」:首报命令(开窗者)内后续**不同**失败行仍应完整可见(诊断信息,用户正
|
|
35
|
+
// 在看第一条失败);只有先前进程留下的窗口才静默本进程——这正是跨步去重的目标形态
|
|
36
|
+
// (platform-sync-failure-visibility 契约:同轮清单 500 + POST 500 都要打)。
|
|
37
|
+
let _openedByThisProcess = false
|
|
38
|
+
// 本进程已打印/已静默过的消息集合:同进程内逐字重复的失败行只打一次。
|
|
39
|
+
const _seenInProcess = new Set()
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 绑定噪音闸 marker 的落盘目录(specRoot 下的 .runtime)。幂等,last-wins。
|
|
43
|
+
* @param {string|null} runtimeDir
|
|
44
|
+
*/
|
|
45
|
+
export function bindSyncNoiseRoot(runtimeDir) {
|
|
46
|
+
_runtimeDir = runtimeDir || null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 从 cwd 静默推导 marker 目录(SyncManager 构造器用):有平台指针 → 指针 specRoot/.runtime;
|
|
51
|
+
* 否则 cwd/.sillyspec/.runtime。**零副作用**——只读指针 JSON,不做存在性/自指/temp 守卫,
|
|
52
|
+
* 也不 resolveSpecDir 上溯:resolvePlatformSpecDir 会打「指针不可用/temp 残留」warn
|
|
53
|
+
* (其职责),卫生 marker 不该引入新的输出副作用(cli-top-level-aliases 字节一致契约)。
|
|
54
|
+
* 坏指针/缺字段落回本地目录——marker 落点只需稳定,不需权威。
|
|
55
|
+
* @param {string} cwd
|
|
56
|
+
*/
|
|
57
|
+
export function bindSyncNoiseFromCwd(cwd) {
|
|
58
|
+
try {
|
|
59
|
+
const p = join(cwd, '.sillyspec-platform.json')
|
|
60
|
+
if (existsSync(p)) {
|
|
61
|
+
const s = JSON.parse(readFileSync(p, 'utf8'))
|
|
62
|
+
if (s && typeof s.specRoot === 'string' && s.specRoot) {
|
|
63
|
+
_runtimeDir = join(s.specRoot, '.runtime')
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch { /* 坏指针:回落本地目录 */ }
|
|
68
|
+
_runtimeDir = join(cwd, '.sillyspec', '.runtime')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _markerPath() {
|
|
72
|
+
return _runtimeDir ? join(_runtimeDir, NOISE_MARKER) : null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _readMute() {
|
|
76
|
+
const p = _markerPath()
|
|
77
|
+
if (!p) return null
|
|
78
|
+
try {
|
|
79
|
+
const s = JSON.parse(readFileSync(p, 'utf8'))
|
|
80
|
+
return typeof s.mutedUntil === 'number' ? s : null
|
|
81
|
+
} catch {
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function _writeMute(mutedUntil, lastMsg) {
|
|
87
|
+
const p = _markerPath()
|
|
88
|
+
if (!p) return
|
|
89
|
+
try {
|
|
90
|
+
mkdirSync(_runtimeDir, { recursive: true })
|
|
91
|
+
writeFileSync(p, JSON.stringify({ mutedUntil, lastMsg }) + '\n', 'utf8')
|
|
92
|
+
} catch { /* marker 写失败 = 闸门失效退化为直通,仅损失降噪 */ }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 连接类失败 warn 的闸门口。判序(platform-sync-failure-visibility 契约):
|
|
97
|
+
* 1. debug env → 恒打;
|
|
98
|
+
* 2. 本进程内逐字重复 → 静默(重复行无新信息);
|
|
99
|
+
* 3. 窗口是**本进程**开的(本进程是首报命令)→ 不同失败行照打——首报命令应完整展示
|
|
100
|
+
* 自己轮次内的所有失败细节;
|
|
101
|
+
* 4. 窗口是**先前进程**留的 → 静默(每步命令重刷的正是这种),标记已见;
|
|
102
|
+
* 5. 无窗口(首次/已过期/刚成功清闸)→ 打 + 开窗。
|
|
103
|
+
* @param {string} msg 已拼好的 warn 文案
|
|
104
|
+
* @returns {boolean} true=已输出到 stderr
|
|
105
|
+
*/
|
|
106
|
+
export function syncConnectionWarn(msg) {
|
|
107
|
+
if (process.env.SILLYSPEC_DEBUG_SYNC) {
|
|
108
|
+
console.warn(msg)
|
|
109
|
+
_seenInProcess.add(msg)
|
|
110
|
+
return true
|
|
111
|
+
}
|
|
112
|
+
if (_seenInProcess.has(msg)) return false
|
|
113
|
+
const now = Date.now()
|
|
114
|
+
const m = _readMute()
|
|
115
|
+
if (m && now < m.mutedUntil) {
|
|
116
|
+
if (_openedByThisProcess) {
|
|
117
|
+
// 首报命令的后续不同失败行:可见(诊断完整性),不重复开窗
|
|
118
|
+
console.warn(msg)
|
|
119
|
+
_seenInProcess.add(msg)
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
_seenInProcess.add(msg)
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
console.warn(msg)
|
|
126
|
+
_seenInProcess.add(msg)
|
|
127
|
+
_writeMute(now + MUTE_WINDOW_MS, msg)
|
|
128
|
+
_openedByThisProcess = true
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 判定 HTTP 状态码是否属连接类(可静默):404(端点未部署/未就绪)与 5xx(服务端故障)。
|
|
134
|
+
* 409 与其余 4xx 不在此列(见模块头注释边界)。
|
|
135
|
+
*/
|
|
136
|
+
export function isConnectionClassStatus(status) {
|
|
137
|
+
return status === 404 || (typeof status === 'number' && status >= 500)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 任意一次同步成功:清 marker。此前处于闸内(marker 存在)时打一行恢复提示——
|
|
142
|
+
* 平台恢复的第一个信号应该可见,之后的成功照常静默(成功不打扰)。
|
|
143
|
+
*/
|
|
144
|
+
export function noteSyncSuccess() {
|
|
145
|
+
_openedByThisProcess = false
|
|
146
|
+
_seenInProcess.clear()
|
|
147
|
+
if (!_readMute()) return
|
|
148
|
+
const p = _markerPath()
|
|
149
|
+
if (p) { try { unlinkSync(p) } catch { /* 残留 marker 只多静默一轮失败,无害 */ } }
|
|
150
|
+
console.log('[sync] 平台连接已恢复(此前静默的同步失败已停止)')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 测试复位:清进程内绑定与开窗状态。 */
|
|
154
|
+
export function _resetSyncNoiseForTest() {
|
|
155
|
+
_runtimeDir = null
|
|
156
|
+
_openedByThisProcess = false
|
|
157
|
+
_seenInProcess.clear()
|
|
158
|
+
}
|