import type { AskState } from "../approval-ask-machine.js"; import type { AskRow, AskTerminalClaimIntent, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, DecideResult, EnsureAskResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js"; /** 数据根下的目录名。**按店命名**,与兄弟 File 店同规(`approval-nonces` / `approval-ask-audit` / * `approval-exemptions` / `resume-anchors` / `checkpoint-ctx` / …)—— 首版写的是族名 `approvals`,而那 * 正是读者会去找**持久 park 行**的地方(它们其实在 `checkpoint-ctx`),且持久数据面的名字按硬 breaking * 三句是「不能靠编译红通知」的那一面,改名只在落地前廉价(替补对抗复审 F8,采纳)。 */ export declare const APPROVAL_ASK_DIR = "approval-asks"; /** 账本文件名。 */ export declare const APPROVAL_ASK_JOURNAL_FILE = "asks.jsonl"; /** * 压实的**下界**:记录数不到这个数,压实的 I/O 不值得付(整本重写 + fsync)。 * 与 {@link ASK_JOURNAL_COMPACT_GROWTH} 一起构成闭形判据,零 env、零启发式。 */ export declare const ASK_JOURNAL_COMPACT_MIN_RECORDS = 2000; /** * 压实的**增长因子**:记录数必须同时超过「活行数 × 本因子」才压实。 * 少了它就是抖动 —— 一个稳定在 1_800 行的部署会在每条记录之后都把整本重写一遍。 */ export declare const ASK_JOURNAL_COMPACT_GROWTH = 2; /** * 账本的**动词闭集**([ref] 形)。运行期的词表判定与编译期的穷尽 `switch` 读同一份: * 往这里加一个词,{@link FileApprovalAskStore.replayOp} 的 switch 立刻编译红(漏一口不可能发布)。 */ declare const JOURNAL_OPS: readonly ["ensureAsk", "transitionAsk", "decideAsk", "claimTerminal", "expireAsk", "bindBatch", "abortBatch", "deferReconcile", "resolveProvisional", "deleteByTask"]; type JournalOpWord = (typeof JOURNAL_OPS)[number]; /** 运行期词表(上面的数组)与类型面动词维({@link JournalCall})的**双向**对表 —— 任一边加词而另一边 * 没跟上,这个别名解析成 `never`,{@link JOURNAL_OP_TABLE_GATE} 的赋值当场编译红。 */ type JournalOpTableIsExhaustive = (JournalCall["op"] extends JournalOpWord ? (JournalOpWord extends JournalCall["op"] ? true : false) : false) extends true ? true : never; /** 上面那条对表的**执法位**(占位常量形,`core-keyset-guard.ts` 的占位元组同精神):运行期无用, * 存在的唯一理由是让「词表与类型面分家」在编译期就红,而不是等到某条记录重放不出来。 */ export declare const JOURNAL_OP_TABLE_GATE: JournalOpTableIsExhaustive; /** 一次写调用的实参(动词维判别式)——盘上记的就是它,重放时原样喂回内核。 */ type JournalCall = { op: "ensureAsk"; row: NewAskRow; } | { op: "transitionAsk"; askId: string; from: AskState; to: AskState; patch: AskTransitionPatch; } | { op: "decideAsk"; askId: string; batchId: string; decision: DecideAskInput; } | { op: "claimTerminal"; askId: string; batchId: string; intent: AskTerminalClaimIntent; } | { op: "expireAsk"; askId: string; batchId: string; } | { op: "bindBatch"; batchId: string; askId: string; gate: BindGateInput; } | { op: "abortBatch"; batchId: string; } | { op: "deferReconcile"; askId: string; expectedState: AskState; expectedRev: number; nowMs: number; } | { op: "resolveProvisional"; askId: string; from: AskState; to: AskState; patch: AskTransitionPatch; } | { op: "deleteByTask"; taskId: string; }; export declare class FileApprovalAskStore implements ApprovalAskStore { /** 语义内核(转移判定的唯一真源)。本类对它只做两件事:重放、代理。 */ private readonly core; private readonly path; private readonly tmpDir; private log; /** 活文件里的记录数(重放读数 + 每次 append,压实后归为快照行数)。压实判据读它。 */ private records; /** * 钉住的钟(见 {@link InMemoryApprovalAskStore} 顶注的读钟纪律)。非 null 的窗口**恒是同步的** —— * 从 append 之后到内核方法返回 promise 为止,其间没有任何 `await` ⇒ 两次并发调用不可能互相看见对方的钉。 */ private pinnedNow; /** 重放期被内核拒掉的记录数(每条都已 warn;读面留给测试与将来的诊断)。 */ private replayRejectedCount; /** 重放期被丢弃的坏行数(撕尾不计:那是本格式存在的理由,由 {@link corruptQuarantinePath} 单独说)。 */ private corruptLineCount; /** 本次启动隔离出去的原件路径(无坏字节 ⇒ `undefined`)。 */ readonly corruptQuarantinePath: string | undefined; /** * boot 期 fail-stop(File 店族同律,`approval-ask-audit-store.ts` 的同段先例):mkdir / 重放失败 * **拒启** —— 一台「审批账写不进却照常发卡」的机器,比起不起来危险得多。 * * @param root local 数据根(= `LocalBackend` 的 `FileStorageBackend.root`,零新 env)。 */ constructor(root: string); /** * 内核的钟。窗口外读 ⇒ **抛**(不静默回落 `Date.now()`):窗口外的读数按定义不在账本上,重放会得出 * 另一个时间戳 —— 那是一条**静默**的重放分岔,而本店的全部价值就是重放等价。今天内核的五个读钟点 * 全在同步前缀里;哪天有人挪到 `await` 之后,这里当场响。 */ private clockRead; private logFor; /** * 一次写:**先 append + fsync,再翻内存**(顶注的崩溃序)。append 与内核调用之间零 `await` ⇒ * 盘上的记录序 == 内存的转移序(内核的写体全在同步前缀里),两副读数永远对得上。 * * 🔴 **内核吃的是「盘上那一份」,不是调用方那一份**(替补对抗复审 F7,采纳):整条调用先过一次 JSON * round-trip,再**把 round-trip 的产物**同时交给账本与内核。不这么做的话,凡是 JSON round-trip 会改形 * 的值(`Date` → ISO 串、函数值键被**静默丢掉**、`undefined` 键消失)都会造出一条真分岔:活路径上内核 * 手里是原对象、重启重放之后是另一份 —— 而这正是本店整个形所依赖的那条等式。首版只为 `updatedInput` * 一格做了这件事(它另有「编出来什么都不是 ⇒ 响亮拒」的三态判据),其余键(`cardJson` / `patch` / * `gate` / `decisionActor`)留了一条没写出来的例外。一条规则覆盖所有键,例外消失。 * 代价如实:不可编码的值(BigInt / 循环引用)在本形上**抛**,而 InMemory 形收得下 —— 那与 SQL 孪生 * 同向(它也存不下),是**介质事实**,写进顶注的边界段。 * * 🔴 **压实失败不许落到调用方头上**(替补对抗复审 F3 [medium],实测后修):走到 `maybeCompact()` 时这 * 一次写**已经 fsync 落盘、也已经翻进内存**,把一次纯管家动作的 I/O 错误抛给调用方 = 把一次已生效的 * 人类批准报成失败(客户端重试会拿到 `ask_not_pending`)。压实是 F 类兜底:失败留痕、下一次写再试。 */ private write; private maybeCompact; /** 整本重写成行快照 + 原子替换。失败 ⇒ 原账本原样留着、`records` 不动(下一次写再试),日志由 * core 的 [ref] 惰性重开接住 —— 一次瞬时 I/O 错误绝不把这条日志变成砖。 */ private compact; private replayRecord; /** 闭集穷尽派发(漏一口 = 编译红,[ref] 形)。返回内核的 promise —— 结果本身在重放期无人消费。 */ private replayOp; ensureAsk(row: NewAskRow): Promise; transitionAsk(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise; /** * 🔴 `updatedInput` 的编码必须在 **append 之前**、且走内核**同一只** {@link encodeDecideUpdatedInput}: * 账本本身是 JSON,而 `JSON.stringify` 对顶层函数是**静默丢键**不是抛 —— 不先编码就会写出一条 * 「没带编辑」的记录,而活路径那一侧是抛。编码抛 ⇒ 账本一个字节没写、内核一个字节没动(与内核自己的 * 「先编码、编码成功才一次性提交」同一条原子性)。 */ decideAsk(askId: string, batchId: string, decision: DecideAskInput): Promise; claimTerminal(askId: string, batchId: string, intent: AskTerminalClaimIntent): Promise; expireAsk(askId: string, batchId: string): Promise; bindBatch(batchId: string, askId: string, gate: BindGateInput): Promise; abortBatch(batchId: string): Promise; deferReconcile(askId: string, expectedState: AskState, expectedRev: number, nowMs: number): Promise; resolveProvisional(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise; deleteByTask(taskId: string): Promise; listByState(state: AskState, limit: number): Promise; listPendingByTask(taskId: string, signal?: AbortSignal): Promise; listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise; getAsk(askId: string): Promise; getByIdempotencyKey(taskId: string, key: string): Promise; getBatch(batchId: string): Promise; /** 重放读数(测试与诊断):被内核拒掉的记录数 / 解析不出的坏行数 / 活文件当前的记录数。 */ replayStats(): { rejected: number; corruptLines: number; records: number; }; /** 释放追加 fd(`LocalBackend.close` → 优雅重启重开)。不包 try/catch 的理由同 * {@link FileApprovalNonceStore.dispose}:core 的 `AppendLog.close()` 自己就是全函数。 */ dispose(): void; } export {}; //# sourceMappingURL=approval-ask-store-file.d.ts.map