iomgaa
|
92785eb3e1
|
feat(stores): 补上易失存储实现,stores 拆成四个文件
design 0003 的否决方案一节承诺过「显式命名的、明确不提供恢复的内存实现」,那个实现一直
没写,migrations/dissect.md 还专门登记着「不要照那句话去找一个不存在的类」。
**名字取 VolatileRunStore 不取「不提供恢复」**:一个真的读不回自己写过的东西的存储过不了
自家的契约套件(套件第一条要的就是「写进去的意图读得回来」),而这一层的准入标准就是那套
套件。一个过不了自家准入标准的实现不该存在。它不提供的是跨进程恢复,Volatile 说的正是这
件事。
桶里存编码后的载荷、读的时候才解码,两个方向的别名都堵上:调用方写完再改自己手里那个 dict
改不到日志,读回来的日志被就地改动也污染不了存储本身。落盘那个实现每次重新解析文件,天然
如此,这个实现靠同一条路径对齐它。**写入只编码不解码**——落盘那边写的时候只做 json.dumps,
一条字段类型不对的记录写得进去、读的时候才炸,两边现在一致。
两张平行的表(记录类→标签、标签→解码器)合成一张三元组再派生视图。加上易失实现要用的第三
张视图之后,三张表之间那个谁也不检查的一致性要求就不可能被违反了。
|
2026-08-27 03:58:29 -04:00 |
|
iomgaa
|
aeb575e0f7
|
fix(stores): 修 Codex 对抗审查报的五条,其中两条同一根因
最实的一条:读取端只要一段能解析成 JSON 就收下,没检查它后面有没有换行。而短写完全可能
正好写完整个 JSON 对象、只差那个换行——那次写从来没被确认过(调用方的 await 还没返回),
按契约就是「没发生」,但它会被当成一条有效的动作意图读回来,恢复据此判成「状态未知」并可能
重放,而那个动作一定没执行过(调用方是在写意图返回之后才去执行的)。
判据改成「这一行有没有被换行终结」,不是「能不能解析」。同一个改动顺带修掉第三条:一行完整
终结的坏行(比如被外部追加的 {})此前会被当成撕裂尾行吞掉,读成「少了一条记录但看起来完整」
的日志;现在终结过的行解不开就是损坏,直接报错。
其余三条:
- 新建日志文件不 fsync 父目录。os.fsync(fd) 刷的是文件内容,刷不到「这个目录里多了一个
文件」这条目录项;掉电后内容可能在而文件不存在,read_log 走「文件不存在」返回空日志,
驱动入口判成全新运行,一次已经花过钱的运行静默没了留痕。只在新建时刷。
- 同一运行标识上的并发写会交错:一条记录可能由不止一次 os.write 写完,而 O_APPEND 只保证
每次 write 的追加位置原子,保证不了一条逻辑行整体原子。按运行标识加锁串起来(不同运行
照样并行),跨进程那一半仍靠独占创建挡。有一条用短写逼出那个窗口的测试。
- 往返测试的 TOTAL_WRITES 是硬编码,而且漏写结束标记它发现不了(恢复会把最后一步之后那次
停止判定重演一遍,得出同样结果)。加一条把十次写的记录类型序列整个钉死的测试。
|
2026-08-10 03:52:35 -04:00 |
|
iomgaa
|
6af289d283
|
feat(stores): 落成逐行追加的日志存储,契约套件第一次真的在跑
design 0011(待确认)定了六条:一次运行一个文件且文件名就是运行标识(不转义不哈希,按标识
去目录里找文件是最自然的用法;标识必须是安全文件名,否则 ../ 会把文件写到目录外面);一行
一条记录加一个 record 类型标签(serialization 编出来的载荷没有元信息键,标签是存储这层加的,
record 从此是保留键);第一条解不开的行就是日志结尾、它后面还有内容就是损坏;fsync 只在
运行开始、两条意图、运行结束四处(其余两处靠前缀持久性兜);写入走 to_thread;运行开始记录
用 O_EXCL 兜住跨进程撞车。
契约套件里那条 test_step_without_an_action_is_still_recorded 转成真断言——它标着 xfail 的
理由是「StepCompleted.result_id 在 0006 里是必填字符串」,而 0006 决策七早就把它改成可为空
并加了不变量。xfail 8→7,跳过 24→14。
原子写「一起不可见」那一半按契约套件的点名在这一层补上了:给实现留一个可注入的故障点
(一个可替换的「把这些字节写进去」),测试把它换成写一半就抛异常,断言那条记录整条不可见。
前缀持久性仍然验不了(掉电才看得出来),继续登记为已知缺口。
调研三条实据写进了 0011:两个下游 fsync 全仓零处(一个的 SQLite 还开着 synchronous=NORMAL),
所以这条比它们都严、代价是每步两次 fsync;一个下游的轨迹检查器同样是「碰到第一条坏行就放弃
整个文件」;另一个下游踩过「文件名少一维导致两个阶段静默互相覆盖」,O_EXCL 把那类静默覆盖
变成显式失败。这几条我自己逐条核过——那份调研的 subagent 承认它编过一句「我抽查过了」。
migrations/dissect.md 登记两条:运行标识要带齐现在文件名里那五维,以及这份意图日志和它那份
逐步轨迹是两样东西不要混。
|
2026-08-10 03:38:31 -04:00 |
|
iomgaa
|
e017160c45
|
build(repo): 落成工具链、依赖契约与十个模块的空骨架
第 ③ 阶段剩下的那半:架构文档之外,import-linter 契约也落地了。
pyproject.toml 把九条依赖规则里的七条写成五条 import-linter 契约。
分层用一条 layers 契约表达规则 1、2、3、8,`|` 表示同层互不 import;
另外四条 forbidden 分别管下游反向 import、polygateway 的唯一入口、
以及三个纯逻辑模块不碰 asyncio 与 pathlib。
契约不是平凡的绿:故意注入两处违规验证过,都被点名到行号。
先建十个模块的空包,是为了避开 PolyGateway bootstrap 期那段 Makefile 门控——
它当时没有包,lint-imports 报 module not found 而红,只好加一段跳过逻辑。
空包让契约从第一天就真的在跑。
剩下两条规则落不进契约,写成了 tests/unit 下的测试:
规则 6「types 与 ports 不许 import 任何第三方」判据要反过来写(只许标准库和自己),
规则 9「import polyloop 之后 sys.modules 里没有 polygateway」是运行时事实。
另加硬约束 §1.1 零业务假设的黑名单扫描——它第一次跑就抓到 _assembly 的 docstring
里写了 dissect 的业务词,已改。三个扫描类测试都带 fail-closed 守卫,
防止目录搬走之后扫到空列表安静地绿。
工程约定取自实验室已有项目:setuptools + src layout、ruff 十一项 select、
line-length 100 来自 PolyGateway;dev 工具链版本钉死、--strict-markers 与
--import-mode=importlib 来自 CHSAnalyzer 与 dissect 踩过的坑,各自的理由写在配置注释里。
e2e 默认不跑,它打真实网关要花钱。
CLAUDE.md §0 那句「契约还没写,要等 src/ 落地」已过期,改掉;
README 勾掉第 ②③ 阶段,补上本地检查命令与 GovDoc-Editor 那一行。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-09 21:28:59 -04:00 |
|