cs-bots 0.1.0 → 0.7.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/CHANGELOG.md +533 -0
- package/README.md +65 -10
- package/docs/01-/346/236/266/346/236/204/346/226/207/346/241/243.md +364 -0
- package/docs/02-/351/234/200/346/261/202/350/247/204/346/240/274/344/271/246.md +284 -0
- package/docs/03-/350/257/246/347/273/206/350/256/276/350/256/241.md +567 -0
- package/docs/04-/346/223/215/344/275/234/346/211/213/345/206/214.md +569 -0
- package/docs/ARCHITECTURE.md +277 -0
- package/docs/adr/ADR-001-/351/241/265/347/255/276/347/273/223/346/236/204/345/233/236/345/210/260/344/270/203/351/241/265.md +70 -0
- package/docs/steps/step-01-/345/216/237/346/236/266/346/236/204/345/244/215/345/216/237/344/270/216IA/350/257/212/346/226/255.md +80 -0
- package/docs/steps/step-02-/351/241/265/347/255/276/346/224/266/346/225/233/344/270/216/345/215/207/347/272/247/351/200/232/351/201/223.md +51 -0
- package/docs/steps/step-03-/346/236/266/346/236/204/350/265/204/346/226/231/346/210/220/346/226/207.md +16 -0
- package/docs/steps/step-04-/344/270/212/347/272/277/344/270/216/351/252/214/350/257/201.md +46 -0
- package/docs//344/272/244/344/273/230/350/257/264/346/230/216-v0.2.0.md +95 -0
- package/docs//346/223/215/344/275/234/346/211/213/345/206/214-v1.md +509 -0
- package/docs//350/277/201/347/247/273/346/226/271/346/241/210-v1.md +64 -0
- package/docs//351/225/277/346/234/237/350/256/260/345/277/206/346/226/271/346/241/210-v1.md +85 -0
- package/package.json +4 -2
- package/src/admission.js +365 -0
- package/src/api.js +4097 -420
- package/src/board.js +883 -14
- package/src/botprofile.js +190 -3
- package/src/client.js +6596 -1082
- package/src/cycle.js +516 -0
- package/src/delivery-watch.js +255 -0
- package/src/dirbrowse.js +20 -0
- package/src/discipline.js +1188 -0
- package/src/fence.js +142 -0
- package/src/index.js +387 -4
- package/src/kernel.js +636 -24
- package/src/library.js +1224 -11
- package/src/memory.js +617 -37
- package/src/metrics.js +339 -0
- package/src/prompts.js +1318 -0
- package/src/reviewer.js +709 -0
- package/src/room.js +655 -14
- package/src/roster.js +346 -26
- package/src/routing/complexity.js +320 -0
- package/src/routing/hints.js +707 -0
- package/src/scratch.js +477 -0
- package/src/settings.js +97 -0
- package/src/stage.js +263 -0
- package/src/store.js +133 -1
- package/src/subscription.js +571 -0
- package/src/suggest.js +1203 -0
- package/src/teams.js +527 -0
- package/src/todo.js +1301 -0
- package/src/tools.js +734 -10
- package/src/verify.js +356 -0
package/src/verify.js
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cs-bots VerifyService —— PDCA 检查面的证据自动化。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:`board.complete` 只要求 `artifacts` 非空,**没人检查产出是否真实**。
|
|
5
|
+
* 「我完成了」不能当验收依据 —— 本模块让验收带上**机器证据**(文件存在 / 命令退出码 /
|
|
6
|
+
* 测试套件结果),人点采纳时看到的是有出处的结果,而不是一句声明。
|
|
7
|
+
*
|
|
8
|
+
* 安全约束(本模块最重要的性质):
|
|
9
|
+
* - **命令白名单**:`kind:'command'` 只允许固定命令名(bash/node/npm/pnpm/npx/git/ls/test/python3),
|
|
10
|
+
* 不在白名单一律抛 `VERIFY_CMD_NOT_ALLOWED`;
|
|
11
|
+
* - **绝不用 shell**:一律 `execFile`(参数数组),杜绝命令注入;
|
|
12
|
+
* - **超时保护**:缺省 120s,超时返回 `{ok:false, evidence:{error:'timeout'}}`;
|
|
13
|
+
* - **任何执行异常都转成可读结果**,绝不抛出去让上层崩(参数校验错误除外)。
|
|
14
|
+
*
|
|
15
|
+
* 不变量:事件流真相源 / 只追加 / 绝不静默失败。
|
|
16
|
+
* 依赖面:node 内置 + `./store.js` + `./kernel.js`。零第三方依赖。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execFile } from 'node:child_process'
|
|
20
|
+
import { existsSync, statSync } from 'node:fs'
|
|
21
|
+
import { resolve } from 'node:path'
|
|
22
|
+
|
|
23
|
+
import { appendJsonl, readJsonl, statePath, writeJsonAtomic } from './store.js'
|
|
24
|
+
|
|
25
|
+
let kernel = null
|
|
26
|
+
let kernelError = null
|
|
27
|
+
try {
|
|
28
|
+
kernel = await import('./kernel.js')
|
|
29
|
+
} catch (error) {
|
|
30
|
+
kernelError = error
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 命令白名单(`kind:'command'` 的 cmd 第一段必须在此列)。 */
|
|
34
|
+
export const CMD_WHITELIST = Object.freeze([
|
|
35
|
+
'bash', 'node', 'npm', 'pnpm', 'npx', 'git', 'ls', 'test', 'python3',
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
/** 检查种类。 */
|
|
39
|
+
export const CHECK_KINDS = Object.freeze(['file', 'command', 'test'])
|
|
40
|
+
|
|
41
|
+
/** 缺省超时(ms)。 */
|
|
42
|
+
const DEFAULT_TIMEOUT_MS = 120_000
|
|
43
|
+
|
|
44
|
+
/** 结构化错误。 */
|
|
45
|
+
export class VerifyError extends Error {
|
|
46
|
+
constructor(message, code = 'VERIFY_ERROR', details) {
|
|
47
|
+
super(message)
|
|
48
|
+
this.name = 'VerifyError'
|
|
49
|
+
this.code = code
|
|
50
|
+
if (details !== undefined) this.details = details
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function clone(value) {
|
|
55
|
+
if (value === undefined || value === null) return value
|
|
56
|
+
return JSON.parse(JSON.stringify(value))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeNow(now, where) {
|
|
60
|
+
if (now === undefined || now === null) return Date.now()
|
|
61
|
+
if (typeof now !== 'number' || !Number.isFinite(now)) {
|
|
62
|
+
throw new VerifyError(
|
|
63
|
+
`verify.${where}:now 必须是有限数字,收到 ${JSON.stringify(now)}`,
|
|
64
|
+
'VERIFY_BAD_FIELD', { where, field: 'now' },
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
return now
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function requireString(value, field, where) {
|
|
71
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
72
|
+
throw new VerifyError(
|
|
73
|
+
`verify.${where}:${field} 必须是非空字符串,收到 ${JSON.stringify(value)}`,
|
|
74
|
+
'VERIFY_BAD_FIELD', { where, field },
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
return value.trim()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveStateDir(config) {
|
|
81
|
+
if (typeof config?.stateDir === 'string' && config.stateDir.trim() !== '') return config.stateDir.trim()
|
|
82
|
+
if (kernel && typeof kernel.stateDir === 'function') return kernel.stateDir()
|
|
83
|
+
const reason = kernelError?.message ?? 'kernel.stateDir 不存在'
|
|
84
|
+
throw new VerifyError(
|
|
85
|
+
`verify.createVerify:既未提供 config.stateDir,也无法从 kernel.js 取得 stateDir(${reason})。`,
|
|
86
|
+
'VERIFY_NO_STATE_DIR',
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function splitArgs(ctx, config) {
|
|
91
|
+
const cfgGiven = config && typeof config === 'object' && Object.keys(config).length > 0
|
|
92
|
+
if (cfgGiven) return { context: ctx ?? {}, config }
|
|
93
|
+
if (ctx && typeof ctx === 'object' && typeof ctx.stateDir === 'string') return { context: {}, config: ctx }
|
|
94
|
+
return { context: ctx ?? {}, config: config ?? {} }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 执行一条命令(白名单 + 无 shell + 超时)。
|
|
99
|
+
* @returns {Promise<{exitCode:number, stdout:string, stderr:string, error?:string}>}
|
|
100
|
+
*/
|
|
101
|
+
function runCommand(cmd, cwd, timeoutMs) {
|
|
102
|
+
return new Promise((resolvePromise) => {
|
|
103
|
+
const file = String(cmd[0])
|
|
104
|
+
if (!CMD_WHITELIST.includes(file)) {
|
|
105
|
+
resolvePromise({ exitCode: -1, stdout: '', stderr: '', error: `command not allowed: ${file}` })
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
const args = cmd.slice(1)
|
|
109
|
+
let settled = false
|
|
110
|
+
const done = (out) => { if (!settled) { settled = true; resolvePromise(out) } }
|
|
111
|
+
try {
|
|
112
|
+
execFile(file, args, { cwd, timeout: timeoutMs, maxBuffer: 8 * 1024 * 1024, shell: false }, (error, stdout, stderr) => {
|
|
113
|
+
if (error && (error.killed || error.signal === 'SIGTERM')) {
|
|
114
|
+
done({ exitCode: -1, stdout: String(stdout ?? ''), stderr: String(stderr ?? ''), error: 'timeout' })
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
done({ exitCode: error ? (typeof error.code === 'number' ? error.code : -1) : 0, stdout: String(stdout ?? ''), stderr: String(stderr ?? '') })
|
|
118
|
+
})
|
|
119
|
+
} catch (e) {
|
|
120
|
+
done({ exitCode: -1, stdout: '', stderr: '', error: String(e?.message ?? e) })
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 创建验收证据服务。
|
|
127
|
+
* @param {{ logger?: { info?: Function, warn?: Function } }} [ctx]
|
|
128
|
+
* @param {{ stateDir?: string, name?: string }} [config]
|
|
129
|
+
* @returns {object} verify service
|
|
130
|
+
*/
|
|
131
|
+
export function createVerify(ctx, config = {}) {
|
|
132
|
+
const { context, config: cfg } = splitArgs(ctx, config)
|
|
133
|
+
const stateDir = resolveStateDir(cfg)
|
|
134
|
+
|
|
135
|
+
const jsonlPath = statePath(stateDir, 'verify.jsonl')
|
|
136
|
+
const snapPath = statePath(stateDir, 'verify.snap.json')
|
|
137
|
+
|
|
138
|
+
const info = (...args) => { try { context?.logger?.info?.(...args) } catch { /* */ } }
|
|
139
|
+
const warn = (...args) => { try { context?.logger?.warn?.(...args) } catch { /* */ } }
|
|
140
|
+
|
|
141
|
+
const entries = new Map()
|
|
142
|
+
const events = []
|
|
143
|
+
let idCounter = 0
|
|
144
|
+
let disposed = false
|
|
145
|
+
|
|
146
|
+
const diagnostics = {
|
|
147
|
+
stateDir, jsonlPath, snapPath,
|
|
148
|
+
kernel: kernel ? 'loaded' : `load-failed:${kernelError?.name ?? 'unknown'}`,
|
|
149
|
+
hydratedFrom: null, replayedEvents: 0, badLines: 0,
|
|
150
|
+
snapshotWriteFailures: 0, lastReplayAt: undefined,
|
|
151
|
+
checksRun: 0, checksFailed: 0,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function assertAlive(where) {
|
|
155
|
+
if (disposed) {
|
|
156
|
+
throw new VerifyError(
|
|
157
|
+
`verify.${where}:实例已 dispose(),请重新 createVerify()(拒绝在已销毁实例上静默返回空数据)`,
|
|
158
|
+
'VERIFY_DISPOSED', { where },
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function snapshot() {
|
|
164
|
+
return { version: 1, savedAt: Date.now(), entries: [...entries.values()] }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function commit(action, at, by, before, after) {
|
|
168
|
+
if (!appendJsonl(jsonlPath, { id: after.id, rev: after.rev, action, by, at, before, after })) {
|
|
169
|
+
throw new VerifyError(`verify.${action}:事件追加失败(${jsonlPath})`, 'VERIFY_APPEND_FAILED', { action })
|
|
170
|
+
}
|
|
171
|
+
entries.set(after.id, clone(after))
|
|
172
|
+
events.push({ id: after.id, rev: after.rev, action, by, at, before: before ? clone(before) : null, after: clone(after) })
|
|
173
|
+
if (!writeJsonAtomic(snapPath, snapshot())) {
|
|
174
|
+
diagnostics.snapshotWriteFailures += 1
|
|
175
|
+
warn(`[cs-bots/verify] 快照写入失败(${snapPath})——事件流已落盘,replay() 可恢复`)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function hydrate() {
|
|
180
|
+
entries.clear(); events.length = 0; idCounter = 0
|
|
181
|
+
const { rows, bad } = readJsonl(jsonlPath)
|
|
182
|
+
diagnostics.badLines = bad
|
|
183
|
+
diagnostics.replayedEvents = rows.length
|
|
184
|
+
if (bad > 0) warn(`[cs-bots/verify] ${jsonlPath} 有 ${bad} 行无法解析——已跳过,请核对`)
|
|
185
|
+
if (rows.length === 0) { diagnostics.hydratedFrom = 'empty'; return }
|
|
186
|
+
let skipped = 0
|
|
187
|
+
for (const ev of rows) {
|
|
188
|
+
if (!ev || typeof ev.id !== 'string' || !ev.after) { skipped += 1; continue }
|
|
189
|
+
const current = entries.get(ev.id)
|
|
190
|
+
if (current && Number.isFinite(ev.rev) && ev.rev <= current.rev) { skipped += 1; continue }
|
|
191
|
+
entries.set(ev.id, clone(ev.after)); events.push(clone(ev))
|
|
192
|
+
const m = /^v-(\d+)$/.exec(ev.id)
|
|
193
|
+
if (m) idCounter = Math.max(idCounter, Number(m[1]))
|
|
194
|
+
}
|
|
195
|
+
if (skipped > 0) warn(`[cs-bots/verify] 回放跳过 ${skipped} 条非递增/残缺事件`)
|
|
196
|
+
diagnostics.hydratedFrom = 'jsonl'
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── 单条检查 ──────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 执行一条声明式检查。
|
|
203
|
+
* @param {object} spec
|
|
204
|
+
* @returns {Promise<object>}
|
|
205
|
+
*/
|
|
206
|
+
async function check(spec = {}) {
|
|
207
|
+
assertAlive('check')
|
|
208
|
+
const started = Date.now()
|
|
209
|
+
const kind = spec?.kind
|
|
210
|
+
if (!CHECK_KINDS.includes(kind)) {
|
|
211
|
+
throw new VerifyError(
|
|
212
|
+
`verify.check:kind=${JSON.stringify(kind)} 非法(合法:${CHECK_KINDS.join(' | ')})`,
|
|
213
|
+
'VERIFY_BAD_KIND', { kind },
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
const by = spec?.by ?? 'system'
|
|
217
|
+
const at = normalizeNow(spec?.now, 'check')
|
|
218
|
+
|
|
219
|
+
// ── 参数校验(这些是可以抛的)──
|
|
220
|
+
let evidence
|
|
221
|
+
if (kind === 'file') {
|
|
222
|
+
const path = requireString(spec.path, 'path', 'check')
|
|
223
|
+
const expected = spec.expected ?? 'exists'
|
|
224
|
+
if (!['exists', 'absent', 'nonempty'].includes(expected)) {
|
|
225
|
+
throw new VerifyError(`verify.check:expected 非法(${JSON.stringify(expected)})`, 'VERIFY_BAD_FIELD', { field: 'expected' })
|
|
226
|
+
}
|
|
227
|
+
let exists = false
|
|
228
|
+
let size = 0
|
|
229
|
+
try {
|
|
230
|
+
if (existsSync(resolve(path))) { exists = true; size = statSync(resolve(path)).size }
|
|
231
|
+
} catch { exists = false }
|
|
232
|
+
let ok
|
|
233
|
+
if (expected === 'exists') ok = exists
|
|
234
|
+
else if (expected === 'absent') ok = !exists
|
|
235
|
+
else ok = exists && size > 0
|
|
236
|
+
evidence = { path, expected, exists, size, ok }
|
|
237
|
+
} else if (kind === 'command') {
|
|
238
|
+
if (!Array.isArray(spec.cmd) || spec.cmd.length === 0 || typeof spec.cmd[0] !== 'string') {
|
|
239
|
+
throw new VerifyError(`verify.check:cmd 必须是字符串数组(execFile 参数,禁用 shell)`, 'VERIFY_BAD_CMD', { field: 'cmd' })
|
|
240
|
+
}
|
|
241
|
+
if (!CMD_WHITELIST.includes(spec.cmd[0])) {
|
|
242
|
+
throw new VerifyError(
|
|
243
|
+
`verify.check:命令 ${JSON.stringify(spec.cmd[0])} 不在白名单(${CMD_WHITELIST.join('|')})——拒绝执行`,
|
|
244
|
+
'VERIFY_CMD_NOT_ALLOWED', { cmd: spec.cmd[0], whitelist: CMD_WHITELIST },
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
const out = await runCommand(spec.cmd, spec.cwd ?? undefined, spec.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
248
|
+
evidence = { cmd: spec.cmd, exitCode: out.exitCode, stdout: out.stdout, stderr: out.stderr, error: out.error }
|
|
249
|
+
// expectExit 指定时**替换**「必须 0 退出」的判定(否则「期望退出码 2」永远不可能成立)。
|
|
250
|
+
let ok = out.error === 'timeout' ? false : out.exitCode === 0
|
|
251
|
+
if (spec.expectExit !== undefined && spec.expectExit !== null) {
|
|
252
|
+
ok = out.error === 'timeout' ? false : out.exitCode === spec.expectExit
|
|
253
|
+
}
|
|
254
|
+
if (spec.expectOutput !== undefined && spec.expectOutput !== null) ok = ok && out.stdout.includes(spec.expectOutput)
|
|
255
|
+
evidence.ok = ok
|
|
256
|
+
} else { // test
|
|
257
|
+
if (!spec.cwd) {
|
|
258
|
+
throw new VerifyError(`verify.check:kind='test' 必须显式提供 cwd(拒绝在任意目录跑测试)`, 'VERIFY_BAD_FIELD', { field: 'cwd' })
|
|
259
|
+
}
|
|
260
|
+
const suite = spec.suite ?? 'test/run-all.sh'
|
|
261
|
+
// ⚠️ 不用 `bash -c <string>`:那等于给任意 shell 注入开口,
|
|
262
|
+
// 与「禁用 shell」的安全约束直接冲突。`suite` 只允许**脚本路径**(`bash <path>`),
|
|
263
|
+
// 参数不含代码 → 不存在注入面。要跑自定义命令请用 kind:'command'(走白名单 + execFile)。
|
|
264
|
+
const out = await runCommand(['bash', suite], spec.cwd, spec.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
265
|
+
evidence = { suite, cwd: spec.cwd, exitCode: out.exitCode, stdout: out.stdout, stderr: out.stderr, error: out.error }
|
|
266
|
+
evidence.ok = out.exitCode === 0
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const durationMs = Date.now() - started
|
|
270
|
+
idCounter += 1
|
|
271
|
+
const id = `v-${idCounter}`
|
|
272
|
+
const rec = { id, rev: 1, kind, spec: clone(spec), result: evidence, at, by, durationMs, createdAt: at, updatedAt: at }
|
|
273
|
+
commit('check', at, by, null, rec)
|
|
274
|
+
diagnostics.checksRun += 1
|
|
275
|
+
if (!evidence.ok) diagnostics.checksFailed += 1
|
|
276
|
+
return clone(rec)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── 批量 ──────────────────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
/** 对一组路径做存在性检查(同步,不执行命令)。 */
|
|
282
|
+
function evaluate(artifacts, options = {}) {
|
|
283
|
+
assertAlive('evaluate')
|
|
284
|
+
const list = Array.isArray(artifacts) ? artifacts : []
|
|
285
|
+
const results = list.map((p) => {
|
|
286
|
+
const abs = resolve(String(p))
|
|
287
|
+
let exists = false, size = 0
|
|
288
|
+
try { if (existsSync(abs)) { exists = true; size = statSync(abs).size } } catch { exists = false }
|
|
289
|
+
return { path: String(p), exists, size, ok: exists }
|
|
290
|
+
})
|
|
291
|
+
const passed = results.filter((r) => r.ok).length
|
|
292
|
+
return { ok: passed === results.length && results.length > 0, results, passed, failed: results.length - passed }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 校验一个任务的产出:artifacts 逐个存在 + writeScopes 存在性检查。
|
|
297
|
+
* @param {{ id: string, artifacts?: string[], writeScopes?: string[] }} task
|
|
298
|
+
*/
|
|
299
|
+
function verifyTask(task = {}, options = {}) {
|
|
300
|
+
assertAlive('verifyTask')
|
|
301
|
+
const by = options?.by ?? task?.by ?? 'system'
|
|
302
|
+
const at = normalizeNow(options?.now, 'verifyTask')
|
|
303
|
+
const artifactCheck = evaluate(task.artifacts ?? [], options)
|
|
304
|
+
const scopes = Array.isArray(task.writeScopes) ? task.writeScopes : []
|
|
305
|
+
const scopeCheck = scopes.map((s) => {
|
|
306
|
+
const abs = resolve(String(s))
|
|
307
|
+
let exists = false
|
|
308
|
+
try { if (existsSync(abs)) exists = true } catch { exists = false }
|
|
309
|
+
return { scope: String(s), exists, ok: exists }
|
|
310
|
+
})
|
|
311
|
+
const scopeOk = scopeCheck.every((r) => r.ok)
|
|
312
|
+
const ok = artifactCheck.ok && (scopes.length === 0 || scopeOk)
|
|
313
|
+
idCounter += 1
|
|
314
|
+
const id = `v-${idCounter}`
|
|
315
|
+
const rec = {
|
|
316
|
+
id, rev: 1, kind: 'task', taskId: task.id ?? null,
|
|
317
|
+
result: { ok, artifactCheck, scopeCheck }, at, by, durationMs: 0,
|
|
318
|
+
createdAt: at, updatedAt: at,
|
|
319
|
+
}
|
|
320
|
+
commit('verifyTask', at, by, null, rec)
|
|
321
|
+
diagnostics.checksRun += 1
|
|
322
|
+
if (!ok) diagnostics.checksFailed += 1
|
|
323
|
+
return clone(rec)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function history(limit = 50) {
|
|
327
|
+
assertAlive('history')
|
|
328
|
+
return clone([...entries.values()].sort((a, b) => b.at - a.at).slice(0, limit))
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function stats() {
|
|
332
|
+
const byKind = {}
|
|
333
|
+
for (const r of entries.values()) byKind[r.kind] = (byKind[r.kind] ?? 0) + 1
|
|
334
|
+
return { total: entries.size, byKind, events: events.length, diagnostics: clone(diagnostics) }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function replay() { hydrate(); diagnostics.lastReplayAt = Date.now(); return { from: diagnostics.hydratedFrom, events: events.length, bad: diagnostics.badLines } }
|
|
338
|
+
function dispose() { disposed = true }
|
|
339
|
+
|
|
340
|
+
hydrate()
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
name: cfg.name ?? 'verify',
|
|
344
|
+
paths: Object.freeze({ stateDir, jsonl: jsonlPath, snapshot: snapPath }),
|
|
345
|
+
check,
|
|
346
|
+
evaluate,
|
|
347
|
+
verifyTask,
|
|
348
|
+
history,
|
|
349
|
+
stats,
|
|
350
|
+
replay,
|
|
351
|
+
diagnostics: () => clone(diagnostics),
|
|
352
|
+
dispose,
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export default createVerify
|