Seed 播种声明(INV-SEED-01)

wesgine 中 CellSpec 的 Cron / Email 声明是"出生声明"——只在 Cell 首次 boot 播种,此后存储是唯一权威。


设计原则

CellSpec.Cron / CellSpec.Email
    ↓
首次 boot → 播种进 wes_cron_jobs / wes_email_configs
    ↓
此后存储是唯一权威,声明不再被读

原缺陷:两件外衣同一个 bug

Cron:删除复活

裸 INSERT 播种。用户运行时删掉的任务,下次 boot 复活——让 INSERT 冲突的那行没了。

"删除"静默地只等于"到重启为止"。

Email:密码被覆盖

upsert 播种。运行时改的密码被声明覆盖回去且哪里都不报错。


标记表 wes_spec_seed

CREATE TABLE IF NOT EXISTS wes_spec_seed (
    kind TEXT PRIMARY KEY,
    seeded_at TEXT NOT NULL
);

为什么同层

标记表与它守护的行同层(state.db)。

分层会让一次快照回滚把行退回而标记留在前面,Cell 从此"已播种但空"。

空声明也打标

标记在探测成功后无条件写,含声明为空。

按 len(spec)>0 加门会让没声明 cron 的 Cell 永不打标,几个月后编辑 CellSpec 就播种进用户手工维护的存储。

探测失败不写标记

把决定权留给下一次 boot。


IMAP 收信注册

IMAP 收信注册与温度 imapWarmnessCount 一律读存储:

// ✅ 读存储
accounts, _ := emailStore.ListAccounts(ctx)

// ❌ 读声明(会让 UI 新增的账号重启后收不到信)
accounts := spec.Email

仅探测失败时退回声明作估计(偏多 = 钉住 Warm,安全一侧)。


配额判点随之拆开

MaxCronJobs / MaxEmailAccounts 曾在 Cell.Start 拿 len(spec) 校验。

而声明首次 boot 后既不再被读、也没有 API 能改,于是两个方向都错:

方向问题
声明 3 条而存储 50 条照样通过
把任务全删光的 Cell永远起不来

现在:

时机校验
创建时validateSeedDeclarationCaps 拒绝装不下自己的声明
运行时cap 落在唯一写入点 cron.Scheduler.Add / email.Store.PutAccount

Channels / MCPs 对比

特性Cron / EmailChannels / MCPs
有运行时存储?✅❌
权威源存储spec
播种?是(一次性)否(spec 就是唯一副本)

两种形状都是单一真相。坏的是第二份持久副本。

但 cap 不因此留在 Cell.Start——因为集合就是 spec,越界写入持久化成功后下次重启才拒绝启动,判点同样下沉到唯一增长点(INV-QUOTA-05)。


反模式

禁止正确做法
每次 boot 无条件从声明插入标记表控制,只播种一次
按 len(spec) > 0 控制是否打标空声明也打标
IMAP 注册读声明读存储,失败退回声明估计
配额在 Cell.Start 校验 len(spec)创建时 validate + 运行时唯一写入点
标记表与数据分层同层(state.db),防快照回滚裂脑