Constitution / Tech Stack 层。本文件只写契约(系统必须 / 不能 / 调用方式是什么),不写现状清单。 Mission 见
mission.md;阶段化路线见roadmap.md;当前实际在跑的东西(表、adapter、CLI、路由)见../SPEC.md;运行时拓扑见../RUNTIME.md。
状态:v7(锁定候选)。v7 升级点:§4.2 LLM 调用契约闭包化——task / scope 改为 prism/pipeline/llm_tasks.py 定义的 StrEnum 闭集(Task 11 项 + Scope 5 项),prism/pipeline/llm.py 的 call_llm / call_llm_json wrapper 必须拒绝 isinstance(task, Task) is False 的调用(运行时 TypeError),tests/test_llm_tagging.py AST 扫全仓强制每个调用点都带 task=;wrapper 同时向 omlx-sdk tags dict 合并 {"task", "scope", "source_key"},token-tracker 据此聚合耗费。v6 内容保留。
v6(2026-04-23):应用 Codex CLI 评审修复:§2.1 新增
prism/privacy/outbound.py::send()运行时出域闸(消除 §9 TW3 仅依赖 grep 静态扫的单点漏洞);§3.2 扩展SourceCandidateschema 至(id, source_key, source_type, evidence_json, similarity, status, gate_status, generator, created_at)并强制落source_candidates_draft表;§5.2 加quality-scan静态检查(web 层不得 import 非 Identity ranker、不得读ranking_shadow)+promote命令必须验证 mission §5 五要素契约与 NN6 gate artifact 已就位;§6 将"候选源 / 推荐理由 / 相似度分数进入候选池或 UI"明确纳入"上线"并归 Mid gate,自动写sources.yaml归 High gate;§7.2 新增外部平台身份命名规范(external_author_id/platform_account_id,禁用user_id);新增 §11 数据生命周期与备份契约回应 Codex 盲点(SQLite 膨胀 / WAL / 备份恢复 / 模型产物存储宪章化)。
以下技术选择写进宪章——改动需回到本节辩论:
- 语言 / 框架:Python 3.12 + FastAPI + Jinja2 + HTMX + vanilla CSS
- 前端无构建工具:不引入 node / webpack / npm;模板层直出 HTML + HTMX 即是整个前端
- 数据库:SQLite 单文件(
data/prism.sqlite3),schema 由prism/db.py::init_db()单入口统一定义 - 本地 LLM:通过
omlx-sdk调用 omlx-manager,不走 raw HTTP - 云 LLM:Anthropic 走
localhost:8100token tracker proxy(独立计量通路) - 调度:macOS launchd(不引入 celery / airflow / cron)
- 部署:Mac Studio 本机,单机 7×24,不做多机 / 不上云
不接受的替代:Postgres / Redis / Node 前端 / Docker / Kubernetes / 云托管 DB——这些偏离 NN2(本地算力上限)或 NN7(拒绝多用户 / 多租户)。
- Schema 单入口:所有表定义在
prism/db.py::init_db();CLI 或任意代码不得 inlineCREATE TABLE - SQLite 之外的状态都是脏点:文件系统状态(如抓取中间产物)可用,但业务状态必须落 DB
- 降频不删除(NN4):源和历史内容永不物理删除,只能下调优先级 / 拉长抓取时间窗口(见 §3),或设
enabled=false;cleanup操作仅针对明确过期的中间态(日志、队列完成态),不得触及源目录或内容表 sources.yaml是源目录的唯一 source of truth,DB 的sources表只存运行时状态(失败计数、last_sync_at 等),不存配置;两者冲突时以 YAML 为准- 审计表必须存在且不可被业务路径跳过:
job_runs:所有 pipeline 跑的流水decision_log:所有自动决策(调源权重、优先级调整、算力上限告警处置等)必须落此表
- 偏好数据不入 git(NN3):
preference_weights/ 画像 snapshot / 学到的排序向量 / 可反推个人偏好的模型产物(embedding 字典、特征模板、微调权重、prompt 模板中的偏好片段)仅存data/或models/;两目录.gitignore强制排除;禁止以 fixture / seed / 示例数据形式把任一类偏好产物提交进 repo - 外部出域白名单(NN3 + TW3 落地):任何把 DB 或
data/内容发往 prism 外部的通路(错误上报 / 云日志 / 公开 dashboard / 第三方 embedding API 等)必须登记在下方 §2.1 出域清单并显式 opt-in;默认禁止带prism/personalize/*或preference_*前缀的字段出域
本节是所有"prism → 外部"数据通路的唯一登记处。新增通路 = 先登记再上线。
| 通路 | 方向 | 允许字段 | 禁止字段 | 开关 |
|---|---|---|---|---|
| Anthropic API(云 LLM) | 出 | 对话 messages、系统 prompt | 任何 personalize/* / preference_* 字段 |
.env ANTHROPIC_API_KEY |
| (将来)错误上报 / 云日志 | 出 | 堆栈、异常类型 | 请求体、DB 行内容、偏好字段 | 默认关,启用需改本表 |
| (将来)公开 dashboard | 出 | 聚合计数(按源维度) | 个体内容、偏好字段 | 默认关 |
运行时出域闸:所有外部调用必须经过 prism/privacy/outbound.py::send(channel, payload) 统一入口;该函数:
- 根据
channel查本表登记项;未登记 channel → raise +decision_log(action='outbound_denied') - 按"禁止字段"列表对
payload做结构化过滤,命中personalize/*/preference_*前缀即阻断 - 成功出域写
outbound_log(channel, bytes_out, ts, hash)审计表
直接调 httpx / requests / 云 SDK 绕过此函数 = 违约。quality-scan 静态扫仍是第二道防线(检 import / grep 字段名),运行时闸是第一道。未在表中登记的出域通路视为违约,TW3 必须同时命中静态扫与运行时闸。
- 所有 adapter 实现
prism/sources/base.py::SourceAdapterProtocol - 新增源类型时必须提供 adapter;不允许在 pipeline 里 inline 处理单一源的特例逻辑
sources.yamlCRUD 必须经过prism/sources/yaml_editor.py;不允许其他地方直接写 YAML- 源的活跃度由"优先级 + 抓取时间窗口"表达(高优先级 → 高频 / 短窗口),不存在显式"active / cold / 淘汰"状态;优先级或时间窗口的自动变更必须落
decision_log - 降频不删除(NN4):reject / 失效 / 长期低胜率的源只下调优先级、拉长时间窗口、或
enabled=false,不从sources.yaml物理删除
mission §5 定义"就位" = 五要素闭环。tech-stack 层对每个要素的接口契约:
| mission §5 要素 | tech-stack 契约 |
|---|---|
| 候选源生成 | 至少一个 prism/sources/candidates/ 下的 generator(LLM-from-profile / follow-graph-expand / external-ref-backtrack 任一),输出 SourceCandidate 结构写入 source_candidates_draft 表 |
| 质量理由 | SourceCandidate.evidence_json 必填,机器可读(JSON),包含内容采样 + 与已有源的偏好相似度或关注图谱路径 |
| 作者 approve/reject | CLI prism source candidate approve/reject <id> 或 Web UI 按钮;不得以 git commit 或口头视为 approve;approve 才 promote 到 sources.yaml |
decision_log 记录 |
每次 approve/reject 写一条 decision_log(layer='source_selection', action='approve'|'reject', context_json=SourceCandidate) |
| 降频不删除 | reject 的候选源 status='rejected' 保留在 source_candidates_draft,不物理删除;重新评估时可再次生成同 source_key 的新候选 |
SourceCandidate schema(契约):
@dataclass
class SourceCandidate:
id: str # UUID,候选池主键
source_key: str # 未来 sources.yaml 的 key,如 "x:some_author"
source_type: str # adapter 类型,如 "x" / "hn" / "arxiv"
evidence_json: dict # 质量理由(采样 + 相似度 / 图谱路径),机器可读
similarity: float # 与既有偏好画像的量化相似度 [0, 1]
status: str # "pending" / "approved" / "rejected"
gate_status: str # "draft" / "shadow" / "live"(对齐 §6 gate matrix)
generator: str # 生成器名("llm_from_profile" / "follow_graph" / ...)
created_at: datetime字段缺任一 = schema 违约;id 必须存在才能走 approve/reject CLI(否则无法定位)。候选源必须先落 source_candidates_draft 表,approve 后才写入 sources.yaml(draft state 模型,见 §6.2)。
五要素全部以契约落地 = mission §5 "就位";任一缺失 = 未就位,mission §8 推荐引擎 shadow-only 约束持续生效。
采集(sync)→ 聚合(cluster)→ 分析(analyze)→ 交付(briefing/publish)必须是独立 CLI 阶段;不允许合并成单一"全跑"命令。每阶段幂等(重跑不产生重复数据)。
所有本地推理必须走 omlx-sdk,不得 raw HTTP 打 :8002/:8003。所有 prism 内部调用必须经 prism/pipeline/llm.py 的 call_llm / call_llm_json wrapper,不得在业务代码里直接 import omlx_sdk——wrapper 是唯一打标入口。
调用契约:
from prism.pipeline.llm import call_llm_json
from prism.pipeline.llm_tasks import Task, Scope
result = call_llm_json(
prompt,
system=SYSTEM_PROMPT,
intent="reasoning", # 可选,默认按任务挑
task=Task.STRUCTURIZE, # 必填,StrEnum 闭集
scope=Scope.ITEM, # 必填,StrEnum 闭集
source_key="x:karpathy", # 可选,出处定位
max_tokens=4096,
)Wrapper 职责(prism/pipeline/llm.py):
task/scope参数强类型 StrEnum;非 Enum 传入 →TypeError- 向
omlx-sdkchat(...)透传tags={"task": task.value, "scope": scope.value, "source_key": ...};project字段保留传task.value以保持 token-tracker 仪表盘兼容 - 必传
caller="prism"、session_id(字符串化的job_runs.id)
闭集枚举(source of truth:prism/pipeline/llm_tasks.py):
| 维度 | 成员 | 新增流程 |
|---|---|---|
Task |
translate / asr / ocr / video_transcribe / summarize / polish / structurize / extract / classify / judge / source_probe |
需改 llm_tasks.py + 更新此表 + 前端 i18n 映射 |
Scope |
item / cluster / daily / source_profile / corpus |
同上 |
显示名走 llm_tasks.DISPLAY_NAMES_ZH(翻译 / 语音转文字 / 图像转文字 / 视频转文字 / 摘要 / 文章加工 / 结构化 / 字段抽取 / 打标/分诊 / 质量门禁 / 源探测 / 单篇 / 簇级 / 日级 / 作者画像 / 全量)。token-tracker 仪表盘不得直接展示英文 slug,必须经 DISPLAY_NAMES_ZH 查表。
Intent 选择准则(调用方按场景自选,具体任务清单见 SPEC.md):
| intent | 选用条件 |
|---|---|
fast |
短输入 / 高吞吐 / 廉价过滤 |
reasoning |
需 CoT、深读、跨 cluster 综合、结构化抽取 |
coding |
代码生成场景 |
default |
不挑、一次性脚本 |
vision |
多模态 |
回归保险:tests/test_llm_tagging.py AST 扫 prism/ 全仓,任何 call_llm(...) / call_llm_json(...) 缺 task= kwarg 即 CI 红。新增调用点若不走 wrapper(例如 slides horse race 直接拿 OmlxClient)必须在此处或 §4.3 列为显式例外。
显式 model= 的合法例外:多模型对比场景(如 slides horse race),本就是拿具体模型做横向比较——仍须经 wrapper,只是 intent 让位给 model。
不走 omlx-sdk 的合法例外:
- Anthropic 云调用 →
localhost:8100token tracker proxy - ASR(目前 omlx-sdk 未包) → 保持直连,待 SDK 扩展后迁移
翻译 / 结构化 / analyze-expand 等积压型任务由 idle-aware drain worker 驱动,不依赖 cron 定时切片。 约束:omlx busy(被 Claude Code 或其他 caller 占用)时让出;idle 时连续消费积压。
- 所有偏好相关代码必须在
prism/personalize/目录下,与 core pipeline 解耦 - Core pipeline(sync / cluster / analyze / articlize / briefing)不得直接 import
personalize/ - Personalize 层通过显式的 ReRanker Protocol接入 web 层;未来替换偏好模型(新推荐引擎 / embedding 重排 / 无偏好 pass-through)= 只改
personalize/,零 core diff - 偏好数据与衍生模型产物不入 git:范围与禁令见 §2(含 embedding / 特征字典 / 微调权重 / prompt 偏好片段)
- Web 层涉及"偏好结果呈现"的路由(
/feed//feed/following//creator/*等)默认不公开:部署公开站点时必须显式 opt-in 白名单,否则走 auth gate
Mission §8 强制:§5 "就位" 未满足前,偏好推荐引擎只能 offline + shadow,不得影响可见排序。tech-stack 落地:
- ReRanker Protocol 必须支持两种模式:
mode='live':输出进入/feed可见排序——仅在 mission §5 五要素闭环且推荐引擎通过 NN6 门禁后才允许激活mode='shadow':输出仅落ranking_shadow表(产物不出现在任何 UI 路径),与mode='live'的当前 ranker 并行运行,供离线对比评估
- 默认 mode:
IdentityReRanker(按时间倒排)是唯一默认 live;任何非 identity 的 ReRanker 初始注册必须以mode='shadow'上线 - 切 live 的唯一通道:通过
prism personalize promote <ranker_name>CLI 显式切换;该命令必须在执行前验证以下全部前置,任一不满足即 exit non-zero:- mission §5 五要素全部以
source_candidates_draft+decision_log(layer='source_selection')存在证据(近 30 天内有 approve/reject 流水) - 目标 ranker 已实现 §6.3 一键失败标记入口 + §6.4
gate_config.py阈值声明(NN6 gate artifact) ranking_shadow中该 ranker 与当前 live ranker 的离线对比报告已产出且作者签字(decision_log(action='personalize_promote_approved'))
- mission §5 五要素全部以
- 误切防护(静态检查):
quality-scan必须扫描prism/web/下源码,命中以下任一模式即阻断发布:from prism.personalize或import prism.personalize.*(非PersonalizeRegistry的 import)- 直接查询
ranking_shadow表 - 硬编码 ranker 类名绕过
PersonalizeRegistry.get_live()
- 误切防护(运行时):
web/routes.py渲染路径调用 ReRanker 时,只认PersonalizeRegistry.get_live();shadow ranker 物理不可达 web 层 - Shadow 对比评估:
prism personalize evalCLI 读ranking_shadow与 live 产物的差异,产出报告,不自动切换
"上线" = 任何自动化写入用户可见路径的行为,包括但不限于:
- LLM 产物进入 UI(翻译
body_zh、tl;dr、结构化文章、briefing 段落、推荐理由) - Source 自动上下线 / 优先级 / 时间窗口变更影响 feed 组成
- Pipeline 自动决策导致 cluster 归并 / 主题标签 / 交叉链接等出现在 UI
- 候选源 / 推荐理由 / 相似度分数进入候选池或 UI 展示:自动 generator 将
SourceCandidate写入source_candidates_draft并暴露给作者的候选审查界面——属于自动化写入用户可见路径,至少受 Mid gate(见 §6.6) - 自动写入
sources.yaml:无论是否经候选池中转,自动修改sources.yaml的行为属于 High gate,必须留人工复核窗口 + 可回滚
每一类"上线"动作必须满足:
- 显式质量 gate:产物落主表前经过质量检查(置信度阈值 / LLM-as-judge / 异常模式检测 / schema 校验)——形式按类型定,但必须存在
- 不过关必须隔离:gate 失败的产物进入待复核状态或回滚,不得进入可见路径;严禁 "gate 失败 fallback 到原文直出" 这种静默降级
- 失败必落日志:gate 失败必须写
quality_anomalies(异常事实)+decision_log(若导致自动决策) - 热路径不得绕过门禁:为响应速度 / UI 流畅度跳过 gate 属于违约;慢就慢,不能静默污染
- Source 级决策(自动上下线 / 优先级变更)必须留人工复核窗口:自动决策不立即生效,而是先写
decision_logpending 态,留短窗口允许作者回滚
所有新 pipeline 上线前,必须实现以下两条之一:
- Draft state:产物先写
*_draft表 /status='draft'字段,gate 通过才 promote 到主路径可读;主路径查询默认过滤 draft - Shadow DB:pipeline 先产出到 shadow 表,与主表并行存在,gate 或人工 approve 后再切换指针
禁止:产物直接 UPSERT 到主表然后"如果质量低再删"——这是静默污染模式。
每条 pipeline 的产物在 UI 上必须提供一键"标记为污染"入口(按钮 / 快捷键 / Agent CLI 命令任一)。标记后:
- 立即回撤该产物出主路径(改
status='rejected'或删除) - 写
quality_anomalies含marked_by='author'+pipeline+product_id - 计入 TW4 滚动窗口统计(2 周内累计 ≥3 次或同类 ≥2 次触发 pipeline 暂停;单次污染只回撤产物,不暂停 pipeline)
没有该入口的 pipeline 不得上线——这是 NN6 在 tech-stack 层的硬前置。
每条 pipeline 必须在 prism/pipeline/<name>/gate_config.py 或等价位置声明:
max_consecutive_failures:连续 N 次 gate 失败 → 该 pipeline 自动降级为 staging 模式(产物只进 draft,不 promote)recovery_condition:恢复自动 promote 的条件(人工 approve M 次 / 修复 commit 后重置)
阈值可选激进或保守,但必须存在且被 gate 逻辑读取。无配置视为未上线。
紧急情况下(如 pipeline 阻塞日常使用)允许临时绕过门禁直出,但:
- 必须写
docs/reviews/adhoc/YYYY-MM-DD-nn6-override-<pipeline>.md,说明产物范围、原因、回滚计划、预计恢复时间 - 同时写
decision_log含action='nn6_override'+ 指向该文件 - 连续两次未记录的绕过(通过 diff / log 审计发现)即触发 mission §9 重评
| 风险等级 | 典型产物 | 最低门禁要求 |
|---|---|---|
| Low | 翻译 body_zh |
schema 校验 + 长度 / 语言检测 + draft state |
| Mid | 结构化摘要 / tl;dr / 观点抽取 |
LLM-as-judge 评分阈值 + 关键字段非空 + draft state + 一键标记 |
| High | 推荐排序 / source 自动上下线 / 交叉链接断言 | 多信号一致性 + 人工复核窗口 + 可回滚 + shadow 表或 pending 态 |
- 公开路由白名单在
prism/web/routes.py::_PUBLIC_PATHS,其他路由必须经_get_user()401 gate - 任何改状态的操作前端后端都要 gate(模板
is_anonymous+ 路由_get_user),单侧 gate 不合规 - 新路由 URL 使用语义 slug(
source_key/ 自然 key),不用数字 ID - 反馈表单用 HTML
<form>+ hidden input,不用hx-valsJSON
NN7 不是道德宣言,是架构红线。tech-stack 层禁止以下结构出现,即使只是"为未来留接口":
- 不实现用户注册 / 登录 / session 以外的身份体系:当前单人使用通过简单 auth cookie,不引入
users表、OAuth、SSO、JWT claims - 不引入租户模型:DB schema 不得出现
tenant_id/org_id/workspace_id等多租户字段;所有表默认"单用户全局" - 不做资源配额 / 速率隔离:omlx 配额、算力预算、抓取频率按项目级全局管理,不按用户切片
- 不做权限系统:没有 role / permission / ACL;"作者"是唯一身份,其他访问路径只能是"匿名只读公开白名单"
- issue / PR 默认拒绝触发条件:任何 PR 引入上述任一结构(grep
tenant_id、user_id非 session 用途、@permission_required装饰器等)即自动违约,不需个案讨论
外部平台身份命名规范:外部内容平台的作者 / 账号标识必须使用 external_author_id / platform_account_id / external_handle 等明确外部语义的字段名;禁用 user_id / account_id 等无限定前缀命名——后者会被 NN7 违约扫(quality-scan grep \buser_id\b)误判为本地用户体系。外部平台身份是"内容属性"而非"本地账号",命名必须体现这一区分。
这些约束使得"多用户化"不再是产品决策,而是需要先推翻 tech-stack 契约——NN7 的真实防线在于此。
- 存在一组只读 CLI 命令供 Agent(Claude Code 等)读取 feed / 订阅 / 源列表;这些命令保证不产生副作用
- 只读命令的稳定性等同于宪章——命名和输出 schema 改动需 mission 级理由
- 写操作(
source add/remove、sources prune等)不属于 Agent 接口范畴
- 使用熔断(mission TW1):作者连续 14 天未访问
/feed/following→ 暂停 pipeline 调度;恢复需手动 - 算力上限熔断(mission TW2):推理队列积压 > 72h 必须触发告警(CLI exit non-zero 或 quality-scan 异常),不得静默降质
- 隐私外流熔断(mission TW3):
quality-scan必须检查 git diff / 外部出域日志 / Sentry payload 是否包含prism/personalize/*或preference_*字段;命中立即阻断发布并 exit non-zero - 污染事后扳机(mission TW4):
quality_anomalies中marked_by='author'的事件按 pipeline 在 2 周滚动窗口内聚合:- 单次污染:立即回撤该产物(
status='rejected'),不暂停 pipeline - 累计 ≥3 次 或 同类 ≥2 次:自动将该 pipeline 切到 staging 模式(只写 draft),恢复需人工 approve
- High 风险管线(推荐排序 / source 自动上下线 / 交叉链接断言)可在
gate_config.py声明更严阈值(如 ≥1 次即暂停),但不得全局收紧到低于 mission TW4 的基线
- 单次污染:立即回撤该产物(
quality-scan是健康度唯一入口;其他管线不得内嵌"静默修复"逻辑绕过异常表
- 所有定时任务通过 macOS launchd plist 注册,plist 放
prism/scheduling/ - 每个定时任务入口必须写
job_runs起止和 stats;不写job_runs= 不合规调度 - launchd 以外的后台循环(drain worker 等)视为"长期运行服务",由独立 plist 管理 + 健康探针
本节回应"本地 7×24 长运行下最大失效模式不是功能 bug,而是 DB 膨胀 / 损坏导致档案库不可恢复"的宪章级风险。NN4 "降频不删除" 在没有容量和备份契约时会变成自毁前提。
- 单文件上限软约束:
data/prism.sqlite3超过 10 GiB 时,每日quality-scan必须 exit 警告(非阻断),触发作者决策(归档 / 切片 / 升级);超过 30 GiB 时 exit non-zero 阻断 pipeline 新写入(只读模式),待人工处置 - WAL 必须启用:
PRAGMA journal_mode=WAL,由init_db()统一设置;wal文件大小 > 1 GiB 视为异常(通常意味着有长事务或 checkpoint 失败),quality-scan告警 - 膨胀源可追溯:每月自动运行
prism db size-report(由 launchd 触发),按表输出行数 + 字节数 + 增长率,写入decision_log(layer='data_lifecycle');作者据此决定哪张表需要归档
- 每日备份必须存在:
data/prism.sqlite3每日至少一份完整备份(SQLite.backup或VACUUM INTO),落backup/YYYY-MM-DD/目录;launchd plist 注册,失败两日连续即quality-scan阻断 pipeline - 备份必须异盘 / 异机:备份目录不得与
data/在同一物理卷;单盘故障即全毁 = 违约 - 至少保留 14 天滚动 + 4 周月末全量:超出窗口可清理,但月末全量强制保留;清理必须写
decision_log - 恢复演练契约:每季度至少一次
prism db restore-dry-run,挂载上周备份到临时路径验证 schema + 核心表可读,结果写decision_log(action='restore_rehearsal');连续两季度未演练 = 触发 mission §9 重评(备份不演练等于无备份) - 备份同受 NN3 约束:备份文件含偏好数据时禁止出域(不上云盘 / 不入 git / 不同步到外部服务);
§2.1 运行时出域闸必须拦截备份目录路径
- 偏好模型、embedding 字典、微调权重等落
models/,总大小软上限 20 GiB,超出quality-scan告警 - 每个模型产物必须有
models/<name>/META.yaml,记录:生成时间 / 数据切片范围 / 上游 job_run_id / 预期保留周期 - 无 META.yaml 的产物视为"孤儿",
quality-scan告警;超过保留周期未使用自动归档至models/_archive/(仍受 NN3 不出域约束)
以下任一条件命中即触发 mission §9 TW(按严重度归 TW2 或独立新 trip-wire,由下次 mission 评审决定归属):
- 备份连续 3 天失败
- 季度恢复演练连续两次缺失
- SQLite 文件损坏(
PRAGMA integrity_check非ok)
具体表清单、adapter 清单、CLI 清单、路由清单见 ../SPEC.md——那里会标记每条的活/僵尸/冻结状态。