sillyspec 3.33.0 → 3.33.1
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 +1 -1
- package/.github/workflows/ci.yml +11 -3
- package/AGENTS.md +1 -1
- package/assets/command-cards/flow.md +1 -1
- package/package.json +1 -1
- package/src/archive-delta.js +4 -3
- package/src/change-delete.js +6 -5
- package/src/contract-matrix.js +2 -1
- package/src/cross-placement.js +5 -1
- package/src/db.js +8 -2
- package/src/deleted-ledger.js +2 -1
- package/src/doctor-diagnostics.js +8 -3
- package/src/flow-draft.js +2 -1
- package/src/flow.js +111 -62
- package/src/friction-ledger.js +2 -1
- package/src/friction-tally.js +8 -4
- package/src/handoff.js +2 -1
- package/src/hooks/worktree-guard.js +5 -5
- package/src/index.js +41 -7
- package/src/knowledge-hits.js +2 -1
- package/src/knowledge-vector.js +2 -1
- package/src/progress/consistency-doctor.js +5 -2
- package/src/progress/stage-machine.js +6 -2
- package/src/progress.js +19 -8
- package/src/quicklog.js +194 -9
- package/src/review-dispatch.js +4 -3
- package/src/review-tier.js +2 -1
- package/src/route-hindsight.js +4 -3
- package/src/run/bg-sync.js +10 -3
- package/src/run/command.js +13 -11
- package/src/run/complete-handlers.js +40 -37
- package/src/run/complete.js +12 -8
- package/src/run/gate-snapshot-ledger.js +2 -1
- package/src/run/gate-snapshot.js +57 -11
- package/src/run/gates.js +31 -7
- package/src/run/prompt.js +6 -5
- package/src/run/scan-profile.js +2 -1
- package/src/run/shared.js +109 -5
- package/src/run/stage.js +5 -4
- package/src/run/test-ledger.js +2 -1
- package/src/run/verify-quality-scan.js +11 -5
- package/src/runtime-paths.js +189 -0
- package/src/scan-refresh.js +4 -3
- package/src/scope-audit.js +7 -6
- package/src/sentinel-assertions.js +37 -0
- package/src/spec-sync.js +38 -22
- package/src/stage-contract.js +24 -2
- package/src/stage-review.js +3 -2
- package/src/stages/brainstorm.js +1 -1
- package/src/sync-noise.js +3 -2
- package/src/sync.js +71 -15
- package/src/task-done.js +2 -1
- package/src/task-review.js +7 -5
- package/src/task-tick.js +23 -1
- package/src/validate-artifacts.js +2 -1
- package/src/verify-draft.js +2 -1
- package/src/verify-postcheck.js +11 -10
- package/src/verify-probes.js +157 -52
- package/src/watcher.js +15 -9
- package/src/worktree-apply.js +2 -1
- package/src/write-audit.js +2 -1
- package/templates/agents-instruction.md +1 -1
package/src/run/gates.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* 不搬:ensureDepsFreshness(execute 入口 deps 自检,调用方 runStage 非 completeStep,归属未来 execute-handler)
|
|
29
29
|
*/
|
|
30
30
|
import { basename, join, relative, dirname } from 'node:path'
|
|
31
|
+
import { runtimeStatePath } from '../runtime-paths.js'
|
|
31
32
|
import { createHash } from 'node:crypto'
|
|
32
33
|
import { execFileSync } from 'node:child_process'
|
|
33
34
|
import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
@@ -444,8 +445,8 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
|
|
|
444
445
|
if (worktreeGone && changeName) {
|
|
445
446
|
try {
|
|
446
447
|
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
447
|
-
const legitimatelyEnded = existsSync(
|
|
448
|
-
|| existsSync(
|
|
448
|
+
const legitimatelyEnded = existsSync(runtimeStatePath(runtimeRoot, `apply-pathspec-${changeName}.txt`))
|
|
449
|
+
|| existsSync(runtimeStatePath(runtimeRoot, `execute-cleanup-${changeName}.json`))
|
|
449
450
|
if (legitimatelyEnded) {
|
|
450
451
|
console.log('ℹ️ worktree 已在 apply/execute 收尾时正当清理(凭据在 .runtime),deps 门放行')
|
|
451
452
|
return true
|
|
@@ -549,7 +550,7 @@ function rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx,
|
|
|
549
550
|
|
|
550
551
|
/** 档位状态文件路径(runtimeRoot 下 per-change;命名对齐 apply-pathspec-<change>.txt 先例) */
|
|
551
552
|
function ceremonyTierFilePath(runtimeRoot, changeName) {
|
|
552
|
-
return
|
|
553
|
+
return runtimeStatePath(runtimeRoot, `ceremony-tier-${changeName}.json`)
|
|
553
554
|
}
|
|
554
555
|
|
|
555
556
|
/**
|
|
@@ -1100,17 +1101,24 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1100
1101
|
// 变更文档随快照(test_strategy 消费)。并行会话在途文件物理不进快照、不参与本门判定。
|
|
1101
1102
|
// 快照基建失败 → 回退主仓现行为(失败路径沿用 renderVerifyTestAttribution 归因提示)。
|
|
1102
1103
|
let verifyGateSnap = null
|
|
1104
|
+
let snapshotFailRef = null // createVerifyGateSnapshot 出参:结构化失败原因(FR-04,替代硬编码笼统文案)
|
|
1103
1105
|
if (!process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF) {
|
|
1104
1106
|
try {
|
|
1105
1107
|
const { createVerifyGateSnapshot } = await import('./gate-snapshot.js')
|
|
1106
|
-
|
|
1108
|
+
snapshotFailRef = {}
|
|
1109
|
+
verifyGateSnap = await createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts, failRef: snapshotFailRef })
|
|
1107
1110
|
if (verifyGateSnap) {
|
|
1108
1111
|
console.log(`🧪 Verify 实测隔离快照(根:${verifyGateSnap.snapshotRoot}):HEAD + 本变更 ${verifyGateSnap.changeFileCount} 个文件${verifyGateSnap.sourceRoot ? '(overlay 自 worktree——本变更分支内容定向跑)' : ''}(并行会话脏文件不参与门判定)`)
|
|
1109
1112
|
} else {
|
|
1110
|
-
// FR-02(2026-10-09-verify-reuse-friction):null 回退与异常回退同等可见 +
|
|
1113
|
+
// FR-02(2026-10-09-verify-reuse-friction):null 回退与异常回退同等可见 + 记账;
|
|
1114
|
+
// FR-04(2026-10-16-fix-downstream-tool-defects):reason 消费 failRef 结构化原因
|
|
1115
|
+
// (pnpm 布局拒建 / 文件集为空三分成因 / junction 失败 / 基建异常详情),不再笼统
|
|
1111
1116
|
try {
|
|
1112
1117
|
const { reportGateSnapshotFallback } = await import('./gate-snapshot.js')
|
|
1113
|
-
|
|
1118
|
+
const fbReason = snapshotFailRef && snapshotFailRef.reason
|
|
1119
|
+
? `createVerifyGateSnapshot 返回 null:${snapshotFailRef.reason}`
|
|
1120
|
+
: 'createVerifyGateSnapshot 返回 null(变更文件集为空 / 快照基建不可用)'
|
|
1121
|
+
if (typeof reportGateSnapshotFallback === 'function') await reportGateSnapshotFallback({ cwd, changeName, platformOpts, where: 'verify --done 实测门', reason: fbReason })
|
|
1114
1122
|
} catch { /* 报告失败不改变回退行为(含测试 mock 模块缺导出的形态) */ }
|
|
1115
1123
|
}
|
|
1116
1124
|
} catch (e) {
|
|
@@ -1120,6 +1128,22 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1120
1128
|
} catch { /* 报告失败不改变回退行为(含测试 mock 模块缺导出的形态) */ }
|
|
1121
1129
|
}
|
|
1122
1130
|
}
|
|
1131
|
+
// FR-04 降级事实落盘:used_snapshot 与失败原因随 test-result.json extra 持久化(快照在场
|
|
1132
|
+
// 时记录随快照销毁;回退主仓时恰好落在主仓 .runtime——降级证据存活面与用途一致)。
|
|
1133
|
+
// 评审 P3-2 整改:OFF 开关是主动关闸,不得记成「基建故障/文件集为空」的笼统回退原因。
|
|
1134
|
+
let snapObsFields
|
|
1135
|
+
if (process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF) {
|
|
1136
|
+
snapObsFields = { used_snapshot: false, snapshot_fallback_reason: 'SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF=1 主动关闸(快照未尝试,非基建故障)' }
|
|
1137
|
+
} else if (verifyGateSnap) {
|
|
1138
|
+
snapObsFields = { used_snapshot: true }
|
|
1139
|
+
} else {
|
|
1140
|
+
try {
|
|
1141
|
+
const { snapshotObservability } = await import('./gate-snapshot.js')
|
|
1142
|
+
snapObsFields = snapshotObservability(false, snapshotFailRef)
|
|
1143
|
+
} catch {
|
|
1144
|
+
snapObsFields = { used_snapshot: false, snapshot_fallback_reason: snapshotFailRef && snapshotFailRef.reason ? String(snapshotFailRef.reason) : 'createVerifyGateSnapshot 返回 null(变更文件集为空 / 快照基建不可用)' }
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1123
1147
|
const gateCwd = verifyGateSnap ? verifyGateSnap.snapshotRoot : cwd
|
|
1124
1148
|
const gateSpecBase = verifyGateSnap ? join(verifyGateSnap.snapshotRoot, '.sillyspec') : specBase
|
|
1125
1149
|
let testCheck
|
|
@@ -1220,7 +1244,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1220
1244
|
`质量扫描: ${reusableScan ? '命中(上层未消费)' : '指纹失配/无记录'}`,
|
|
1221
1245
|
`green-cache: ${greenHit ? '命中(上层未消费)' : '指纹失配/过期/无记录'}`,
|
|
1222
1246
|
].join(';')
|
|
1223
|
-
testCheck = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName, ctx, observability: { fingerprint: _obsFingerprint, reuse_decision: { layer: 'real-run', hit: false, reason: _missReason } }, ...(Array.isArray(_restrict) && _restrict.length > 0 ? { restrictFiles: _restrict } : {}) })
|
|
1247
|
+
testCheck = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName, ctx, observability: { fingerprint: _obsFingerprint, reuse_decision: { layer: 'real-run', hit: false, reason: _missReason }, ...snapObsFields }, ...(Array.isArray(_restrict) && _restrict.length > 0 ? { restrictFiles: _restrict } : {}) })
|
|
1224
1248
|
printVerifyTestCheck(testCheck)
|
|
1225
1249
|
// 自动绑定补全(full-autopilot-parity,R21 thin 迁移):测试已跑、test-result.json 已落盘,
|
|
1226
1250
|
// verify --done 收口时从测试结果自动补全 requirements.md 的空绑定槽——与 thin 同逻辑(逐行扫描)。
|
package/src/run/prompt.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* - loadModuleContextIndex/buildModuleContextInjection 内 require('fs'/'path') 改顶部静态 import
|
|
20
20
|
*/
|
|
21
21
|
import { basename, dirname, join } from 'node:path'
|
|
22
|
+
import { runtimeStatePath } from '../runtime-paths.js'
|
|
22
23
|
import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
23
24
|
import { createHash } from 'node:crypto'
|
|
24
25
|
import jsYaml from 'js-yaml'
|
|
@@ -868,7 +869,7 @@ export async function renderPreflightFailures({ stageName, stepName, cwd, specBa
|
|
|
868
869
|
export function shouldInjectFullContext({ changeName, stageName, runtimeRoot } = {}) {
|
|
869
870
|
try {
|
|
870
871
|
if (!runtimeRoot || !changeName || !stageName) return { full: true }
|
|
871
|
-
const ledgerPath =
|
|
872
|
+
const ledgerPath = runtimeStatePath(runtimeRoot, `prompt-inject-${changeName}.json`)
|
|
872
873
|
if (!existsSync(ledgerPath)) return { full: true }
|
|
873
874
|
const ledger = JSON.parse(readFileSync(ledgerPath, 'utf8'))
|
|
874
875
|
const rec = (ledger && ledger.stages && typeof ledger.stages === 'object') ? ledger.stages[stageName] : null
|
|
@@ -906,7 +907,7 @@ function renderPromptInjectSummaryLine({ firstStep, digest, specBase, projectNam
|
|
|
906
907
|
* @param {{ runtimeRoot: string, changeName: string, stageName: string, firstStep: number, digest: string }} args
|
|
907
908
|
*/
|
|
908
909
|
async function recordPromptInjectLedger({ runtimeRoot, changeName, stageName, firstStep, digest }) {
|
|
909
|
-
const ledgerPath =
|
|
910
|
+
const ledgerPath = runtimeStatePath(runtimeRoot, `prompt-inject-${changeName}.json`)
|
|
910
911
|
await withFileLock(ledgerPath + '.lock', () => {
|
|
911
912
|
let ledger = null
|
|
912
913
|
try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { ledger = null }
|
|
@@ -1428,7 +1429,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1428
1429
|
// 注入结果同步落 runtime JSON(与既有 KNOWLEDGE_HIT_REPORT 落盘口径一致)
|
|
1429
1430
|
const decRuntimeDir = join(decSpecBase, '.runtime')
|
|
1430
1431
|
mkdirSync(decRuntimeDir, { recursive: true })
|
|
1431
|
-
writeAtomicSync(
|
|
1432
|
+
writeAtomicSync(runtimeStatePath(decRuntimeDir, 'decision-hits.json'), JSON.stringify({ matched: decResult.matched, decisionHits: decResult.decisionHits || [] }, null, 2) + '\n')
|
|
1432
1433
|
} catch (e) {
|
|
1433
1434
|
substitute(/\{DECISION_HITS\}/g, `[decisions] 决策命中检测异常(${e.message}),跳过`)
|
|
1434
1435
|
}
|
|
@@ -1495,7 +1496,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1495
1496
|
)
|
|
1496
1497
|
// 写入 runtime JSON(既有机制照旧,新旧遥测共存)
|
|
1497
1498
|
mkdirSync(runtimeDir, { recursive: true })
|
|
1498
|
-
writeAtomicSync(
|
|
1499
|
+
writeAtomicSync(runtimeStatePath(runtimeDir, 'knowledge-hit-report.json'), JSON.stringify(knowledgeInjection.json, null, 2) + '\n')
|
|
1499
1500
|
} catch (e) {
|
|
1500
1501
|
substitute(/\{KNOWLEDGE_HIT_REPORT\}/g, 'Status: no matches (error: ' + e.message + ')')
|
|
1501
1502
|
}
|
|
@@ -1552,7 +1553,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1552
1553
|
let runId = ''
|
|
1553
1554
|
const execSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1554
1555
|
const runtimeRoot = resolveRuntimeRoot(platformOpts, execSpecBase)
|
|
1555
|
-
const runIdFile =
|
|
1556
|
+
const runIdFile = runtimeStatePath(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
1556
1557
|
try {
|
|
1557
1558
|
if (existsSync(runIdFile)) {
|
|
1558
1559
|
const c = readFileSync(runIdFile, 'utf8').trim()
|
package/src/run/scan-profile.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - executeScanPostcheck 原冗余动态 import('fs'/'path'/'child_process') 删除,改顶部静态(execSync 实际未直接用,纯遗留)
|
|
10
10
|
*/
|
|
11
11
|
import { join, basename, extname } from 'node:path'
|
|
12
|
+
import { runtimeStatePath } from '../runtime-paths.js'
|
|
12
13
|
import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync, readFileSync } from 'node:fs'
|
|
13
14
|
import { safeGit, resolveRuntimeRoot } from './shared.js'
|
|
14
15
|
import { SCAN_REQUIRED_DOCS } from '../constants.js'
|
|
@@ -332,7 +333,7 @@ export async function executeScanResumeCheck(cwd, platformOpts) {
|
|
|
332
333
|
// .runtime/last-delta.json(归档/delta 补跑写入的最近变更快照),14 天窗口内提示本轮
|
|
333
334
|
// scan 优先核对其涉及模块——advisory 不阻断不改变步骤结构;缺失/过期/解析失败静默。
|
|
334
335
|
try {
|
|
335
|
-
const sidecarPath =
|
|
336
|
+
const sidecarPath = runtimeStatePath(resolveRuntimeRoot(platformOpts, specBase), 'last-delta.json')
|
|
336
337
|
if (existsSync(sidecarPath)) {
|
|
337
338
|
const sidecar = JSON.parse(readFileSync(sidecarPath, 'utf8'))
|
|
338
339
|
const ageMs = Date.now() - Date.parse(sidecar.updatedAt || '')
|
package/src/run/shared.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* 本模块仅 re-export,run/ 层现有调用方路径与行为不变。
|
|
12
12
|
*/
|
|
13
13
|
import { basename, join, resolve, dirname, sep } from 'node:path'
|
|
14
|
+
import { runtimeTempPath } from '../runtime-paths.js'
|
|
14
15
|
import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, appendFileSync, realpathSync } from 'node:fs'
|
|
15
16
|
import { fileURLToPath } from 'node:url'
|
|
16
17
|
import os from 'node:os'
|
|
@@ -644,7 +645,7 @@ export function warnSelfRefPointerOnce(cwd, specRoot, message) {
|
|
|
644
645
|
try {
|
|
645
646
|
const dir = join(cwd, '.sillyspec', '.runtime')
|
|
646
647
|
mkdirSync(dir, { recursive: true })
|
|
647
|
-
const markerPath =
|
|
648
|
+
const markerPath = runtimeTempPath(dir, SELFREF_WARN_MARKER)
|
|
648
649
|
const now = Date.now()
|
|
649
650
|
try {
|
|
650
651
|
const m = JSON.parse(readFileSync(markerPath, 'utf8'))
|
|
@@ -943,7 +944,9 @@ async function raceWithAbort(op, timeoutMs = resolveSyncTotalTimeoutMs()) {
|
|
|
943
944
|
const controller = new AbortController()
|
|
944
945
|
let timer
|
|
945
946
|
try {
|
|
946
|
-
|
|
947
|
+
// 透传 op 结果(2026-10-17 终态强校验消费):历史调用方(triggerSync 等)忽略返回值
|
|
948
|
+
// 不受影响;超时分支 resolve(null)——消费方以 null 判「预算熔断」。
|
|
949
|
+
return await Promise.race([
|
|
947
950
|
op({ signal: controller.signal }),
|
|
948
951
|
new Promise((resolve) => { timer = setTimeout(() => { controller.abort(); resolve(null) }, timeoutMs) }),
|
|
949
952
|
])
|
|
@@ -1039,6 +1042,86 @@ export async function triggerSync(cwd, changeName, platformOpts = {}, opts = {})
|
|
|
1039
1042
|
}
|
|
1040
1043
|
}
|
|
1041
1044
|
|
|
1045
|
+
/**
|
|
1046
|
+
* 终态强校验同步(2026-10-17-archive-half-state-location-stuck-active)。
|
|
1047
|
+
*
|
|
1048
|
+
* 背景(坑档案 docs/sillyspec/archive-half-state-location-stuck-active.md,生产实证):
|
|
1049
|
+
* triggerSync 全链 best-effort + 后台化,重试是事件驱动(「下一条命令」)——而归档/删除
|
|
1050
|
+
* 是变更生命周期最后一个动作,之后大概率没有下一条命令。平台慢窗口内 DB 通道
|
|
1051
|
+
* (status=archived)成功而文件树通道(location)失败 → 半态:平台「进行中」列表
|
|
1052
|
+
* 幽灵滞留已归档变更,无人再碰则无限期。
|
|
1053
|
+
*
|
|
1054
|
+
* 与 triggerSync 的三点差异:
|
|
1055
|
+
* 1. **终态判定驱动**:仅当 DB 该变更 status ∈ (archived, deleted) 才走强校验;
|
|
1056
|
+
* 非终态(普通阶段完成的 completion 同步等)原样委托 triggerSync(后台化不变,
|
|
1057
|
+
* 零行为变化)。归档收尾允许阻塞换一致性——终态动作没有「下次再推」的假设。
|
|
1058
|
+
* 2. **内联有界**:绕过后台化(结果不再藏进 bg 日志文件),sync() 直跑 + raceWithAbort
|
|
1059
|
+
* 预算 = max(env SILLYSPEC_SYNC_TIMEOUT_MS 可调值, 45s 后台轮下限同款);opts.timeoutMs
|
|
1060
|
+
* 显式传入时直接生效(测试注入短窗)。
|
|
1061
|
+
* 3. **失败显性化**:sync() 返回 treeOk!==true(syncSpecTree ok 契约,fail-closed——
|
|
1062
|
+
* 无 ok 字段视同未确认)→ 🚨 告警 + 自救命令 + process.exitCode = 1(对齐
|
|
1063
|
+
* _submitApproval 的「终态网络失败必须可见」先例)。未决冲突(result.conflict)
|
|
1064
|
+
* 不在此列——冲突面已有自己的 resolve 处置横幅,重复告警是噪音。
|
|
1065
|
+
*
|
|
1066
|
+
* @returns {{confirmed: boolean, delegated?: boolean, skipped?: string, result?: object}}
|
|
1067
|
+
*/
|
|
1068
|
+
export async function triggerSyncTerminalStrict(cwd, changeName, platformOpts = {}, opts = {}) {
|
|
1069
|
+
// quick 会话无实体变更目录(无归档/删除终态)——沿用既有 best-effort 语义
|
|
1070
|
+
if (changeName && QUICK_SID_RE.test(changeName)) {
|
|
1071
|
+
await triggerSync(cwd, changeName, platformOpts, opts)
|
|
1072
|
+
return { confirmed: false, delegated: true }
|
|
1073
|
+
}
|
|
1074
|
+
const syncMod = await import('../sync.js')
|
|
1075
|
+
// 未连接平台:静默 no-op(本地合法状态;强校验只在平台模式下有意义)
|
|
1076
|
+
if (!syncMod.peekPlatformConnected(cwd, platformOpts)) {
|
|
1077
|
+
return { confirmed: false, skipped: 'not-connected' }
|
|
1078
|
+
}
|
|
1079
|
+
// 终态判定(fail-open:DB 读不到/解析失败 → 非终态委托既有语义,不因校验层故障阻断主链)
|
|
1080
|
+
// opts.changeStatus / opts.syncImpl:测试注入缝(对齐 bakeArchiveTimeline writeImpl 惯例)
|
|
1081
|
+
let dbStatus = opts.changeStatus ?? null
|
|
1082
|
+
if (dbStatus === null) {
|
|
1083
|
+
try {
|
|
1084
|
+
const { resolvePlatformSpecDir, ProgressManager } = await import('../progress.js')
|
|
1085
|
+
const specDir = resolvePlatformSpecDir(cwd)
|
|
1086
|
+
if (specDir && existsSync(join(specDir, '.runtime', 'sillyspec.db'))) {
|
|
1087
|
+
const pm = new ProgressManager({ specDir })
|
|
1088
|
+
const row = pm._ensureDB(cwd).getDb().prepare(
|
|
1089
|
+
'SELECT status FROM changes WHERE name = ?'
|
|
1090
|
+
).get(changeName)
|
|
1091
|
+
dbStatus = row ? row.status : null
|
|
1092
|
+
}
|
|
1093
|
+
} catch { /* 判定失败按非终态处理 */ }
|
|
1094
|
+
}
|
|
1095
|
+
if (dbStatus !== 'archived' && dbStatus !== 'deleted') {
|
|
1096
|
+
await triggerSync(cwd, changeName, platformOpts, opts)
|
|
1097
|
+
return { confirmed: false, delegated: true }
|
|
1098
|
+
}
|
|
1099
|
+
// 终态:内联有界同步(绕过后台化——结果必须当场可判)
|
|
1100
|
+
const timeoutMs = opts.timeoutMs ?? Math.max(resolveSyncTotalTimeoutMs(), 45_000)
|
|
1101
|
+
let result = null
|
|
1102
|
+
try {
|
|
1103
|
+
// raceWithAbort 向 op 传入 { signal } 形状对象(非裸 AbortSignal,与既有 triggerSync
|
|
1104
|
+
// 内联用法 `sync(name, cwd, sig)` 同一形状)——默认路径原样透传,禁止二次包装
|
|
1105
|
+
//(dogfood 实证:包装成 { signal: sig } 会把套娃对象传进 AbortSignal.any → TypeError,
|
|
1106
|
+
// 强校验当场以 🚨 告警兜住——安全网本身工作正常,此处修被网住的 bug)。
|
|
1107
|
+
const runSync = opts.syncImpl || ((syncOpts) => syncMod.sync(changeName, cwd, syncOpts))
|
|
1108
|
+
result = await raceWithAbort(runSync, timeoutMs)
|
|
1109
|
+
} catch (e) {
|
|
1110
|
+
result = { synced: 0, errors: [(e && e.message) || String(e)] }
|
|
1111
|
+
}
|
|
1112
|
+
if (result === null) result = { synced: 0, errors: [`同步总预算 ${timeoutMs}ms 熔断`] }
|
|
1113
|
+
// 未决冲突:已有 resolve 处置横幅(syncSpecTree 冲突分支自带),不重复告警
|
|
1114
|
+
if (result.conflict === true) return { confirmed: false, skipped: 'conflict', result }
|
|
1115
|
+
const confirmed = result.treeOk === true
|
|
1116
|
+
if (!confirmed) {
|
|
1117
|
+
const detail = ((result.errors && result.errors[0]) || '文件树通道未确认收敛(见上方 [spec-sync]/[sync] 行)').slice(0, 200)
|
|
1118
|
+
console.error(`🚨 [终态同步] 本地${dbStatus === 'deleted' ? '删除' : '归档'}已完成,但平台文件树推送未确认:${detail}`)
|
|
1119
|
+
console.error(` 平台变更中心可能滞留「进行中」半态。自救(幂等):sillyspec platform sync --change ${changeName}`)
|
|
1120
|
+
if (opts.exitOnUnconfirmed !== false) process.exitCode = 1
|
|
1121
|
+
}
|
|
1122
|
+
return { confirmed, result }
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1042
1125
|
/**
|
|
1043
1126
|
* 触发步骤开始上报(X3 / design §8.2,变更 2026-08-29-change-delete-closure-and-spec-pull task-13)。
|
|
1044
1127
|
*
|
|
@@ -1087,7 +1170,8 @@ const AUTO_PULL_THROTTLE_MS = 10_000
|
|
|
1087
1170
|
const AUTO_PULL_TIMEOUT_MS = 2_000
|
|
1088
1171
|
|
|
1089
1172
|
function _autoPullThrottlePath(cwd) {
|
|
1090
|
-
|
|
1173
|
+
// 2026-10-18 布局分层:节流戳是可再生产物归 temp/;路径助手自带 mkdir-p
|
|
1174
|
+
return runtimeTempPath(join(cwd, '.sillyspec', '.runtime'), 'auto-pull-throttle.json')
|
|
1091
1175
|
}
|
|
1092
1176
|
|
|
1093
1177
|
function _autoPullRecently(cwd) {
|
|
@@ -1546,15 +1630,35 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
1546
1630
|
// gen:types 重写行尾制造假 M):git diff --ignore-cr-at-eol HEAD 的变更名单是「真实内容变更」
|
|
1547
1631
|
// 集——tracked 修改出现在 porcelain 但不在此集 = 纯行尾重写。单次对照调用,剔出 changedFiles
|
|
1548
1632
|
// 归 eolOnlyFiles 软警告(.gitattributes 指引),不再计入文件行/触发各门。
|
|
1633
|
+
// 探测必须走 --numstat:--name-only 不吃 --ignore-cr-at-eol(git 实测怪癖——git ≤2.43 仍把
|
|
1634
|
+
// 行尾-only 文件列进名单,Debian 12 git 2.39.5 / Ubuntu 24.04 git 2.43.0 实测;git ≥2.54 才
|
|
1635
|
+
// 听话),走 --name-only 会让本过滤在老 git 上静默失效、假 M 坑复活。同
|
|
1636
|
+
// verify-postcheck reconcileActualFiles 的 eolOnlyFiles 探测同口径(该处已按此结论实装)。
|
|
1549
1637
|
// 注意:gitQuiet 不在本模块 import 面(safeGit 才是)——首版用 gitQuiet 触发
|
|
1550
1638
|
// ReferenceError 被 catch 吞成 null,又被 new Set(null) 包装成空集 → 全部 tracked 修改
|
|
1551
1639
|
// 被误判 EOL-only 剔除(audit-quick-completion 回归抓出)。改用 safeGit(已 import,
|
|
1552
1640
|
// 返回 {value,error}),失败显式落 null 透传不过滤。
|
|
1641
|
+
// -z 记录文法(od 实测,git 2.39.5 与 2.54 同形):普通条目 `<A>\t<D>\t<path>\0`;
|
|
1642
|
+
// rename `<A>\t<D>\t\0<old>\0<new>\0`(计数后路径字段为空,旧/新名各占一个 NUL 字段)——
|
|
1643
|
+
// 按位置取新名。非 -z 的 `old => new` 文本形态与「文件名里字面含 ` => `」不可区分(实测
|
|
1644
|
+
// `sub/a => b.js` 真实内容变更会被误判 rename 而剔出 changedFiles——审计放宽),-z 消歧。
|
|
1645
|
+
// -z 同时不做引号/C 转义,路径原样出(与 porcelain 侧键更一致)。
|
|
1646
|
+
const numstatZPaths = (out) => {
|
|
1647
|
+
const toks = String(out).split('\0')
|
|
1648
|
+
const paths = []
|
|
1649
|
+
for (let i = 0; i < toks.length; i++) {
|
|
1650
|
+
const m = toks[i].match(/^(?:\d+|-)\t(?:\d+|-)\t(.*)$/s)
|
|
1651
|
+
if (!m) continue
|
|
1652
|
+
if (m[1] === '') { paths.push(String(toks[i + 2] || '')); i += 2 }
|
|
1653
|
+
else paths.push(m[1])
|
|
1654
|
+
}
|
|
1655
|
+
return paths.filter(Boolean)
|
|
1656
|
+
}
|
|
1553
1657
|
const eolRawList = (() => {
|
|
1554
1658
|
try {
|
|
1555
|
-
const res = safeGit(cwd, ['diff', '--
|
|
1659
|
+
const res = safeGit(cwd, ['diff', '--numstat', '-z', '--ignore-cr-at-eol', 'HEAD'], { timeout: 30000 })
|
|
1556
1660
|
if (!res || res.error) return null
|
|
1557
|
-
return String(res.value || '')
|
|
1661
|
+
return numstatZPaths(String(res.value || ''))
|
|
1558
1662
|
} catch { return null }
|
|
1559
1663
|
})()
|
|
1560
1664
|
const eolRealChanged = eolRawList === null ? null : new Set(eolRawList)
|
package/src/run/stage.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* - 'child_process'(execSync)裸模块名不变
|
|
18
18
|
*/
|
|
19
19
|
import { join, dirname } from 'node:path'
|
|
20
|
+
import { runtimeStatePath } from '../runtime-paths.js'
|
|
20
21
|
import { existsSync, readdirSync, readFileSync, mkdirSync, rmSync, unlinkSync } from 'node:fs'
|
|
21
22
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
22
23
|
import { resolveSpecDir, resolveChangeDir, resolveRuntimeRoot, resolveQuickSessionsDir, triggerSync, safeGit, formatWaitOptions, checkApproval, getStageSteps, warnApprovalUnknown, predictProtectedQuickFiles, mergeQuickBoundaryFiles, readStageBurst, STAGE_BURST_STAGES, readStageWall, STAGE_WALL_STAGES } from './shared.js'
|
|
@@ -55,7 +56,7 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
55
56
|
const curSession = process.env.SILLYSPEC_SESSION_ID || ''
|
|
56
57
|
const wallStageData = progress.stages && progress.stages[stageName]
|
|
57
58
|
const firstEntry = !wallStageData || !Array.isArray(wallStageData.steps) || wallStageData.steps.length === 0
|
|
58
|
-
const wallLedgerPath = join(specBase, '.runtime', `stage-session-ledger-${changeName}.json`)
|
|
59
|
+
const wallLedgerPath = runtimeStatePath(join(specBase, '.runtime'), `stage-session-ledger-${changeName}.json`)
|
|
59
60
|
if (curSession && firstEntry && existsSync(wallLedgerPath)) {
|
|
60
61
|
const wallLedger = JSON.parse(readFileSync(wallLedgerPath, 'utf8'))
|
|
61
62
|
const { isStageWallBlocked } = await import('./shared.js')
|
|
@@ -151,7 +152,7 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
151
152
|
const { generateExecuteRunId, isValidExecuteRunId, claimExecuteRunId } = await import('../task-review.js')
|
|
152
153
|
const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
153
154
|
const runtimeRoot = resolveRuntimeRoot(platformOpts, execSpecBase)
|
|
154
|
-
const runIdFile =
|
|
155
|
+
const runIdFile = runtimeStatePath(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
155
156
|
mkdirSync(runtimeRoot, { recursive: true })
|
|
156
157
|
// 优先读取已有的变更专属标记文件(agent 可写内容,格式校验防注入/穿越,非法视为缺失重生成)
|
|
157
158
|
try {
|
|
@@ -325,7 +326,7 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
325
326
|
startedAt: new Date().toISOString(),
|
|
326
327
|
forceRescan: quickOpts?.isForceRescan || false,
|
|
327
328
|
}
|
|
328
|
-
const guardFile = join(specBase, '.runtime', 'scan-guard.json')
|
|
329
|
+
const guardFile = runtimeStatePath(join(specBase, '.runtime'), 'scan-guard.json')
|
|
329
330
|
mkdirSync(dirname(guardFile), { recursive: true })
|
|
330
331
|
// 原子写:hook 进程(worktree-guard 的 readScanGuard)并发读,裸 truncate→write 的
|
|
331
332
|
// 半截窗口会被 JSON.parse 吞成 null → 覆盖保护静默失效(fail-open)
|
|
@@ -417,7 +418,7 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
417
418
|
// 进程内直调 runCommand 的入口(测试/编程调用)与 --change 指向不存在会话的形态。
|
|
418
419
|
console.error(`❌ quick 通道已退役,无法启动 quick 会话(若带 --change 则该会话不存在或已清理)。`)
|
|
419
420
|
console.error(` 新工作请走轻量变更(--input 引号内换行合法,可照抄):`)
|
|
420
|
-
console.error(' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
421
|
+
console.error(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
|
|
421
422
|
console.error('')
|
|
422
423
|
console.error(' 成功标准:')
|
|
423
424
|
console.error(' - <可验证标准>"')
|
package/src/run/test-ledger.js
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
* (meta.json 并发读先例同款约束)。
|
|
37
37
|
*/
|
|
38
38
|
import { createHash } from 'crypto'
|
|
39
|
+
import { runtimeStatePath } from '../runtime-paths.js'
|
|
39
40
|
import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
|
|
40
41
|
import { join, relative, isAbsolute } from 'path'
|
|
41
42
|
import { gitQuiet } from '../git-helper.js'
|
|
@@ -209,7 +210,7 @@ function ledgerKeyFor({ runtimeRoot, changeName, projectRoot, testRoot, specBase
|
|
|
209
210
|
|
|
210
211
|
/** 账本路径(per-change 单文件) */
|
|
211
212
|
export function testLedgerPath(runtimeRoot, changeName) {
|
|
212
|
-
return
|
|
213
|
+
return runtimeStatePath(runtimeRoot, `test-ledger-${changeName}.json`)
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
/**
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* 刻意区分)。指纹已含 commands 段原文——配置 smoke 前后指纹自动变化,代码未变不重跑。
|
|
28
28
|
*/
|
|
29
29
|
import { createHash } from 'node:crypto'
|
|
30
|
+
import { runtimeStatePath } from '../runtime-paths.js'
|
|
30
31
|
import { execSync, spawnSync } from 'node:child_process'
|
|
31
32
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
32
33
|
import { join, dirname } from 'node:path'
|
|
@@ -43,7 +44,7 @@ const RECORD_SCHEMA_VERSION = 1
|
|
|
43
44
|
|
|
44
45
|
export function qualityScanRecordPath(specBase, changeName) {
|
|
45
46
|
if (!changeName) return null
|
|
46
|
-
return join(specBase, '.runtime', `verify-quality-scan-${changeName}.json`)
|
|
47
|
+
return runtimeStatePath(join(specBase, '.runtime'), `verify-quality-scan-${changeName}.json`)
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/**
|
|
@@ -365,7 +366,7 @@ export async function renderVerifyTestAttribution({ cwd, changeName, specBase, r
|
|
|
365
366
|
// 第二判据(坑 derived-artifact-stale-baseline / verify-attr2-foreign-wip-misdirection):
|
|
366
367
|
// 完整语义见 gates.js filterStaleBaselineOverlap 注释——两道过滤(必须 dirty + 剔除他者声明集)。
|
|
367
368
|
try {
|
|
368
|
-
const pathspecFile =
|
|
369
|
+
const pathspecFile = runtimeStatePath(runtimeRoot, `apply-pathspec-${changeName}.txt`)
|
|
369
370
|
if (changeName && existsSync(pathspecFile)) {
|
|
370
371
|
const ownFiles = new Set(readFileSync(pathspecFile, 'utf8').split('\n').map((l) => l.trim()).filter(Boolean).map((f) => f.replace(/\\/g, '/')))
|
|
371
372
|
if (ownFiles.size > 0) {
|
|
@@ -655,14 +656,19 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
|
|
|
655
656
|
} else {
|
|
656
657
|
try {
|
|
657
658
|
const { createVerifyGateSnapshot } = await import('./gate-snapshot.js')
|
|
658
|
-
|
|
659
|
+
const snapshotFailRef = {} // FR-04:结构化失败原因(pnpm 布局拒建/文件集三分成因/junction/基建异常)
|
|
660
|
+
snap = await createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts, failRef: snapshotFailRef })
|
|
659
661
|
if (snap) {
|
|
660
662
|
console.log(`🧪 noAI 扫描隔离快照(根:${snap.snapshotRoot}):HEAD + 本变更 ${snap.changeFileCount} 个文件${snap.sourceRoot ? '(overlay 自 worktree——本变更分支内容定向跑)' : ''}(并行会话脏文件不参与判定)`)
|
|
661
663
|
} else {
|
|
662
|
-
// FR-02(2026-10-09-verify-reuse-friction):null 回退与异常回退同等可见 +
|
|
664
|
+
// FR-02(2026-10-09-verify-reuse-friction):null 回退与异常回退同等可见 + 记账;
|
|
665
|
+
// FR-04(2026-10-16-fix-downstream-tool-defects):reason 消费 failRef 结构化原因,不再笼统
|
|
663
666
|
try {
|
|
664
667
|
const { reportGateSnapshotFallback } = await import('./gate-snapshot.js')
|
|
665
|
-
|
|
668
|
+
const fbReason = snapshotFailRef && snapshotFailRef.reason
|
|
669
|
+
? `createVerifyGateSnapshot 返回 null:${snapshotFailRef.reason}`
|
|
670
|
+
: 'createVerifyGateSnapshot 返回 null(变更文件集为空 / 快照基建不可用)'
|
|
671
|
+
if (typeof reportGateSnapshotFallback === 'function') await reportGateSnapshotFallback({ cwd, changeName, platformOpts, where: 'noAI 质量扫描步', reason: fbReason })
|
|
666
672
|
} catch { /* 报告失败不改变回退行为(含测试 mock 模块缺导出的形态) */ }
|
|
667
673
|
}
|
|
668
674
|
} catch (e) {
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime-paths.js — .runtime 布局分层(state/ 与 temp/)与旧布局一次性迁移。
|
|
3
|
+
*
|
|
4
|
+
* 背景(2026-10-18 用户反馈):.runtime 根目录堆积 1649 个条目——22 类按 change 命名
|
|
5
|
+
* 等 24 类散文件不在归档回收名单、日志/db 备份与核心
|
|
6
|
+
* 状态混放。分野沿用 runtime-hygiene.js 的定界思路,按「删除是否安全」切两层:
|
|
7
|
+
* - **state/**:按 change 命名的派生状态(watcher 事件流、draft ledger、冲突标记…)。
|
|
8
|
+
* 活跃变更在读,归档/删除后无读者——由 pruneArchivedChangeRuntime 精确回收。
|
|
9
|
+
* - **temp/**:可随时整体删除的产物(watcher.log / spec-sync-bg.log / audit.log /
|
|
10
|
+
* db 备份 .bak|.corrupt-*|.pre-import-* / 锁残留 *.tmp)。无不可再生的状态语义。
|
|
11
|
+
*
|
|
12
|
+
* 家族表是唯一清单源(写入点路径助手 + 归档回收精确名 + 迁移前缀匹配三方共源,
|
|
13
|
+
* 防三处清单漂移)。契约:
|
|
14
|
+
* - 迁移幂等:目标已存在则跳过(不覆盖——混版本窗口防旧进程迟到文件盖掉新文件);
|
|
15
|
+
* 逐条 try/catch fail-open,绝不抛(卫生动作不阻断主流程,与 runtime-hygiene 同族)。
|
|
16
|
+
* - 迁移只动已知家族与已知临时产物,未知文件一律不动(用户/工具自置文件无归属语义)。
|
|
17
|
+
* - 文件名一概保持原样,仅目录前缀变化——写入点改动=一行 join 换助手,读写面零重命名。
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, mkdirSync, readdirSync, renameSync } from 'fs'
|
|
20
|
+
import { join } from 'path'
|
|
21
|
+
|
|
22
|
+
export const RUNTIME_STATE_DIRNAME = 'state'
|
|
23
|
+
export const RUNTIME_TEMP_DIRNAME = 'temp'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* state/ 家族表:prefix 用于迁移匹配与回收名生成;ext 是文件名后缀;
|
|
27
|
+
* stages 特例仅 current-stage-review-run-id(无扩展名且内嵌 stage 枚举
|
|
28
|
+
* brainstorm|plan|execute——stage-review.js stageReviewMarkerPath 的构造形态)。
|
|
29
|
+
*/
|
|
30
|
+
const STATE_FAMILIES = [
|
|
31
|
+
{ prefix: 'sync-docs-lastpush-', ext: '.json' },
|
|
32
|
+
{ prefix: 'draft-ledger-', ext: '.json' },
|
|
33
|
+
{ prefix: 'route-hindsight-baseline-', ext: '.json' },
|
|
34
|
+
{ prefix: 'watcher-events-', ext: '.jsonl' },
|
|
35
|
+
{ prefix: 'watcher-stage-timing-', ext: '.json' },
|
|
36
|
+
{ prefix: 'watcher-last-snapshot-', ext: '.json' },
|
|
37
|
+
{ prefix: 'test-rerun-', ext: '.json' },
|
|
38
|
+
{ prefix: 'test-ledger-', ext: '.json' },
|
|
39
|
+
{ prefix: 'ceremony-tier-', ext: '.json' },
|
|
40
|
+
{ prefix: 'stage-session-ledger-', ext: '.json' },
|
|
41
|
+
{ prefix: 'wave-session-ledger-', ext: '.json' },
|
|
42
|
+
{ prefix: 'verify-draft-', ext: '.json' },
|
|
43
|
+
{ prefix: 'verify-quality-scan-', ext: '.json' },
|
|
44
|
+
{ prefix: 'verify-result-hwm-', ext: '.json' },
|
|
45
|
+
{ prefix: 'review-dispatch-', ext: '.json' },
|
|
46
|
+
{ prefix: 'review-dispatch-shadow-', ext: '.json' },
|
|
47
|
+
{ prefix: 'execute-cleanup-', ext: '.json' },
|
|
48
|
+
{ prefix: 'friction-tally-', ext: '.json' },
|
|
49
|
+
{ prefix: 'prompt-inject-', ext: '.json' },
|
|
50
|
+
{ prefix: 'sync-conflict-', ext: '.json' },
|
|
51
|
+
{ prefix: 'spec-sync-conflict-', ext: '.json' },
|
|
52
|
+
{ prefix: 'apply-pathspec-', ext: '.txt' },
|
|
53
|
+
{ prefix: 'scope-audit-', ext: '.json' },
|
|
54
|
+
{ prefix: 'scan-refresh-', ext: '.json' },
|
|
55
|
+
{ prefix: 'current-execute-run-id-', ext: '' },
|
|
56
|
+
{ prefix: 'current-stage-review-run-id-', ext: '', stages: ['brainstorm', 'plan', 'execute'] },
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 全局面单例(无 change 归属但有读者)→ state/(2026-10-18 二轮收敛:根目录只留
|
|
61
|
+
* db/锁/会话身份/global/spec-version/user-inputs 六件核心)。迁移精确名匹配。
|
|
62
|
+
*/
|
|
63
|
+
export const RUNTIME_STATE_SINGLETONS = [
|
|
64
|
+
'friction-ledger.json',
|
|
65
|
+
'deleted-changes.jsonl',
|
|
66
|
+
'active-gate-snapshots.json',
|
|
67
|
+
'scan-guard.json',
|
|
68
|
+
'verify-lint-tally.json',
|
|
69
|
+
'decision-hits.json',
|
|
70
|
+
'knowledge-hits.jsonl',
|
|
71
|
+
'knowledge-hit-report.json',
|
|
72
|
+
'vector-capability.json',
|
|
73
|
+
'spec-sync-base.json',
|
|
74
|
+
'spec-sync-last-success.json',
|
|
75
|
+
'spec-sync-fail-streak.json',
|
|
76
|
+
'spec-sync-follow-reported.json',
|
|
77
|
+
'sync-noise-self-heal.json',
|
|
78
|
+
'last-delta.json',
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
/** 可再生全局单例产物(删后可重建、无不可再生状态)→ temp/。 */
|
|
82
|
+
export const RUNTIME_TEMP_SINGLETONS = [
|
|
83
|
+
'doctor-diagnosis.json',
|
|
84
|
+
'flow-telemetry.jsonl',
|
|
85
|
+
'write-audit.jsonl',
|
|
86
|
+
'anon-session-warn.json',
|
|
87
|
+
'selfref-pointer-warn.json',
|
|
88
|
+
'auto-pull-throttle.json',
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
/** temp/ 侧已知产物:精确名 + 带时间戳/pid 尾的正则(corrupt/pre-import/lock-tmp)。 */
|
|
92
|
+
const TEMP_EXACT = new Set(['watcher.log', 'spec-sync-bg.log', 'audit.log', 'sillyspec.db.bak'])
|
|
93
|
+
const TEMP_PATTERNS = [
|
|
94
|
+
/^sillyspec\.db\.corrupt-/,
|
|
95
|
+
/^sillyspec\.db\.pre-import-/,
|
|
96
|
+
/^watcher\.lock\.\d+\.tmp$/,
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* state/ 下某变更的全家族精确文件名清单(归档/删除回收用;禁 glob 的精确名形态)。
|
|
101
|
+
* @param {string} changeName
|
|
102
|
+
* @returns {string[]}
|
|
103
|
+
*/
|
|
104
|
+
export function runtimeStateFileNames(changeName) {
|
|
105
|
+
const names = []
|
|
106
|
+
for (const f of STATE_FAMILIES) {
|
|
107
|
+
if (f.stages) {
|
|
108
|
+
for (const s of f.stages) names.push(`${f.prefix}${s}-${changeName}`)
|
|
109
|
+
} else {
|
|
110
|
+
names.push(`${f.prefix}${changeName}${f.ext}`)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return names
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 路径助手自带目录保证(mkdir -p 语义,幂等 fail-open):库级直调(测试/embedded)不经
|
|
118
|
+
* CLI 入口迁移,裸 writeFileSync/appendFileSync 落 state|temp 会 ENOENT——写点分散逐个补
|
|
119
|
+
* mkdir 是打地鼠(2026-10-18 实证四起),在唯一路径构造点收口。读检查顺带建空目录无害
|
|
120
|
+
* (init 预建面与入口迁移本就常态建这两层)。
|
|
121
|
+
*/
|
|
122
|
+
function _pathWithDir(runtimeRoot, dirname_, filename) {
|
|
123
|
+
const p = join(runtimeRoot, dirname_, filename)
|
|
124
|
+
try { mkdirSync(join(runtimeRoot, dirname_), { recursive: true }) } catch { /* fail-open:写入侧自抛真实原因 */ }
|
|
125
|
+
return p
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** @param {string} runtimeRoot @param {string} filename @returns {string} */
|
|
129
|
+
export function runtimeStatePath(runtimeRoot, filename) {
|
|
130
|
+
return _pathWithDir(runtimeRoot, RUNTIME_STATE_DIRNAME, filename)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @param {string} runtimeRoot @param {string} filename @returns {string} */
|
|
134
|
+
export function runtimeTempPath(runtimeRoot, filename) {
|
|
135
|
+
return _pathWithDir(runtimeRoot, RUNTIME_TEMP_DIRNAME, filename)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function _isTempArtifact(name) {
|
|
139
|
+
return TEMP_EXACT.has(name) || TEMP_PATTERNS.some((re) => re.test(name))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 旧布局一次性迁移:把 .runtime 根目录下已知家族文件移动到 state/ 或 temp/。
|
|
144
|
+
* 幂等(目标存在跳过)、fail-open(单条失败/目录缺失零副作用)、未知文件不动。
|
|
145
|
+
* @param {string} runtimeRoot .runtime 绝对路径
|
|
146
|
+
* @returns {{ stateMoved: number, tempMoved: number, skipped: number }}
|
|
147
|
+
*/
|
|
148
|
+
export function migrateRuntimeLayout(runtimeRoot) {
|
|
149
|
+
const out = { stateMoved: 0, tempMoved: 0, skipped: 0 }
|
|
150
|
+
let entries
|
|
151
|
+
try {
|
|
152
|
+
entries = readdirSync(runtimeRoot, { withFileTypes: true })
|
|
153
|
+
} catch {
|
|
154
|
+
return out // 目录缺失(该仓库从未跑过流程)= 无可迁
|
|
155
|
+
}
|
|
156
|
+
const prefixes = STATE_FAMILIES.map((f) => f.prefix)
|
|
157
|
+
const stateSingletons = new Set(RUNTIME_STATE_SINGLETONS)
|
|
158
|
+
const tempSingletons = new Set(RUNTIME_TEMP_SINGLETONS)
|
|
159
|
+
const moves = [] // { name, dest }
|
|
160
|
+
for (const e of entries) {
|
|
161
|
+
if (!e.isFile()) continue
|
|
162
|
+
const name = e.name
|
|
163
|
+
if (prefixes.some((p) => name.startsWith(p)) || stateSingletons.has(name)) moves.push({ name, dest: RUNTIME_STATE_DIRNAME })
|
|
164
|
+
else if (_isTempArtifact(name) || tempSingletons.has(name)) moves.push({ name, dest: RUNTIME_TEMP_DIRNAME })
|
|
165
|
+
// 未知归属:不动(用户/工具自置文件无生命周期语义,迁移不猜)
|
|
166
|
+
}
|
|
167
|
+
// 无条件预建两层目录(幂等廉价):CLI 入口路径保证 state/ 在一切写入前就位——
|
|
168
|
+
// 新仓首跑无文件可搬,靠这里兜住「迁移零动作、写入点裸写 ENOENT」的窗口
|
|
169
|
+
for (const d of [RUNTIME_STATE_DIRNAME, RUNTIME_TEMP_DIRNAME]) {
|
|
170
|
+
try { mkdirSync(join(runtimeRoot, d), { recursive: true }) } catch { /* fail-open */ }
|
|
171
|
+
}
|
|
172
|
+
const madeDirs = new Set()
|
|
173
|
+
for (const { name, dest } of moves) {
|
|
174
|
+
const dst = join(runtimeRoot, dest, name)
|
|
175
|
+
try {
|
|
176
|
+
if (existsSync(dst)) { out.skipped++; continue } // 不覆盖:防旧进程迟到文件盖新文件
|
|
177
|
+
if (!madeDirs.has(dest)) {
|
|
178
|
+
mkdirSync(join(runtimeRoot, dest), { recursive: true }) // 并发 mkdir recursive 不抛
|
|
179
|
+
madeDirs.add(dest)
|
|
180
|
+
}
|
|
181
|
+
renameSync(join(runtimeRoot, name), dst) // 同卷原子;跨进程竞争失败即跳过下次再收
|
|
182
|
+
if (dest === RUNTIME_STATE_DIRNAME) out.stateMoved++
|
|
183
|
+
else out.tempMoved++
|
|
184
|
+
} catch {
|
|
185
|
+
out.skipped++
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out
|
|
189
|
+
}
|