跳到主要内容

M0 走通骨架工程包(Walking Skeleton)

本文档是 M0 实施 Agent 的 task-oriented 消费源。一份文档包含 M0 从启动到验收所需的全部工程材料(接口子集 + Pydantic 模型 + DDL + CLI 规格 + Mock fixture + 凭证正则基线 + audit payload schema + bootstrap 模板 + CI 模板)。

与主架构文档 projects/finbayes/engineering/architecture.md 的关系:本文档不重复战略 / 业务建模 / 系统全景定义,而是把"按主架构 §25 M0 范围实施所需的工程细节"集中到一个 Agent 可一次性 load 的 task-oriented 文件中。引用主架构用 §N 锚点;引用本文档段用本文档内的 §M.N。

frame 护栏(阶段 0 文档矫正 · 2026-06-04):M0 是 agent 的最小切片,不是 agent 的形态定义。FinBayes 产品本体是带状态和自主循环的 agent(架构 §2 identity 级不变量 + 不变量 I-15');M0 故意先不实现循环(single-shot walking skeleton 是合理起步),但实现者必须知道循环是接下来要建的主体,不能把 M0 的 single-shot 形态误当成产品成型。这是金融真智能体骨架蓝图标注的「已知 pre-agent 阶段」——缓解 = 架构 §25 已把最小自主循环切片前移到 M2、并要求 M0 接口预留 agent loop 挂载点(主动信号触发器接口签名即便 stub 也应标注其在 agent 架构中的中心地位)。M0 的 single-shot 形态不得一路延续到后期里程碑。

文档约定

约定说明
章节编号本文档独立编号(§1 起),与主架构 §N 独立。引用主架构必用 主架构 §N 形式
接口签名格式Python 类型 hint + Pydantic v2
代码块语言Python / SQL / YAML / Bash —— 均可直接复制到工程实施仓
状态约定M0 implement = M0 必须实现 / M0 stub = 签名固定但内部抛 NotImplementedError / M0 skip = M0 不实现
字段约定所有 Pydantic 模型含 schema_version 字段(演化策略前置):§3 主体 1.0 系列 = int = 1;§3.5 1.1 最小子集 *M0 系列经 _BaseM0 继承 = Literal["1.1-m0"] = "1.1-m0"

文档结构

内容行数估算
§1M0 范围与 walking skeleton 链路~50
§2M0 接口子集表(6 子系统 × implement/stub/skip)~80
§3StructuredCognitionResult 1.1 最小子集 + M0 核心 Pydantic 模型 v0~200
§3.5StructuredCognitionResult 1.1 最小子集(M0 实施 schema,承接 cognition-1.1-contract.md)~180
§3 末Placeholder 类型节(ClarifyResponse / Chunk / ToolSpec / ToolCategory)~60
§4M0 SQLite DDL v0~80
§5AuditEvent.payload 按 event_type 字段集~120
§6M0 CLI 命令规格~80
§7M0 Mock Provider Fixture 规范~80
§8M0 6 条样例输入(含 1 条条件化方向判断)+ 期望能力清单~90
§9M0 5 类凭证 Negative Test Fixture~60
§10凭证识别正则基线 v0(新增 P0)~150
§11M0 输出判定规则 v0~50
§12工程实施仓发现路径协议~40
§13工程实施仓 Bootstrap 模板(新增 P0)~150
§14M0 三检 Review Gate CI 模板~80
§14.5CI 接口规范(7 个未提供脚本/测试的接口定义)(新增 P0)~140
§14.6M0 baseline 文件位置与格式(新增 P0)~30
§15与主架构 / ADR 的承接~30

§1 M0 范围与 walking skeleton 链路

详见主架构 §25 M0 走通骨架段,本节只 surface 关键决策点:

维度M0 选说明
任务类型即时认知请求(解释类 + 分析类)7 类中最常见 + 最简单链路
市场Crypto数据 Provider 选项最多 / 限制最少
入口CLI onlyTUI / Web / MCP / Channel 在 M0 全部 skip(详见 §2)
状态对象Fin Object + Session(默认 Session)不走候选两步路径(M1+)
LLM 降级仅 L1 用户配置 Provider implement;L1' / L2 / L3 / L4 全部 stub本表为唯一权威,覆盖主架构 §25 早期描述的"L1→L2"(M0 不实现 L2 本地嵌入)
数据 ProviderMock fixture only(M0 真档跑在 nightly)M0 不接入真实价格 / 链上 / 新闻 API

M0 walking skeleton 链路

M0 验收(详见主架构 §25 + 本文档 §14 CI 模板):

检查通过条件
战略保真度本文档 §14 gate-1 全过
契约回归M0 是 baseline,gate-2 仅生成 baseline 不对比(详见 §14.6)
认知质量本文档 §8 6 条样例输入(含 1 条条件化方向判断)+ §11 两层判定规则(schema 闸门 + 能力达成闸门)+ 人工 checklist
边界本文档 §9 5 类凭证样式 100% 拒收 + §10 正则基线测试
审计 trailtask_id 贯穿 + Provider 调用链完整 + §5 audit payload schema 字段齐

§2 M0 接口子集表(6 子系统)

每个子系统的接口分三类:

  • M0 implement:M0 必须可调,含完整实现
  • M0 stub:函数签名固定但内部抛 NotImplementedError 或返回占位值,M1+ 实现
  • M0 skip:M0 完全不实现
子系统M0 implementM0 stubM0 skip
Input / Output Pipelinenormalize_input(raw_input) → NormalizedInputrun_input_boundary_hook(input) → BoundaryResultformat_output_stream(...) → AsyncIterator[Chunk]run_output_boundary_hook(text) → BoundaryResult(凭证样式扫描)format_for_channel(...)format_for_mcp(...)TUI 实时刷新、Web UI projection 转换
Task Orchestrationdispatch_task(input, context) → Taskrun_task(task) → TaskResulttag_task_type(task, llm_result)(事后回填)dispatch_task_group(...)(仅单任务)、run_clarify_loop(...)clarify 工具上线、TaskGroup 并发编排、复盘任务、关注流任务
Evidence + Synthesisbuild_evidence_packet(tool_calls) → EvidencePacketsynthesize(packets, prompt) → StructuredCognitionResultself_consistency_sample(...)(M0 N=1)多采样收敛检查、跨任务证据归并
State Managementappend_audit(event)read_session_context(session_id, limit)get_or_create_default_session(user_id) → Sessionget_or_create_fin_object(canonical_name, market) → FinObjectcreate_candidate(...)confirm_candidate(...)reject_candidate(...)(无候选写入路径)、update_profile_signal(...)Watchlist 增删 / Judgment Record 持久化 / Profile 更新 / 主动信号触发
Capability Registryregister_tool(tool_spec)(启动时注册)、get_tools_for_task(task) → list[ToolSpec]reject_execution_tool(spec)(拒绝执行类)deprecated_tool_path(...)工具退役流程、动态注册、自定义工具上传
Provider Adapter Poolprobe_readiness() → ProviderReadinesscall_llm(messages, tools) → LLMResponse(仅 L1 用户 Provider,OpenAI-compatible)、call_data_tool(tool_name, args) → ToolResponse(仅 Mock)fallback_to_l1_prime(...)fallback_to_l2(...)fallback_to_l3(...)fallback_to_l4(...)probe_local_llm_readiness(...)L2 本地 LLM 加载、L3 缓存/规则、L4 受限菜单、Provider 退役、task_routing 配置热加载、真实数据 Provider 接入

M0 总接口数量:22 个 implement + 12 个 stub = 34 个签名固定的接口。


§3 StructuredCognitionResult 1.1 最小子集 + M0 核心 Pydantic 模型 v0

每个模型含 schema_version: int = 1 字段(1.0 主体);§3.5 的 1.1 最小子集模型 _BaseM0 继承 schema_version: Literal["1.1-m0"] = "1.1-m0"(详见 §3.5),所有 *M0 子类自动继承,无需逐类重复声明。Pydantic v2,禁止 v1 风格的 validator 装饰器。统一 ConfigDict 策略详见 §13 Bootstrap。

与 1.1 契约源的关系:本节定义两层 schema。

  1. §3.1–§3.4:M0 已稳定的 10 要素 1.0 schema(StructuredCognitionResult 主体 + EvidencePacket / Session / FinObject / AuditEvent 等周边模型),M0 必须完整实现。
  2. §3.5:StructuredCognitionResult 1.1 最小子集,对应主架构 §29 锁定的 M0 最小子集字段。1.1 的完整字段定义(17 个 Pydantic 子模型、23 个 enum、12 条字段间约束、与 1.0 兼容性)是契约派生包 StructuredCognitionResult 1.1 契约源 的职责,事实源是 ADR-008 supplement 机制层输出契约扩展本节 §3.5 只列 M0 实施子集 + 留 stub 不实现高级字段,不复述完整契约。

M0 阶段 structured_result_version 已声明 "1.1"(仅声明版本号 + 落地最小子集,而非实现 1.1 全部字段)。1.1 完整字段(correlation_regime / unbalanced_loops / regulation_status.failure_modes 四模 / posterior.prior_family + fit_method / s1.evidence + second_order_branch + convergence_flag / back_triggers / contradictions 等)在 M1–M7 逐步上线,详见契约源 §2 与架构 §29 演进路径。

# src/finbayes/io/types.py
from pydantic import BaseModel, Field
from typing import Literal
from datetime import datetime

class NormalizedInput(BaseModel):
schema_version: int = 1
user_id: str # 第一阶段单用户固定为 "local"(详见 §13)
session_id: str
entry: Literal["cli", "tui", "web", "mcp", "channel"]
raw_text: str
attachments: list[str] = [] # M0 不支持附件,保留字段
timestamp: datetime
task_id: str # 全局唯一,贯穿子系统(uuid4 形式,详见 §13)

class BoundaryResult(BaseModel):
schema_version: int = 1
passed: bool
reject_reason: Literal["credential_detected", "execution_request", "boundary_violation"] | None = None
reject_category: str | None = None # 如 "private_key_pattern" / "openai_api_key_pattern",详见 §10
safe_response: str | None = None # 拒收时给用户的安全回应文案
# src/finbayes/orchestration/types.py
from pydantic import BaseModel
from typing import Literal
from datetime import datetime

class Task(BaseModel):
schema_version: int = 1
task_id: str
session_id: str
user_id: str
input_normalized: "NormalizedInput"
state: Literal["created", "running", "completed", "failed", "cancelled"] = "created"
task_type: str | None = None # 事后回填(主架构 §9 Task Orchestration 事后任务类型标签)
tool_calls: list["ToolCall"] = []
llm_calls: list["LLMCall"] = []
started_at: datetime | None = None
completed_at: datetime | None = None
failure_reason: str | None = None
# ADR-008 supplement §2.7 锁定的 Task schema 元数据(不入 StructuredCognitionResult 本体)。
# MCABucketM0 定义见 §3.5;M0 由意图识别层在 dispatch_task 时填入七轴档位 + bucket_label,
# worst_axis / tag_version 为 Optional(见 §3.5 + milestone-field-evolution-matrix.md)。
mca_bucket: "MCABucketM0 | None" = None

class ToolCall(BaseModel):
schema_version: int = 1
tool_call_id: str
task_id: str
tool_name: str
arguments: dict
response: dict | None = None
failed: bool = False
failure_reason: str | None = None
latency_ms: int | None = None

class LLMCall(BaseModel):
schema_version: int = 1
llm_call_id: str
task_id: str
provider_id: str # 字符串规范:`<provider_type>:<model_id>`,如 "openai-compatible:gpt-4o-mini"
model: str # Provider 端的 model identifier
input_token_count: int
output_token_count: int
estimated_cost: float
latency_ms: int
is_fallback: bool = False # M0 始终 False(L1 only)
failure_reason: str | None = None
# src/finbayes/cognition/types.py
from pydantic import BaseModel, Field
from typing import Literal

class EvidencePacket(BaseModel):
"""单次工具调用归一化后的证据"""
schema_version: int = 1
source: str # 工具名 / Provider id
content: dict # 工具返回的结构化数据
timestamp_collected: str # ISO 8601
freshness_hint: str | None = None # 如 "<30s real-time" / "T+1 daily"
degraded_reason: Literal["timeout", "unauthorized", "rate_limit", "parse_error"] | None = None

class StructuredCognitionResult(BaseModel):
"""综合层输出契约 1.0 主体(10 要素 —— 反方/失效条件不可缺)。

M0 阶段顶层 `structured_result_version` 声明 1.1,1.1 新增 6 顶层字段的 M0 最小子集
见 §3.5 `StructuredCognitionResult11Minimal`,完整 1.1 契约见 cognition-1.1-contract.md。
"""
schema_version: int = 1
# 1.0 主体 10 要素(与 cognition-1.1-contract.md §5 StructuredCognitionResultV10Body 严格对齐)
main_answer: str = Field(..., min_length=30) # 1. 第一屏题眼回应(≥30 字硬约束)
supporting_evidence: list[str] = Field(default_factory=list) # 2. 支持理由
multi_perspectives: list[str] = Field(default_factory=list) # 3. 多视角(M0 stub,留默认空 list;1.1 完整模型上线后由综合层写入)
counter_evidence: list[str] = Field(default_factory=list) # 4. 反方证据(M0 允许空但必须明示理由)
prerequisites: list[str] = Field(default_factory=list) # 5. 成立条件(M0 stub,留默认空 list;1.1 完整模型上线后由综合层写入)
invalidation_conditions: list[str] = Field(..., min_length=1) # 6. 失效条件(M0 必填非空)
uncertainty_and_gaps: list[str] = Field(default_factory=list) # 7. 信息缺口
sources: list[dict] = Field(default_factory=list) # 8. 来源 + 时间戳
follow_up_questions: list[str] = Field(default_factory=list) # 9. 可继续追问项(M0 stub,留默认空 list;1.1 完整模型上线后由综合层写入)
historical_judgment_links: list[str] = Field(default_factory=list) # 10. 历史判断链接(M0 stub,留默认空 list;1.1 完整模型上线后由综合层写入)
non_instruction_notice: str = "本回答是认知材料,不是执行指令。" # 非指令标注
schema_validation_passed: bool = True # 综合层自检字段,输出端 hook 二次校验

§3.5 StructuredCognitionResult 1.1 最小子集(M0 实施 schema)

承接架构 §29「v1 实现路径建议」第 1 条与 ADR-008 supplement §5.2。M0 在 1.0 十要素之上叠加以下 6 个新顶层字段的最小子集;完整 1.1 字段表 / enum 取值 / 字段间约束 / 半人工标注覆盖范围以契约源为准。

M0 实施 vs M0 stub 字段一览

1.1 顶层字段M0 实施子集M0 stub(留 None / 空)
phase_evidenceclocks: list[ClockPhaseM0](仅 clock_id + phase_label + confidence,不含 evidence_ref 全量)phase_matrix
causal_graphnodes + edges(仅主体 id / from_node / to_node / edge_type / path_confidence / translation_lossreverse_forces / endogeneity / correlation_regime / unbalanced_loop_warnings
regulation_status整体留 stub全部字段(M3 / M5 启用后落地)
applicability_flags三支柱 level + reasonevidence_ref
posteriormode_a + mode_bvalue + weightfit_method / prior_family / slow_thinking_triggered / tail_width
s1s1_mode + coupling_direction + confidencecoupling_strength / evidence / falsification_ref / backtrigger / second_order_branch / convergence_flag
mca_bucket(Task schema 元数据,承接 ADR-008 supplement §2.7,由 Task 模型携带,不入 StructuredCognitionResult)七轴档位 + bucket_label(由 dispatch_task 在意图识别层产出)worst_axis / tag_version(见 里程碑字段演化矩阵
# src/finbayes/cognition/contract_v1_1_m0.py
# M0 实施子集 —— 1.1 完整契约见 ./cognition-1.1-contract.md §5
from __future__ import annotations
from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict, Field

# enum 复用契约源,M0 仅使用以下取值集(命名 snake_case 与契约源一致)
S1Mode = Literal[
"a-far-extrapolation", "b-source-missing",
"c-narrative-data-detachment", "d-unit-accounting-incompatible",
"positive-coupling",
]
CouplingDirection = Literal[
"narrative-leads-numbers", "numbers-lead-narrative",
"decoupled", "positive-coupled",
]
PillarLevel = Literal["applicable", "partial", "not-applicable"]
NodeType = Literal["entity", "relation", "attribute"]
EdgeType = Literal[
"causal", "reflexivity", "shared-book",
"institutional-friction", "cross-market-mapping",
]
BucketLabel = Literal["B1", "B2", "B3", "B4", "B5a", "B5b", "B6", "B7"]


class _BaseM0(BaseModel):
# extra="ignore" 承接 1.0 → 1.1 兼容(详见契约源 §6):
# 旧 1.0 consumer 读到 1.1 输出时忽略未知字段,不阻断管线。
model_config = ConfigDict(extra="ignore")

# M0 阶段固定 "1.1-m0",与 1.0 主体的 schema_version: int = 1 平行;
# M1+ 合并到完整 1.1 schema 时收紧到契约源 §5 草案(届时与 1.0 主体的
# schema_version 字段合并到同一命名空间,详见里程碑字段演化矩阵)。
# 所有 *M0 子类自动继承本字段,C-1 contract test
# test_all_pydantic_models_have_schema_version 据此通过。
schema_version: Literal["1.1-m0"] = "1.1-m0"


class ClockPhaseM0(_BaseM0):
"""M0 最小:仅 clock_id + phase_label + confidence。evidence_ref M1+ 落地。"""
clock_id: str = Field(...) # 取值 M3.t1–M3.t9
phase_label: str = Field(...) # 机制自定枚举,M3 v1 校准后再 Literal 化
confidence: float = Field(..., ge=0.0, le=1.0)


class PhaseEvidenceM0(_BaseM0):
clocks: list[ClockPhaseM0] = Field(default_factory=list)
# M0 stub: phase_matrix 留 None 不实现(M3 启用后由综合层产出)
phase_matrix: Optional[dict] = Field(default=None)


class NodeM0(_BaseM0):
id: str = Field(...)
object_ref: str = Field(...)
node_type: NodeType = Field(...)


class EdgeM0(_BaseM0):
id: str = Field(...)
from_node: str = Field(...)
to_node: str = Field(...)
edge_type: EdgeType = Field(...)
# M0 实施:path_confidence + translation_loss 必填(消费端可观测)
path_confidence: float = Field(..., ge=0.0, le=1.0)
translation_loss: float = Field(..., ge=0.0, le=1.0)
# M0 stub: form 留 None(shared-book 四形态 M5 启用后落地)
form: Optional[str] = Field(default=None)


class TransmissionGraphM0(_BaseM0):
nodes: list[NodeM0] = Field(default_factory=list)
edges: list[EdgeM0] = Field(default_factory=list)
# M0 stub: 以下 4 字段留空,M5 启用后由 KnowledgeGraphService 填充
reverse_forces: list[dict] = Field(default_factory=list)
endogeneity: dict = Field(default_factory=dict)
correlation_regime: Optional[dict] = Field(default=None)
unbalanced_loop_warnings: list[dict] = Field(default_factory=list)


class PillarApplicabilityM0(_BaseM0):
level: PillarLevel = Field(...)
reason: Optional[str] = Field(default=None) # level=applicable 时可 None
# M0 stub: evidence_ref 留 None(M6 启用后由综合层引用)
evidence_ref: Optional[str] = Field(default=None)


class ApplicabilityFlagsM0(_BaseM0):
valuation: PillarApplicabilityM0 = Field(...)
factor: PillarApplicabilityM0 = Field(...)
derivatives: PillarApplicabilityM0 = Field(...)


class PosteriorModeM0(_BaseM0):
value: float = Field(...)
weight: float = Field(..., ge=0.0, le=1.0)
# M0 stub: tail_width 留 None(M7.uq 启用后落地)
tail_width: Optional[float] = Field(default=None)


class BimodalPosteriorM0(_BaseM0):
mode_a: PosteriorModeM0 = Field(...)
mode_b: PosteriorModeM0 = Field(...)
# M0 stub: fit_method / prior_family / slow_thinking_triggered M7.uq 启用后落地
# 注:kelly_cap 已退役(ADR-021,2026-06-04),不再是 posterior 字段
fit_method: Optional[str] = Field(default=None)
prior_family: Optional[str] = Field(default=None)
slow_thinking_triggered: Optional[bool] = Field(default=None)


class NarrativeNumberConsistencyM0(_BaseM0):
"""横切机制 S1(叙事-数字一致性)的 M0 最小子集。

M0 仅落 s1_mode + coupling_direction + confidence 三字段,足以驱动
评测维度 D2(叙事数字一致性)M0 退化判定。完整字段在 M5 / M7 启用。
"""
s1_mode: list[S1Mode] = Field(default_factory=list) # 可多选
coupling_direction: CouplingDirection = Field(...)
confidence: float = Field(..., ge=0.0, le=1.0)
# M0 stub: 以下字段留 None / 空,M5 + M7 启用后填充
coupling_strength: Optional[float] = Field(default=None)
evidence: Optional[dict] = Field(default=None)
falsification_ref: Optional[str] = Field(default=None)
backtrigger: list[dict] = Field(default_factory=list)
second_order_branch: Optional[dict] = Field(default=None)
convergence_flag: Optional[dict] = Field(default=None)


class MCABucketM0(_BaseM0):
"""Task schema 元数据,不入 StructuredCognitionResult 本体(契约源 §2.7)。"""
axis_1_investor_structure: Literal["L1", "L2", "L3"] = Field(...)
axis_2_derivatives_maturity: Literal["D1", "D2", "D3"] = Field(...)
axis_3_institutional_friction: Literal["F1", "F2", "F3"] = Field(...)
axis_4_non_market_actor: Literal["N1", "N2", "N3"] = Field(...)
axis_5_credit_environment: Literal["C1", "C2", "C3"] = Field(...)
axis_6_information_availability: Literal["I1", "I2", "I3"] = Field(...)
axis_7_currency_cross_border: Literal["K1", "K2", "K3"] = Field(...)
bucket_label: BucketLabel = Field(...)
# M0 stub: worst_axis 与 tag_version 在多桶命中裁决规则落地后必填(MCAClassifier §7)
worst_axis: Optional[str] = Field(default=None)
tag_version: Optional[str] = Field(default=None)


class StructuredCognitionResult11Minimal(_BaseM0):
"""StructuredCognitionResult 1.1 M0 实施子集。

与 1.0 主体(上方 StructuredCognitionResult)的关系:M0 阶段两个 schema 并存。
综合层产出时,1.0 十要素填入 StructuredCognitionResult;1.1 新增 6 顶层字段
填入本模型;序列化层合并为单一 JSON object(顶层声明 structured_result_version="1.1")。
M1+ 合并为单一 Pydantic 模型,按契约源 §5 草案完整落盘。
"""
structured_result_version: Literal["1.1"] = "1.1"
# M0 实施 5 字段(mca_bucket 不入此模型,由 Task 携带,见 MCABucketM0)
phase_evidence: Optional[PhaseEvidenceM0] = Field(default=None)
causal_graph: Optional[TransmissionGraphM0] = Field(default=None)
applicability_flags: Optional[ApplicabilityFlagsM0] = Field(default=None)
posterior: Optional[BimodalPosteriorM0] = Field(default=None)
s1: Optional[NarrativeNumberConsistencyM0] = Field(default=None)
# M0 stub: regulation_status M3 / MCA 轴 3 = F2/F3 启用后必填
regulation_status: Optional[dict] = Field(default=None)

§3.5 约束 / 与 §3 主体的差异

  • _BaseM0.model_configextra="ignore" 而非 §3 主体的 extra='forbid' —— 1.1 contract 显式要求向后兼容(详见契约源 §6);旧 1.0 consumer 读到 1.1 输出时需要忽略新字段,不能 raise。
  • _BaseM0.schema_version: Literal["1.1-m0"] = "1.1-m0" 是 §3.5 模型族的版本指纹(与 §3 主体的 schema_version: int = 1 平行不冲突)。所有 *M0 子类(PhaseEvidenceM0 / NodeM0 / EdgeM0 / TransmissionGraphM0 / PillarApplicabilityM0 / ApplicabilityFlagsM0 / PosteriorModeM0 / BimodalPosteriorM0 / NarrativeNumberConsistencyM0 / MCABucketM0 / StructuredCognitionResult11Minimal 等)自动继承,C-1 contract test test_all_pydantic_models_have_schema_version 据此通过。M1+ 合并到完整 1.1 schema 时收紧到契约源 §5 草案统一命名空间。
  • 字段间约束(契约源 §4 第 1 / 2 / 3 / 5 / 6 / 8 条共 12 条)M0 阶段不落 @model_validator,留 M1+ 合并 schema 时按 Pydantic v2 @model_validator(mode="after") 一次落盘。
  • 命名 *M0 后缀仅为 M0 阶段区分;M1+ 合并到契约源 §5 完整草案时去掉 M0 后缀。
  • 半人工标注覆盖范围(如 causal_graph.edges[?].form 标注、applicability_flags.*.level 边缘判定)在 M0 不上线,详见契约源 §7。
  • 字段从 Optional(M0)→ required(M1+)的收紧时点与 audit 触发条件见 里程碑字段演化矩阵,至少覆盖 s1 / mca_bucket.worst_axis / mca_bucket.tag_version / regulation_status 四项。
# src/finbayes/state/types.py
from pydantic import BaseModel
from typing import Literal
from datetime import datetime

class Session(BaseModel):
schema_version: int = 1
session_id: str
user_id: str
name: str | None = None # M0 仅 default session 可不命名
state: Literal["active", "snapshotted", "archived", "deleted"] = "active"
created_at: datetime
last_active_at: datetime
turn_count: int = 0
is_default: bool = False

class FinObject(BaseModel):
schema_version: int = 1
fin_object_id: str
user_id: str
object_type: Literal["asset", "sector", "theme", "portfolio", "event", "policy", "actor", "narrative"]
canonical_name: str # 如 "BTC" / "ETH" / "NVDA"
market: Literal["crypto", "us_stocks"] | None = None
created_at: datetime
archived_at: datetime | None = None
metadata: dict = {}

class AuditEvent(BaseModel):
"""通用审计事件结构。各 event_type 对应的 payload 字段集见 §5"""
schema_version: int = 1
event_id: str
timestamp: datetime
event_type: Literal[
"task_started", "task_completed", "task_failed", "task_cancelled",
"llm_call", "tool_call",
"degradation_triggered", "boundary_rejected",
"state_changed", "user_action",
]
user_id: str
session_id: str | None = None
task_id: str | None = None
payload: dict # 字段集由 event_type 决定,详见 §5
# src/finbayes/providers/types.py
from pydantic import BaseModel
from typing import Literal

class ProviderReadiness(BaseModel):
schema_version: int = 1
provider_id: str
state: Literal["ready", "degraded", "blocked", "unsupported"]
last_probe_at: str
failure_count_24h: int = 0
last_failure_reason: str | None = None

class LLMResponse(BaseModel):
schema_version: int = 1
content: str
tool_calls: list[dict] = [] # OpenAI Function Calling 格式
finish_reason: Literal["stop", "tool_calls", "length", "content_filter"]
usage: dict # input_tokens / output_tokens / total_tokens

class ToolResponse(BaseModel):
schema_version: int = 1
tool_name: str
success: bool
data: dict | None = None # 工具实际返回数据
error: str | None = None # 失败原因
latency_ms: int
is_mock: bool = True # M0 阶段始终 True

§3 末 — Placeholder 类型节(M0 stub / skip 阶段需要的最小定义)

下列类型在主架构 §9 接口签名中出现,但本文档 §2 表中标 stub / skip。M0 阶段定义最小字段以保证 import 不破。

# src/finbayes/orchestration/clarify_types.py
from pydantic import BaseModel
from typing import Literal

class ClarifyResponse(BaseModel):
"""clarify 工具的响应(M0 stub,实际逻辑在 M2 上线)"""
schema_version: int = 1
clarification_question: str
related_field: Literal["task_type", "object", "time_window", "depth"]
suggested_options: list[str] = []
# src/finbayes/io/streaming_types.py
from pydantic import BaseModel
from typing import Literal

class Chunk(BaseModel):
"""流式输出的单元(CLI streaming 协议)"""
schema_version: int = 1
chunk_type: Literal["text", "metadata", "audit_summary", "completion"]
content: str # 文本类型时是文本片段;metadata 时是 JSON 字符串
sequence_no: int # 单 task 内递增
is_final: bool = False
# src/finbayes/capabilities/types.py
from pydantic import BaseModel, Field
from typing import Literal

ToolCategory = Literal[
"read_only", # 只读数据(价格 / 新闻 / 链上)
"analysis", # 推理 / 综合 / 解释
"synthesis", # 输出综合
"clarify", # 澄清
"state_action", # 状态操作(候选写入 / 用户动作)
# 注意:禁止 "execution" 枚举值(详见主架构 §17 执行类工具拒绝)
]

class ToolSpec(BaseModel):
schema_version: int = 1
tool_id: str
tool_name: str
category: ToolCategory # 严格枚举,无 execution
description: str
parameters_schema: dict # JSON Schema
deprecated: bool = False
replacement_tool_id: str | None = None

约束

  • 所有 BaseModel 必填字段含 Field(...),可选用 | None = None
  • M0 不引入 Generic[T] / Annotated 等高级类型
  • 全局 ConfigDict 策略:extra='forbid'(详见 §13)
  • 例外:§3.5 StructuredCognitionResult11Minimal 系列模型用 extra='ignore',承接 1.1 契约源 §6 与 1.0 consumer 的向后兼容承诺
  • 工具 schema 注册时静态校验 category != "execution"(不存在的枚举值会被 Pydantic 拒绝)

§4 M0 SQLite DDL v0

M0 阶段最小表集(4 张表 + 元表)。完整 8 张业务表 + 元表的 schema 在 M1+ 补充。

-- migrations/001_initial.sql
-- M0 SQLite schema v0
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;

CREATE TABLE IF NOT EXISTS schema_metadata (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
INSERT OR IGNORE INTO schema_metadata (key, value) VALUES
('schema_version', '1'),
('finbayes_version', '0.1.0'),
('migrated_at', strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
('migration_history', '[]');

CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT,
state TEXT NOT NULL CHECK (state IN ('active', 'snapshotted', 'archived', 'deleted')),
created_at TEXT NOT NULL,
last_active_at TEXT NOT NULL,
turn_count INTEGER NOT NULL DEFAULT 0,
is_default INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_sessions_user_state ON sessions (user_id, state);

CREATE TABLE IF NOT EXISTS fin_objects (
fin_object_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
object_type TEXT NOT NULL, -- asset/sector/theme/portfolio/event/policy/actor/narrative
canonical_name TEXT NOT NULL,
market TEXT, -- 'crypto' / 'us_stocks' / NULL(跨市场如宏观变量)
created_at TEXT NOT NULL,
archived_at TEXT,
metadata TEXT -- JSON payload(M0 简化)
);
CREATE INDEX IF NOT EXISTS idx_fin_objects_user ON fin_objects (user_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_fin_objects_canonical ON fin_objects (user_id, canonical_name, market);

CREATE TABLE IF NOT EXISTS audit_events (
event_id TEXT PRIMARY KEY,
timestamp TEXT NOT NULL,
event_type TEXT NOT NULL,
user_id TEXT NOT NULL,
session_id TEXT,
task_id TEXT,
payload TEXT NOT NULL, -- JSON, schema 详见本文档 §5
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
);
CREATE INDEX IF NOT EXISTS idx_audit_user_time ON audit_events (user_id, timestamp);
CREATE INDEX IF NOT EXISTS idx_audit_task ON audit_events (task_id);
CREATE INDEX IF NOT EXISTS idx_audit_type ON audit_events (event_type, timestamp);

M0 表数量:4 张业务/审计表 + 1 张元表 = 5 张。M1+ 加 watchlist_objects / judgment_records / dynamic_profiles / state_candidates / context_snapshots 5 张,达成主架构 §15 完整 8 张业务表 + 1 张元表的格局。

与 1.1 schema 的关系:§3.5 引入的 1.1 最小子集 6 顶层字段 M0 以 JSON blob 形式落入 audit_events.payloadtask_completed 事件含 structured_result_version + 1.1 字段子集摘要),不在本节 DDL 拆专列。M1+ judgment_records 上线后按契约源 §6 兼容性条款独立索引 1.1 顶层字段(structured_result_version / mca_bucket.bucket_label / applicability_flags.*.level)。

约束

  • 所有 DDL 用 CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS —— 保证 migration 幂等(DDL 层)
  • 所有 payload / metadata 用 TEXT 存 JSON(Pydantic schema 在应用层校验)
  • 不在 DDL 阶段建复杂触发器(M0 简化)

§5 AuditEvent.payload 按 event_type 字段集(新增 P0)

AuditEvent.payloaddict(持久化为 JSON TEXT),各 event_type 对应的字段集如下。M0 实现的 event 类型均需符合:

task_started

字段类型必填说明
task_idstr见 NormalizedInput.task_id
session_idstr
entrystrcli / tui / web / mcp / channel
input_excerptstr前 200 字(不含敏感信息)
input_length_charsint完整长度(用于 audit)

task_completed

字段类型必填说明
task_idstr
task_typestr7 类任务之一(事后回填)
output_summarystr综合输出的 main_answer 前 200 字
provider_calls_countintLLM 调用次数
tool_calls_countint工具调用次数
total_latency_msint
total_cost_estimatefloatUSD
degradedbool是否走过降级路径
structured_result_versionstr固定 "1.1"(承接 §3.5 与 cognition-1.1-contract.md §6)
mca_bucket_labelstr | None1.1 桶标签摘要(如 B1 / B5a),M0 stub 阶段可缺省

task_failed

字段类型必填说明
task_idstr
failure_stagestrinput / orchestration / llm / synthesis / output
failure_reasonstr错误类型 + 简短描述(不含 secret)
recoverablebool是否能通过重试 / 降级恢复

task_cancelled

字段类型必填说明
task_idstr
cancellation_sourcestruser / timeout / task_group_failure
partial_result_keptbool是否保留了部分中间结果

llm_call

字段类型必填说明
llm_call_idstr
task_idstr
provider_idstr见 LLMCall.provider_id 格式
modelstr
input_token_countint
output_token_countint
estimated_costfloat
latency_msint
is_fallbackboolM0 始终 false
finish_reasonstrstop / tool_calls / length / content_filter
failure_reasonstr | None失败时填

tool_call

字段类型必填说明
tool_call_idstr
task_idstr
tool_namestr
arguments_summarystr参数摘要(不含 secret,详见 §10 凭证识别
successbool
latency_msint
is_mockboolM0 始终 true
failure_reasonstr | None失败时填

degradation_triggered

字段类型必填说明
from_layerstrL1 / L1' / L2 / L3(M0 stub 不应出现)
to_layerstrL1' / L2 / L3 / L4
trigger_reasonstrprovider_unavailable / rate_limit / auth_failed / timeout
task_idstr

boundary_rejected —— 同步写入(不允许异步丢失)

字段类型必填说明
entrystr拒收的入口
reject_reasonstrcredential_detected / execution_request / boundary_violation
reject_categorystr详见 §10 凭证识别 reject_category 取值
confidencefloat0.0-1.0
input_length_charsint完整长度
safe_response_idstr安全回应模板 id(不写文案原文)

约束boundary_rejected 事件必须同步写入(不入异步队列),凭证类负面事件零丢失。

state_changed

字段类型必填说明
object_typestrsession / fin_object(M0 范围)
object_idstr
change_typestrcreated / updated / archived / deleted
field_diffdict字段级 before/after(M0 简化为顶层字段,不递归)
triggered_bystruser / system / task

user_action

字段类型必填说明
action_typestrsession_create / session_delete / fin_object_view / etc
target_object_idstr | None
confirmedbool用户是否经过二次确认

实施约束

  • 每个 event_type 的 payload 在写入前用 Pydantic 模型校验(M0 可以是 dict + manual validate;M1+ 升级为各 event_type 独立 Pydantic 类)
  • 写入接口 append_audit(event: AuditEvent) 在 M0 阶段对 boundary_rejected 同步写,其他事件可异步(详见 §13 异步审计写入模式)
  • 任何 payload 字段不得含凭证字符串、完整 LLM input/output、完整用户画像;只含摘要 / token 数 / hash

§6 M0 CLI 命令规格

finbayes ask "<text>" # 单次提问,流式输出到 stdout
finbayes status # Provider Readiness / State Store 健康 / 当前 Session
finbayes init # 首次启动初始化(创建 State Store + 默认 Session)
finbayes session list # 列出所有 Session(M0 仅默认 Session)
finbayes audit list [--since=7d] # 时间范围内的审计事件(M0 最小输出)

CLI 框架选定:click(详见 §13)。

流式输出格式(默认人类可读)

[task abc12345 starting...]
[provider: openai-compatible:gpt-4o-mini]
{流式回答内容}
---
反方证据:
- ...

失效条件:
- ...

信息缺口:
- ...

来源:
- {tool_name} @ {timestamp}

[task abc12345 completed in 8.3s, cost ~$0.03]

--json 切换 JSON 输出

{
"schema_version": 1,
"task_id": "abc12345",
"result": { ...StructuredCognitionResult... },
"audit_summary": { "provider_calls": 2, "tool_calls": 3, "latency_ms": 8300, "cost_estimate": 0.03 },
"exit_code": 0
}

退出码(M0 子集)

Code状态含义
0success任务正常完成
10invalid_input输入空 / 格式错
20provider_not_readyLLM 或数据 Provider 不可用 + 已尝试降级失败
30boundary_reject输入含凭证类内容被拒收
40task_failed任务执行中失败
71finbayes_error_defaultFinBayesError 基类未细化时的默认 exit code
90audit_write_failed审计写入主路径 + JSONL fallback 同时失败(详见 §13 AuditWriter)

M1+ 引入 50(degraded)/ 60(unsupported)/ 等扩展退出码。

退出码对应的 Python Exception 类层级见 §13 Bootstrap。

退出码命名空间约定

为避免子类异常与基类 FinBayesError 默认值冲突,退出码按段位分配,新增退出码必须落入对应段

用途
0success
10-19输入 / 格式类错误(CLI 入口前置校验)
20-29Provider / 外部依赖类错误
30-39边界 hook 拒收类(凭证 / 执行请求 / 内容违规)
40-49任务执行类失败(orchestration / synthesis 阶段)
50-59降级路径(M1+ 启用)
60-69不支持 / 能力缺失类(M1+ 启用)
70-79FinBayesError 基类与未分类内部错误(71 = 基类默认值,70 保留给未捕获异常 / Exception 兜底)
80-89边界拒收的扩展类(M2+ 启用,预留给多类 reject_reason 细分)
90-99audit / 持久化层故障(详见 §13)

§7 M0 Mock Provider Fixture 规范

case 库三集分集物理实施(70/20/10 dev / test / holdout + case_id 命名 + 公开范围 gate)见 Case 库 70/20/10 物理实施约定。本节只定义 LLM Provider Mock fixture 的目录与哈希;case 库 fixtures/cases/ 路径与 schema 不在本节复述。

外部数据 Provider 字段映射 / freshness SLA / 凭证治理(CFTC TFF / 13F / FX swap / ETF flow / IV 曲面 / 链上 / A 股两融 / 跨境通道额度 等 19 源)见 外部数据源单点规约。M0 阶段不消费任何外部数据源,所有数据走本节定义的 Mock fixture;M2+ Provider Adapter 接入时按 data-providers.md 字段映射落地。

目录结构

tests/fixtures/llm/
├── openai_compatible/
│ ├── eth_analysis_basic.json
│ ├── btc_explanation.json
│ ├── credential_rejection.json
│ ├── multi_tool_call.json
│ └── empty_response.json
├── _index.json # request hash → fixture filename 映射
└── _hash_spec.md # hash 算法说明(→ Python 实现见 §13)

Hash 算法(白名单字段)

# tests/fixtures/llm/_hash_spec.py
HASH_INCLUDED_FIELDS = [
"model",
"messages", # 含 system + user,按 role 拼接,content 规范化(strip + lower-newlines)
"tools", # 按 tool name + parameter schema 哈希;排序后哈希
"tool_choice",
"temperature",
"max_tokens",
]
HASH_EXCLUDED_FIELDS = [
"user_id", # 测试隔离用
"session_id", # 同上
"task_id", # 每次随机
"timestamp", # 每次新
"request_id", # Provider 端生成
"trace_id", # contextvars 注入
]

def compute_request_hash(request: dict) -> str:
"""SHA256 hash on whitelisted fields,序列化前规范化:
- 按字段名字母序排序
- messages 中的 content 字段 strip 空白 + 规范化换行
- tools 按 tool name 字母序排序
- 浮点数(temperature)保留 4 位小数
返回形如 'sha256:abc...' 的字符串
"""
import hashlib
import json
# 详细实现见工程实施仓的 src/finbayes/providers/mock/hash.py
# 此处约定算法接口

_index.json Schema

{
"schema_version": 1,
"fixtures": [
{ "hash": "sha256:abc...", "filename": "eth_analysis_basic.json", "provider_api_version": "openai-2024-10" }
]
}

单条 fixture 格式

{
"fixture_id": "eth_analysis_basic_v1",
"request_hash": "sha256:abc...",
"provider_api_version": "openai-2024-10",
"request_summary": {
"model": "gpt-4o-mini",
"user_message_excerpt": "分析 ETH 这周走势..."
},
"response": {
"id": "chatcmpl-mocked",
"object": "chat.completion",
"created": 1700000000,
"model": "gpt-4o-mini",
"choices": [{ "message": { "role": "assistant", "content": "...", "tool_calls": [...] }, "finish_reason": "tool_calls" }],
"usage": { "prompt_tokens": 350, "completion_tokens": 180, "total_tokens": 530 }
}
}

Provider API 变化检测

  • provider_api_version 字段在每条 fixture 中
  • nightly 真档跑认知输出样例输入(m0_s1 / m0_s2 / m0_s4 / m0_s6)→ 与 fixture 字段级 diff
  • diff 阈值:任何 fixture 中的 fixture.response 字段(除 created / id 外)不一致即触发
  • 阈值配置:configs/mock_drift_thresholds.yaml(M0 一次性写入)
  • 触发后 CI 输出 FIXTURE_OUT_OF_DATE: provider_api_version=X drift to Y 并 fail

§8 M0 6 条样例输入 + 期望"能力清单"

# tests/m0/sample_inputs.yaml
samples:
- sample_id: m0_s1_eth_weekly
input_text: "分析 ETH 这周走势"
scenario: S1 即时认知请求(分析类)
expected_capabilities:
- assertion: "main_answer 含 ETH 与时间窗口"
pytest_check: "'ETH' in result.main_answer and any(t in result.main_answer for t in ['本周', '这周', '过去 7 天'])"
- assertion: "counter_evidence 至少 1 条"
pytest_check: "len(result.counter_evidence) >= 1"
- assertion: "invalidation_conditions 至少 2 条"
pytest_check: "len(result.invalidation_conditions) >= 2"
- assertion: "sources 含价格 + 至少一类外部源"
pytest_check: "len(result.sources) >= 2"
- assertion: "non_instruction_notice 存在"
pytest_check: "result.non_instruction_notice"

- sample_id: m0_s2_btc_explain
input_text: "什么是 BTC 减半?"
scenario: 解释类
expected_capabilities:
- assertion: "main_answer 解释机制 + 周期"
pytest_check: "'BTC' in result.main_answer or '减半' in result.main_answer"
- assertion: "invalidation_conditions 至少 1 条"
pytest_check: "len(result.invalidation_conditions) >= 1"

- sample_id: m0_s3_credential_reject
input_text: "我的交易所 API key 是 sk-abc123..."
scenario: S6 边界拒收凭证
expected_capabilities:
- assertion: "BoundaryResult.passed == False"
pytest_check: "not boundary_result.passed"
- assertion: "reject_reason == credential_detected"
pytest_check: "boundary_result.reject_reason == 'credential_detected'"
- assertion: "不进入 Task / LLM / 状态对象 / 审计 payload 中含具体凭证"
pytest_check: "task is None and not any('sk-abc123' in (e.payload_text or '') for e in audit_events)"
notes: "2026-05-28 Step 11 IV-5 修复: 原断言用 str(audit_events) 触发 safe_globals 不含 str() 的 NameError; 改为基于 audit event payload_text 字段的结构化扫描, 不依赖内置函数. Step 10 Codex 红点关闭."

- sample_id: m0_s4_btc_etf_flow
input_text: "最近 BTC 现货 ETF 资金流向变化怎么看?"
scenario: 分析类(链上 + ETF 资金流事件冲击,crypto-only)
expected_capabilities:
- assertion: "main_answer 含 BTC + ETF 摘要"
pytest_check: "'BTC' in result.main_answer and 'ETF' in result.main_answer"
- assertion: "supporting_evidence 至少 1 条"
pytest_check: "len(result.supporting_evidence) >= 1"
- assertion: "counter_evidence 至少 1 条"
pytest_check: "len(result.counter_evidence) >= 1"

- sample_id: m0_s5_unsupported
input_text: "帮我下单买入 1 个 BTC"
scenario: 边界拒收(执行类,crypto-only)
expected_capabilities:
- assertion: "BoundaryResult.passed == False"
pytest_check: "not boundary_result.passed"
- assertion: "reject_reason == execution_request"
pytest_check: "boundary_result.reject_reason == 'execution_request'"
- assertion: "safe_response 含'认知/执行分工'说明"
pytest_check: "'认知' in boundary_result.safe_response or '执行' in boundary_result.safe_response"

- sample_id: m0_s6_eth_reduce_directional
input_text: "我持有 ETH,跌到 2000 我要不要减?"
scenario: 主架构 §6 S6/S7(条件化方向判断,crypto-only 落地——产品最核心能力,M0 必须被验证一次)
capability_class: directional_judgment # 与 m0_s5 执行类拒收明确区分:本条要求"按条件给方向倾向",拒收的是"替用户执行"
expected_capabilities:
# 核心能力断言:必须产出"在 X 条件下偏向减仓/不减"的条件化方向倾向,而非泛泛分析或拒答。
# 区分锚点:S6/S7 是"给方向但不替执行";m0_s5 拒收的是执行动作。本条 boundary_result.passed 必须为 True。
- assertion: "应进入 Task 正常综合,不被边界 hook 拒收(方向判断 ≠ 执行请求)"
pytest_check: "boundary_result.passed and task is not None"
- assertion: "应给出含条件化方向倾向的 main_answer('在 X 条件下偏向减仓 / 在 Y 条件下不减' 的条件化表述,而非'无法判断'或泛泛复述)"
pytest_check: "'ETH' in result.main_answer and any(k in result.main_answer for k in ['偏向', '倾向', '减仓', '减']) and any(k in result.main_answer for k in ['条件', '若', '如果', '在', '前提'])"
# 四要素齐备(方向 + 失效条件 + 反方 + 不确定性),缺一即不达成能力
- assertion: "应给出失效条件(方向判断在何种可观测前提下失效),≥1 条"
pytest_check: "len(result.invalidation_conditions) >= 1"
- assertion: "应给出反方证据(支持相反方向的真实对立证据),≥1 条"
pytest_check: "len(result.counter_evidence) >= 1"
- assertion: "应诚实标注不确定性 / 信息缺口(如未声明仓位 / 风险预算 / 时间窗时不给具体减仓比例),≥1 条"
pytest_check: "len(result.uncertainty_and_gaps) >= 1"
- assertion: "应显式标注非指令边界(这是检验而非指令、执行权在用户)"
pytest_check: "result.non_instruction_notice"

m0_s6 能力达成判定与 m0_s5 拒收的边界m0_s6("我持有 ETH,跌到 2000 要不要减")锚定主架构 §6 S6/S7「条件化方向判断」——FinBayes 可以给"在 X 条件下偏向减仓"的方向倾向,本条 boundary_result.passed == True、正常进入 Task;m0_s5("帮我下单买入 1 个 BTC")拒收的是执行动作(替用户下单),boundary_result.passed == False。两者区别是「给方向判断」vs「替执行」,不是「判断」本身被拒。架构 §6 S7 canonical 用 NVDA(US stocks),M0 scope 为 crypto-only(§1),故本条落地为等价的 crypto 资产 ETH 减仓场景,保持「条件化方向 + 四要素」能力契约一致。

M0 6 条 fixture 真实样本(与 §11 验收对应)

sample_inputs.yaml 描述「输入 + 期望断言」,对应的 fixture 实体(含 task_id / fin_object / output_phase_evidence / mca_bucket 等)落于工程仓 tests/m0/fixtures/samples/,每条单独一个 YAML 文件,给 Codex 直接照写:

fixture 文件位置约定(治理库 vs 工程仓边界)

  • 下列 5 段 YAML 是 fixture 模板,不是治理库内的可执行文件。治理库本仓不存 runtime data / 不存可执行测试 fixture(详见 CLAUDE.md「写入权限边界」与 项目 README · 工程产物归属)。
  • C-1 task packet 启动后,Codex 按下述模板在 工程仓 tests/m0/fixtures/samples/m0_s{1,2,3,4,5}*.yaml 实际落盘,sample runner(§14.5 第 7 项)才有可执行 fixture。
  • 工程仓 fixture 文件的字段顺序、命名(snake_case)、与 §3 / §3.5 Pydantic 模型字段的一致性由 C-1 task packet 验收清单锁定;治理库本仓只锁定 acceptance condition(§8 expected_capabilities[?].pytest_check 表达式 + §11 自动判定表)。
  • 团队读 m0-pack 时不应默认治理库内已有可执行 fixture;任何指向治理库内 tests/m0/fixtures/samples/*.yaml 的工具调用预期都需在工程仓侧落盘。

共享片段:所有 fixture task.user_id = "local"session_id = "sess_m0_default"mca_bucket 七轴档位均为 L1 / D2 / F1 / N1 / C2 / I2 / K2(crypto + L1 个人投资者档),bucket_label ∈ {B1, B2}mca_bucket 七字段展开见 §3.5 MCABucketM0,下列 fixture 仅写 bucket_label 占位,实施时按 §3.5 七字段补齐。

# tests/m0/fixtures/samples/m0_s1_eth_weekly.yaml
sample_id: m0_s1_eth_weekly
task: { task_id: task_m0s1eth0001, task_type: analysis, mca_bucket: { bucket_label: B2 } }
fin_object: { fin_object_id: obj_eth_001, object_type: asset, canonical_name: ETH, market: crypto }
output_phase_evidence:
clocks: [{ clock_id: "M3.t1", phase_label: "growth", confidence: 0.7 }]
# tests/m0/fixtures/samples/m0_s2_btc_explain.yaml
sample_id: m0_s2_btc_explain
task: { task_id: task_m0s2btc0001, task_type: explain, mca_bucket: { bucket_label: B1 } }
fin_object: { fin_object_id: obj_btc_001, object_type: asset, canonical_name: BTC, market: crypto }
output_phase_evidence: { clocks: [] } # 解释类 M0 可不激活时钟
# tests/m0/fixtures/samples/m0_s3_credential_reject.yaml
sample_id: m0_s3_credential_reject
task: null # 边界拒收,不入 Task
boundary_result: { passed: false, reject_reason: credential_detected, reject_category: openai_api_key_pattern, safe_response_id: safe_response_credential_v1 }
audit_event:
event_type: boundary_rejected
payload: { entry: cli, reject_reason: credential_detected, reject_category: openai_api_key_pattern, confidence: 0.95 }
# tests/m0/fixtures/samples/m0_s4_btc_etf_flow.yaml
sample_id: m0_s4_btc_etf_flow
task: { task_id: task_m0s4etf0001, task_type: analysis, mca_bucket: { bucket_label: B2 } }
fin_object: { fin_object_id: obj_btc_etf_001, object_type: asset, canonical_name: BTC, market: crypto, metadata: { instrument: spot_etf_flow } }
output_phase_evidence:
clocks: [{ clock_id: "M3.t3", phase_label: "speculative", confidence: 0.6 }]
# tests/m0/fixtures/samples/m0_s5_unsupported.yaml
sample_id: m0_s5_unsupported
task: null # 执行类拒收,不入 Task
boundary_result: { passed: false, reject_reason: execution_request, reject_category: execution_request_pattern, safe_response_id: safe_response_execution_v1 }
audit_event:
event_type: boundary_rejected
payload: { entry: cli, reject_reason: execution_request, reject_category: execution_request_pattern, confidence: 0.85 }
# tests/m0/fixtures/samples/m0_s6_eth_reduce_directional.yaml
# 条件化方向判断样本(主架构 §6 S6/S7 的 crypto-only 落地):正常进入 Task,不被拒收。
# fixture 实体(mock LLM response 文案需含「在 X 条件下偏向减仓 / 在 Y 条件下不减」的条件化方向 +
# 失效条件 + 反方 + 不确定性 四要素)由 C-1 task packet 在工程仓侧补齐;
# 治理库本仓只锁定 §8 expected_capabilities[?].pytest_check 与 §11 能力达成判定为 acceptance condition。
sample_id: m0_s6_eth_reduce_directional
boundary_result: { passed: true, reject_reason: null } # 方向判断 ≠ 执行请求,不拒收
task: { task_id: task_m0s6eth0001, task_type: trade_prep, mca_bucket: { bucket_label: B2 } }
fin_object: { fin_object_id: obj_eth_002, object_type: asset, canonical_name: ETH, market: crypto }
output_phase_evidence:
clocks: [{ clock_id: "M3.t3", phase_label: "speculative", confidence: 0.6 }]
# expected output StructuredCognitionResult 必含的能力要素(四要素齐备,实施侧填充实体):
# main_answer : 含 ETH + 条件化方向倾向("在 X 条件下偏向减仓 / 在 Y 条件下不减")
# invalidation_conditions: ≥1(方向判断在何前提下失效,用户可观测)
# counter_evidence : ≥1(支持相反方向的真实对立证据)
# uncertainty_and_gaps : ≥1(未声明仓位 / 风险预算 / 时间窗时不给具体减仓比例)
# non_instruction_notice : 非空(检验而非指令、执行权在用户)

约束:6 条 fixture 全部 crypto-only(与 §1 M0 scope 严格一致;架构 §6 S7 canonical 的 NVDA 在 M0 等价落地为 crypto 资产 ETH,保持条件化方向能力契约不变);task_id 走固定 mock 值(不走 uuid4)保证 CI 可重放。

半人工标注 M0 接入

承接 Phase 7 半人工标注 SLA 附录 锁定的四任务 SLA(任务 A:M5.3(c) 政策信用 / 任务 B:M5.3(d) A 股散户急性 / 任务 C:M7.meta-v1 慢思考触发 / 任务 D:MCA 轴 4 N3)。M0 阶段不上线完整 reviewer 工具串,仅锁定字段权限边界。

M0 必须模型自动产出(不允许人工覆盖最终值):

  • phase_evidence.clocks(M3 时钟阶段标签,七字段子集见 §3.5 ClockPhaseM0)。
  • causal_graph.nodes + causal_graph.edges 主体结构(M1 / M5 链路建模产物)。
  • posterior.mode_a / posterior.mode_b(M7.uq 双峰拟合产物;kelly_cap 已退役,见 ADR-021)。
  • s1.s1_mode / s1.coupling_direction / s1.confidence(S1 横切量化判定)。
  • mca_bucket 七轴档位 + bucket_label(意图识别层 dispatch_task 自动判定)。

M0 允许人工覆盖(v1 工程回退路径,承接 ADR-007 supplement §5.3 + ADR-008 supplement §5.1):

  • regulation_status.failure_modes[?].mode ∈ {"c-policy-credit", "d-a-share-retail-acute"}(M5.3 失败模式 c/d,仅当 MCA 轴 3 = F2/F3 时启用;M0 stub 不实施完整 RegulationStatus 子模型,仅留 audit override 字段)。
  • s1.evidence 引用 id 路径(任务 C 推翻路径的中间产物)。
  • posterior.fit_method 慢思考路径(reviewer 可推翻模型自动判定的 slow_thinking_triggered,对应 Phase 7 SLA 任务 C)。
  • mca_bucket.axis_4_non_market_actor = "N3" 时的人工标注(任务 D,季度刷新 + 事件驱动)。

M0 接入方式(不上线 reviewer 工具串):

  • 半人工覆盖发生时,在 audit_events.payload额外写入字段 semi_manual_override: {field: str, reviewer: str, timestamp: str, reason: str}。该字段为 M0 在不引入 reviewer 工具的前提下保留 audit trail 的最小接入。
  • event_type = state_changed 时,若 field_diff 含半人工覆盖项,必须额外携带 semi_manual_override;M0 不实施反向校验,只保证写入存在。
  • M1+ 引入完整 reviewer 工具串后切换到 Phase 7 SLA 附录的四任务工作流(24h / 12h / 月度 / 季度 SLA + Tier 1/2 标注者 + IAA kappa 门槛),M0 阶段所有覆盖事件按「事件后归档复核」处理,不强制 Tier 1 拍板。
  • 凡涉及 M0 半人工覆盖的 fixture 必须在 audit_event.payload 显式写入 semi_manual_override 字段,供 §14.5 test_credential_isolation.py 同族 fixture 校验。

pytest_check 字段是可直接 eval 的 Python 表达式(不是字符串描述)—— 用于 §14.5 中 tests/m0/sample_inputs.yaml runner 的实现。


§9 M0 5 类凭证 Negative Test Fixture

# tests/m0/credential_negatives.yaml
fixtures:
- id: cred_private_key_hex
pattern_class: private_key
input_text: "我钱包的 private key 是 0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
expected_reject_category: "private_key_pattern"
expected_confidence_min: 0.9

- id: cred_mnemonic_12_words
pattern_class: mnemonic
input_text: "我的助记词:abandon ability able about above absent absorb abstract absurd abuse access accident"
expected_reject_category: "bip39_mnemonic_pattern"
expected_confidence_min: 0.95

- id: cred_api_key_openai_like
pattern_class: api_key
input_text: "用这个 key 调一下 GPT:sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCdEfGhIjKl"
expected_reject_category: "openai_api_key_pattern"
expected_confidence_min: 0.9

- id: cred_bank_account_luhn
pattern_class: bank_account
input_text: "我的银行卡号 4532015112830366,今天进账了多少?"
expected_reject_category: "luhn_card_number_pattern"
expected_confidence_min: 0.8

- id: cred_config_paste
pattern_class: config_paste
input_text: |
帮我看看这个配置:
[binance]
api_key = abc123
secret = xyz789
expected_reject_category: "config_paste_with_secrets"
expected_confidence_min: 0.7

M0 验收信号

  • 5 条 fixture 全部命中 expected_reject_categoryconfidence ≥ expected_confidence_min
  • 凭证字符串不进入任何后续路径 —— 见 §14.5 test_credential_isolation.py 接口规范
  • 边界 hook 内部不调 LLM(这一点 §10 LLM 辅助二次判断在 M0 stub,避免 LLM 看到原文)

§10 凭证识别正则基线 v0(新增 P0)

5 类凭证 Python pattern

# src/finbayes/security/credential_patterns.py
import re
from typing import Literal

# Pattern 优先级:完全匹配先于启发式匹配
RejectCategory = Literal[
"private_key_pattern",
"bip39_mnemonic_pattern",
"openai_api_key_pattern",
"anthropic_api_key_pattern",
"generic_api_key_pattern",
"luhn_card_number_pattern",
"config_paste_with_secrets",
"execution_request_pattern",
]

# 1. 私钥(hex format,含可选 0x 前缀,64 hex chars)
PRIVATE_KEY_HEX = re.compile(r"\b(?:0x)?[a-fA-F0-9]{64}\b")
# WIF 格式(Base58,~52 字符,5/K/L 开头)
PRIVATE_KEY_WIF = re.compile(r"\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\b")

# 2. BIP-39 助记词(12 / 18 / 24 词序列)
# 实施策略:先用启发式(连续 12+ 个小写英文单词),再交叉验证 BIP-39 词表
MNEMONIC_HEURISTIC = re.compile(r"\b(?:[a-z]+\s+){11,23}[a-z]+\b")
# BIP-39 词表 source:Python `mnemonic` 库(pip install mnemonic);M0 内嵌前 2048 词的 set 用于快速校验
# 见 src/finbayes/security/bip39_wordlist.py(M0 实施时从 mnemonic 库 export)

# 3. OpenAI API Key
OPENAI_API_KEY = re.compile(r"\bsk-(?:proj-)?[A-Za-z0-9_-]{40,200}\b")
# Anthropic API Key
ANTHROPIC_API_KEY = re.compile(r"\bsk-ant-[A-Za-z0-9_-]{40,200}\b")
# 通用 API Key 启发式(高熵 base64 / hex 长串)
GENERIC_API_KEY = re.compile(r"\b[A-Za-z0-9_-]{32,}\b") # 配合上下文关键词 "api_key" / "secret" / "token"

# 4. 银行卡号 + Luhn 校验
CARD_NUMBER_HEURISTIC = re.compile(r"\b\d{13,19}\b")

def luhn_check(card_num: str) -> bool:
"""Luhn 算法校验"""
digits = [int(d) for d in card_num if d.isdigit()]
if len(digits) < 13 or len(digits) > 19:
return False
odd_sum = sum(digits[-1::-2])
even_sum = sum(sum(divmod(d * 2, 10)) for d in digits[-2::-2])
return (odd_sum + even_sum) % 10 == 0

# 5. 配置文件粘贴(多行 key=value 形式,关键词触发)
CONFIG_PASTE_HEURISTIC = re.compile(
r"(?im)^\s*(?:api_key|secret|token|password|access_key|private_key)\s*=\s*\S+",
re.MULTILINE,
)

# 6. 执行请求模式("帮我下单 / 买入 / 卖出 / 转账 / 提币" 等)
EXECUTION_REQUEST = re.compile(
r"(?:帮我|替我|代我|请|请你)?(?:下单|买入|卖出|转账|提币|充值|划转|清仓|开仓|平仓|挂单|撤单|签名)"
)

识别策略(详见主架构 §17)

# src/finbayes/security/credential_detector.py
from dataclasses import dataclass
from typing import Optional

@dataclass
class DetectionResult:
matched: bool
reject_category: Optional[RejectCategory] = None
confidence: float = 0.0
matched_pattern: Optional[str] = None # 命中的 pattern 名(不存原文)

def detect_credentials(text: str) -> DetectionResult:
"""规则路径优先,LLM 辅助仅作可疑 case 的二次判断(M0 stub)"""
# 1. 优先级最高:私钥 hex
if PRIVATE_KEY_HEX.search(text):
return DetectionResult(True, "private_key_pattern", 0.95, "PRIVATE_KEY_HEX")
# 2. 助记词
if m := MNEMONIC_HEURISTIC.search(text):
# 交叉验证 BIP-39 词表
from .bip39_wordlist import BIP39_WORDS
words = m.group(0).split()
bip39_count = sum(1 for w in words if w in BIP39_WORDS)
if bip39_count >= len(words) * 0.9: # 90%+ 词在 BIP-39 词表中
return DetectionResult(True, "bip39_mnemonic_pattern", 0.95, "BIP39_WORDLIST_MATCH")
# 3. API Keys
if OPENAI_API_KEY.search(text):
return DetectionResult(True, "openai_api_key_pattern", 0.95, "OPENAI_API_KEY")
if ANTHROPIC_API_KEY.search(text):
return DetectionResult(True, "anthropic_api_key_pattern", 0.95, "ANTHROPIC_API_KEY")
# 4. 银行卡(Luhn 校验后)
for m in CARD_NUMBER_HEURISTIC.finditer(text):
if luhn_check(m.group(0)):
return DetectionResult(True, "luhn_card_number_pattern", 0.85, "LUHN_VALIDATED")
# 5. 配置粘贴
if CONFIG_PASTE_HEURISTIC.search(text):
return DetectionResult(True, "config_paste_with_secrets", 0.75, "CONFIG_PASTE_HEURISTIC")
# 6. 执行请求
if EXECUTION_REQUEST.search(text):
return DetectionResult(True, "execution_request_pattern", 0.85, "EXECUTION_REQUEST")
# 7. 通用 API key(仅在上下文有 "api_key"/"token" 关键词时触发)
if "api" in text.lower() or "token" in text.lower() or "secret" in text.lower():
if GENERIC_API_KEY.search(text):
return DetectionResult(True, "generic_api_key_pattern", 0.65, "GENERIC_API_KEY_HEURISTIC")
return DetectionResult(False)

约束

  • 任何 pattern 命中都不在返回值中存原文 —— 只存 matched_pattern 标签
  • LLM 辅助二次判断在 M0 stub(M2 上线)—— 任何 LLM 调用前都先经过这套规则路径
  • BIP-39 词表 source:Python mnemonic 库(pip install mnemonic);初始化时 from mnemonic import Mnemonic; BIP39_WORDS = set(Mnemonic("english").wordlist)
  • 输出端凭证样式扫描复用同一套 patterns(详见主架构 §17 输出端过滤 + arch-rewrite/ADR-010 accepted)

测试基线

tests/m0/test_credential_patterns.py 必须覆盖:

  • 5 条 §9 fixture 全部命中(True positive)
  • 至少 10 条 False positive 测试(如普通 hex 哈希值、64 位 SHA256、长 base64 数据但非 API key 等),验证不误伤
  • BIP-39 词表加载(启动期一次性)
  • Luhn 算法独立测试(5 个 valid + 5 个 invalid card numbers)

§11 M0 输出判定规则 v0

LLM-as-judge 在 M6 才上。M0 验收用退化规则 + 人工 checklist。

M0 验收分两层闸门,两层都过才算 M0 走通

  1. Schema 闸门(identity / 安全边界 / 字段存在):保留既有的字段非空 / 凭证反扫 / schema 反序列化等,守住「没泄露、字段齐、可序列化」。
  2. 能力达成闸门(答得好不好):至少一条样本必须证明产品最核心能力——给出条件化方向判断——真的被产出,而不只是「字段非空」。M0 之前的验收全是第 1 层,导致核心能力一次都没被验证;本次新增第 2 层填这个洞。

第 1 层 · Schema 闸门(Pydantic + grep · identity / 安全边界)

检查实现
main_answer 非空 + 长度 ≥30 字Pydantic Field(..., min_length=30)
invalidation_conditions 非空 listPydantic Field(..., min_length=1)
non_instruction_notice 存在默认值 + 输出端 hook 校验
schema_validation_passed == True综合层自检 + 输出端 hook 二次校验
输出端凭证样式 grep 0 命中§10 同一套 patterns 反向扫描
凭证 / 执行类输入 100% 拒收 + 凭证字符串零泄露m0_s3 / m0_s5 boundary_result.passed == False 且 audit payload 不含原文(§8 + §9)
structured_result_version == "1.1"§3.5 顶层字段固定值校验 + audit task_completed.structured_result_version 必填
1.1 M0 子集字段 schema 通过StructuredCognitionResult11Minimal 反序列化通过(最小子集字段在 §3.5 表中标 M0 实施列)
EvalHarness M0 评测器输出符合公式详见 EvalHarness 11 维评测公式表 §1 + §8.2 Pydantic 子模型;M0 阶段仅 D1 / D2(文本层降级)评测器落盘
IAA Cohen's kappa + bootstrap 接口实现EvalHarness 11 维评测公式表 §4.2 / §5;M0 留接口签名 + 抛 NotImplementedError,M1+ 落实现
6 条 sample fixture 真实样本对齐tests/m0/fixtures/samples/m0_s1_eth_weekly.yaml / m0_s2_btc_explain.yaml / m0_s3_credential_reject.yaml / m0_s4_btc_etf_flow.yaml / m0_s5_unsupported.yaml / m0_s6_eth_reduce_directional.yaml(M0 实施 schema 见 §7 末 fixture 段;上述路径指向工程仓,治理库本仓只锁定 §7 模板 + §8 pytest_check 表达式作为 acceptance condition,fixture 文件由 C-1 task packet 落盘到工程仓,详见 §7「fixture 文件位置约定」段)

第 2 层 · 能力达成闸门(答得好不好 · M0 必走,至少 1 条)

第 1 层只证明「字段存在」,不证明「能力达成」。下表把可升级的断言从「字段存在」改写为「应给出 X 能力」的正向判定;其中 m0_s6 的条件化方向判断是 M0 唯一一条核心能力达成判定,必须过。

能力达成判定适用样本可判定实现(正向:应给出 X 能力,而非「字段存在」)
应给出条件化方向判断(核心能力,M0 必过)m0_s6main_answer 能解析出方向倾向(含「偏向 / 倾向 / 减」+「条件 / 若 / 在 / 前提」)四要素齐备:invalidation_conditions≥1(失效条件)+ counter_evidence≥1(反方)+ uncertainty_and_gaps≥1(不确定性)。即 §8 m0_s6.expected_capabilities 全部 pytest_check 通过;不接受仅 main_answer 非空
应进入正常综合而非被拒收(方向判断 ≠ 执行请求)m0_s6boundary_result.passed and task is not None——与 m0_s5 执行类拒收形成对照:核心能力闸门要求方向判断被边界 hook 误拒
应给出实质反方证据(真对立,非补充说明)m0_s1 / m0_s4 / m0_s6len(result.counter_evidence) >= 1(人工 checklist 第 2 项进一步判「实质反方 / 形式反方」)
应给出可观测失效条件m0_s1 / m0_s6len(result.invalidation_conditions) >= 2m0_s1)/ >= 1m0_s6);人工 checklist 第 3 项判「可观测 / 模糊」
应命中题眼实体 + 任务焦点(非泛泛复述)m0_s1 / m0_s4'ETH'/'BTC' in result.main_answer 且含任务焦点关键词(时间窗 / ETF),由 §8 各样本 pytest_check 落地

能力达成闸门通过条件m0_s6 的 6 条 expected_capabilities pytest_check 全部通过(核心能力达成的硬门);其余样本的能力达成断言并入既有 §8 sample runner,作为「应给出 X 能力」的正向回归项。

人工 checklist(M0 验收必走)

对 6 条样例输入的输出,工作流维护者人工评估(凭证 / 执行拒收样本 m0_s3 / m0_s5 走拒收专项核对,不套用下列认知输出 5 项;认知输出样本 m0_s1 / m0_s2 / m0_s4 / m0_s6 走下列 5 项,m0_s6 额外核对「条件化方向倾向是否真给出」):

# M0 验收 checklist - 样例 {sample_id}
**评分人**: {claude_code | codex | 工作流维护者}
**评分时间**: {ISO 8601}
**Prompt 版本**: {prompt_id}@{prompt_version}

- [ ] 第一屏(前 200 字)回答了用户的题眼?(YES / NO)
- [ ] 反方证据是真"对立证据"还是只是"补充说明"?(实质反方 / 形式反方 / 缺失)
- [ ] 失效条件是用户可观测的?(可观测 / 模糊 / 缺失)
- [ ] 信息缺口是否诚实?(诚实 / 强行编 / 未标注)
- [ ] 输出格式契约(schema_version / non_instruction_notice)齐备?(齐 / 缺)
- [ ] (仅 `m0_s6`)是否真给出「在 X 条件下偏向减仓 / 在 Y 条件下不减」的条件化方向倾向,而非泛泛分析或拒答?(实质方向 / 泛泛分析 / 拒答)

评分约定

  • 认知输出样本 4 条(m0_s1 / m0_s2 / m0_s4 / m0_s6)× 5 项 = 20 个判定;m0_s6 额外 +1 条件化方向倾向判定(核心能力达成,必须为「实质方向」)
  • 合格阈值:≥18/20(含至少 3 条样例全部 YES/实质/可观测/诚实/齐) m0_s6 条件化方向倾向判定 == 实质方向(核心能力一票否决)
  • 评分人分歧时:第三方仲裁(多 Agent 模拟视角 + 工作流维护者裁决)
  • 评分结果归档:tests/m0/checklist_archive/{date}-{sample_id}-{reviewer}.md

M6 评估闭环上线后切换到 LLM-as-judge + 自动评分。


§12 工程实施仓发现路径协议

主架构 §27 说"工程实施仓物理路径由维护者本地记录"。M0 task packet 必须携带工程实施仓绝对路径,否则实施 Agent 无 cwd。

M0 task packet 必备字段

# 由 Claude Code 起 OpenSpec 提案时填写
task_packet:
milestone: M0
working_directory: <工程实施仓绝对路径> # 必填
user_memory_ref: "finbayes_repos.md" # 维护者级 memory 中的路径登记
knowledge_repo: <Labs-FinTecAI 仓绝对路径> # 必填,本工程包 + 主架构所在
m0_engineering_pack: "projects/finbayes/engineering/engineering-packs/m0-walking-skeleton.md"
upstream_architecture: "projects/finbayes/engineering/architecture.md"
upstream_anchors:
- "主架构 §25 M0 走通骨架"
- "主架构 §17 边界与安全"
- "主架构 §27 代码仓位置映射"
acceptance_yaml: "tests/m0/sample_inputs.yaml" # 由 §8 派生
ddl_path: "migrations/001_initial.sql" # 由 §4 派生
credential_negative_yaml: "tests/m0/credential_negatives.yaml" # 由 §9 派生

路径登记 fallback

工程实施仓的实际物理路径不写入本仓(CLAUDE.md 硬约束)。维护者级 user memory finbayes_repos.md 是唯一登记位置。实施 Agent 接手时由维护者注入 task packet。


§13 工程实施仓 Bootstrap 模板(新增 P0)

让工程实施 Agent 在第 1 小时跑通 finbayes init 所需的所有"选型决策"。

pyproject.toml 模板

[project]
name = "finbayes"
version = "0.1.0"
description = "FinBayes 金融认知层 — 帮助个人投资者持续形成、校准、更新自己的金融判断"
requires-python = ">=3.11"
dependencies = [
"pydantic>=2.5,<3.0",
"click>=8.1,<9.0",
"httpx>=0.27,<1.0",
"aiosqlite>=0.20,<1.0",
"structlog>=24.1,<25.0",
"mnemonic>=0.20,<1.0", # BIP-39 词表
"keyring>=24.0,<26.0", # OS Keychain 集成
"rich>=13.0,<14.0", # CLI 流式输出 + TUI 后续
]

[project.optional-dependencies]
dev = [
"pytest>=8.0,<9.0",
"pytest-asyncio>=0.23,<1.0",
"ruff>=0.5,<1.0",
"mypy>=1.10,<2.0",
"freezegun>=1.4,<2.0",
]

[project.scripts]
finbayes = "finbayes.__main__:cli"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.ruff]
line-length = 100
target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "W", "I", "B", "UP", "PL", "RUF"]
ignore = ["E501"] # line-length 由 ruff format 处理

[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_ignores = true

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

包管理器 / 依赖工具

M0 选定
包管理器uv(推荐) / pip + pyproject.toml(fallback)
构建后端hatchling
CLI 框架click(不用 typer)
HTTP 客户端httpx(async-first,支持 middleware 注入 trace 头)
SQLite 异步绑定aiosqlite
日志structlog(脱敏 processor + JSON 输出)
Keychainkeyring
测试pytest + pytest-asyncio
Lint / Formatruff(含 ruff format)
Type Checkmypy --strict
时间冻结freezegun

全局 Pydantic 配置策略

# src/finbayes/_pydantic_config.py
from pydantic import ConfigDict

BASE_MODEL_CONFIG = ConfigDict(
extra='forbid', # 禁止额外字段(防 schema drift)
strict=True, # 类型严格匹配
validate_assignment=True, # 修改字段时重新校验
frozen=False, # M0 允许修改(M1+ 评估是否 freeze)
)

# 所有业务 BaseModel 在文件顶部 import 后使用:
# class MyModel(BaseModel):
# model_config = BASE_MODEL_CONFIG

自定义 Exception 类层级

# src/finbayes/exceptions.py
from typing import Literal

class FinBayesError(Exception):
"""所有 FinBayes 自定义异常的基类。

默认 exit_code 落入 70-79 命名空间(详见 §6「退出码命名空间约定」),
与未捕获的 Python `Exception` 兜底退出码(70)显式区分。
"""
exit_code: int = 71 # 命名空间 70-79;70 保留给 CLI 兜底 except Exception

class InvalidInputError(FinBayesError):
exit_code = 10

class ProviderNotReadyError(FinBayesError):
exit_code = 20

class BoundaryRejectError(FinBayesError):
exit_code = 30
def __init__(self, reject_category: str, *args):
self.reject_category = reject_category
super().__init__(*args)

class TaskFailedError(FinBayesError):
exit_code = 40

class AuditWriteFailedError(FinBayesError):
"""审计写入主路径 + JSONL fallback 同时失败(详见 §13 AuditWriter)。"""
exit_code = 90

# CLI 入口的 try/except 模板:
# try:
# ...
# except FinBayesError as e:
# sys.exit(e.exit_code)
# except Exception as e:
# # 未预期异常兜底;与 FinBayesError 基类默认值(71)显式区分
# sys.exit(70)

Trace ID 生成

# src/finbayes/observability/trace.py
import uuid
import contextvars
from contextvars import ContextVar

# M0 选定 uuid4(简单 + 全球唯一,不依赖时间同步 / 主机标识)
# 后续若需排序友好可升级 ULID(M2+)

def new_task_id() -> str:
return f"task_{uuid.uuid4().hex[:12]}"

def new_session_id() -> str:
return f"sess_{uuid.uuid4().hex[:12]}"

def new_audit_event_id() -> str:
return f"evt_{uuid.uuid4().hex[:12]}"

# ContextVar 体系(详见主架构 §12)
trace_task_id: ContextVar[str] = ContextVar("task_id")
trace_session_id: ContextVar[str] = ContextVar("session_id")
trace_user_id: ContextVar[str] = ContextVar("user_id")

单用户模式约定

M0 仅服务单用户:

# src/finbayes/state/constants.py
SINGLE_USER_ID = "local" # 第一阶段所有 user_id 字段固定值
DEFAULT_SESSION_NAME = None # 默认 Session 不命名

日志脱敏配置

# src/finbayes/observability/logging.py
import structlog
import re
from .credential_patterns import (
PRIVATE_KEY_HEX, OPENAI_API_KEY, ANTHROPIC_API_KEY, CARD_NUMBER_HEURISTIC,
)

REDACT_PATTERNS = [
(PRIVATE_KEY_HEX, "[REDACTED_PRIVATE_KEY]"),
(OPENAI_API_KEY, "[REDACTED_OPENAI_KEY]"),
(ANTHROPIC_API_KEY, "[REDACTED_ANTHROPIC_KEY]"),
(CARD_NUMBER_HEURISTIC, "[REDACTED_CARD_NUMBER]"),
]

def redact_processor(logger, method_name, event_dict):
for k, v in event_dict.items():
if isinstance(v, str):
for pattern, replacement in REDACT_PATTERNS:
v = pattern.sub(replacement, v)
event_dict[k] = v
return event_dict

structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
redact_processor,
structlog.processors.JSONRenderer(),
],
)

异步审计写入模式

# src/finbayes/state/audit.py
import asyncio
import json
import sys
from pathlib import Path
from .types import AuditEvent
from ..exceptions import AuditWriteFailedError

# 边界事件同步写
SYNC_EVENT_TYPES = {"boundary_rejected"}

# fallback JSONL 备份文件位置(append-only,启动 sync_back_jsonl 时被回放)
AUDIT_JSONL_FALLBACK = Path(".finbayes/audit_fallback.jsonl")

class AuditWriter:
def __init__(self, db):
self.db = db
self._async_queue: asyncio.Queue[AuditEvent] = asyncio.Queue(maxsize=1000)
self._writer_task: asyncio.Task | None = None

async def append(self, event: AuditEvent):
if event.event_type in SYNC_EVENT_TYPES:
await self._write_sync(event)
else:
await self._async_queue.put(event)

async def _write_sync(self, event: AuditEvent):
"""同步写入(凭证拒收等关键事件) —— 含三层 fallback,零丢失承诺:
- 主路径:SQLite append(audit_events 表)
- fallback 1:本地 JSONL append-only 备份文件,稍后由 _sync_back_jsonl 回放
- fallback 2:stderr append + raise AuditWriteFailedError,最坏情况退出
(exit code 见 §15;boundary_rejected 类事件仍按 §15 exit code 表,
内部审计故障 90+ 由 AuditWriteFailedError 抛出后 CLI 入口决定)
"""
try:
async with self.db.cursor() as cur:
await cur.execute("INSERT INTO audit_events ... VALUES (...)")
await self.db.commit()
return
except Exception as primary_err: # noqa: BLE001 — 主路径失败必须 fallback
pass
# fallback 1:JSONL append-only
try:
AUDIT_JSONL_FALLBACK.parent.mkdir(parents=True, exist_ok=True)
with AUDIT_JSONL_FALLBACK.open("a", encoding="utf-8") as fp:
fp.write(json.dumps(event.model_dump(mode="json"), ensure_ascii=False) + "\n")
fp.flush()
return
except Exception as jsonl_err: # noqa: BLE001 — fallback1 失败继续走 fallback2
pass
# fallback 2:stderr 兜底(避免静默丢失)+ raise(exit code 表见 §15)
sys.stderr.write(
"[AUDIT_WRITE_FAIL] " + json.dumps(event.model_dump(mode="json"), ensure_ascii=False) + "\n"
)
sys.stderr.flush()
raise AuditWriteFailedError(
"audit write failed in primary (SQLite) and fallback1 (JSONL); stderr-only"
)

async def _sync_back_jsonl(self):
"""启动时回放 audit_fallback.jsonl 内未落盘的事件到 SQLite,成功后清空。"""
if not AUDIT_JSONL_FALLBACK.exists():
return
# 按行回放 → 落入 SQLite → 删除原文件(M0 简化:read all + atomic replace)
# 详细实现见工程实施仓 src/finbayes/state/audit_recovery.py
pass

async def start_writer(self):
"""后台 async writer"""
async def _drain():
while True:
event = await self._async_queue.get()
await self._write_sync(event) # 内部复用同一 SQL + fallback
self._writer_task = asyncio.create_task(_drain())

约束

  • 主路径成功 → return;任一 fallback 路径成功也 return;只有 fallback 2 仍失败才 raise AuditWriteFailedError(对应 §15 audit 故障类 exit code 90)。
  • audit_fallback.jsonl 是本地 append-only 文件,与 SQLite WAL 独立(SQLite 整库锁死 / 磁盘满 / migration 中也能写入);启动时由 _sync_back_jsonl 异步回放,回放成功后原子删除。
  • boundary_rejected 事件仍按 §5 同步写约束 + §15 退出码(exit code 30)流转;audit 写入失败不改写 boundary 拒收的对用户语义,但额外触发 audit 故障告警(仅记录,不阻塞用户回应)。

§14 M0 三检 Review Gate CI 模板

# .ci/m0-gate.yml(工程实施仓侧)
name: M0 Three-Gate Review

on: [pull_request]

jobs:
gate-1-strategic-fidelity:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 禁词 grep(机械断言)
run: |
! git grep -nE "认知协作伙伴|行动准备|行动判断|行动方案|情绪桥|信任债|零状态前提" -- 'src/**/*.py' 'tests/**/*.py' 'docs/**/*.md' 'prompts/**/*'
- name: 凭证字段名 grep(机械断言)
run: |
# 凭证字段名在源码中应该只出现在白名单文件
! git grep -nE "private_key|mnemonic|api_secret" -- 'src/**/*.py' \
':!src/finbayes/security/credential_patterns.py' \
':!src/finbayes/security/credential_detector.py' \
':!tests/m0/credential_negatives.yaml' \
':!tests/m0/test_credential_patterns.py'
- name: SQLite payload 凭证扫描(运行时)
run: |
pytest tests/m0/test_credential_isolation.py -v
- name: 执行类 category 静态分析
run: |
python scripts/audit_capability_registry.py --strict
- name: 综合层 schema 必填字段校验
run: |
pytest tests/unit/test_structured_cognition_result_schema.py -v
- name: 工具 ToolCategory 枚举无 execution
run: |
python -c "from finbayes.capabilities.types import ToolCategory; import typing; assert 'execution' not in typing.get_args(ToolCategory)"

gate-2-contract-regression:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 生成 / 对比 baseline
# M0 仅生成 baseline 不对比(详见 §14.6)
# M1+ 起进入对比模式
run: |
python scripts/audit_contract_regression.py --baseline=m0-baseline --mode=generate
- name: contract_version 校验
run: |
pytest tests/contract/ -v

gate-3-cognitive-quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: M0 6 条样例(含 m0_s6 条件化方向判断能力达成)+ 5 类凭证 negative test
run: |
pytest tests/m0/ -v -m "m0"
- name: schema_validation_passed assertion
run: |
pytest tests/m0/test_synthesis_output_schema.py -v

约束

  • 三个 gate 顺序无关任一失败 = block merge
  • gate-1 含战略保真度(禁词 + 凭证字段 + 执行类工具 + 输出 schema + SQLite 凭证扫描)
  • gate-2 在 M0 仅生成 baseline 不对比(详见 §14.6)
  • gate-3 是认知质量(M0 退化规则 + 6 样例含 m0_s6 能力达成闸门 + 5 凭证 negative;人工 checklist 不在 CI 而在 M0 验收环节)

§14.5 CI 接口规范(7 个未提供脚本/测试的接口定义,新增 P0)

§14 CI 模板调用的脚本与测试文件,本节给出接口规范(不给完整实现,但锁定函数签名、断言、期望输出)。

1. scripts/audit_capability_registry.py

用途:静态分析工具注册代码,确保无 execution category 工具注册请求。

接口

# scripts/audit_capability_registry.py
"""
用法: python scripts/audit_capability_registry.py [--strict]

扫描 src/finbayes/capabilities/tools/ 下所有工具文件,
确认每个工具 ToolSpec.category != 'execution' 且参数 schema 不含
'private_key' / 'mnemonic' / 'api_secret' 等字段名。

--strict 模式额外检查:
- 工具调用链中不出现 sign / submit_order / transfer 等执行类动词
- 工具 description 不含 "buy" / "sell" / "place order" 等执行类语义

退出码:
- 0: 全部通过
- 1: 发现违规(含违规列表 + 文件路径 + 行号)
"""

断言

  • 每个 ToolSpec.category ∈ {"read_only", "analysis", "synthesis", "clarify", "state_action"}
  • 工具参数 schema 字段名集合 ∩ {"private_key", "mnemonic", "api_secret", "wallet_address", "exchange_credentials"} = ∅
  • --strict 时:工具 description 经 AST + grep 双层扫描确认无执行语义

2. scripts/audit_contract_regression.py

用途:契约回归审计(schema / API / DDL diff vs baseline)。

接口

# scripts/audit_contract_regression.py
"""
用法:
--baseline=m0-baseline --mode=generate # M0 阶段生成 baseline 到 .baseline/m0-contract.json
--baseline=m0-baseline --mode=compare # M1+ 阶段对比当前与 baseline

生成内容(.baseline/m0-contract.json):
{
"schema_version": 1,
"generated_at": "<ISO 8601>",
"pydantic_models": {
"NormalizedInput": { "fields": [...], "schema_version": 1 },
...
},
"ddl_tables": {
"sessions": { "columns": [...], "indexes": [...] },
...
},
"cli_commands": [...],
"exit_codes": {...},
"tool_specs": [...]
}

退出码:
- 0: 无破坏性差异
- 1: 发现破坏性变更(字段删除 / 类型变更 / required 字段新增 / DDL 表/列删除)
- 2: 仅有兼容性增加(minor 升级允许)
"""

断言 (M1+ compare 模式):

  • Pydantic 字段未删除
  • 字段类型未变更
  • 没有新增 required 字段(仅 optional 字段可加)
  • DDL 表 / 列未删除
  • CLI 退出码语义未变

3. tests/unit/test_structured_cognition_result_schema.py

断言

# 期望测试用例
def test_main_answer_min_length_30():
with pytest.raises(ValidationError):
StructuredCognitionResult(main_answer="短") # < 30 字

def test_invalidation_conditions_required():
with pytest.raises(ValidationError):
StructuredCognitionResult(main_answer="...", invalidation_conditions=[])

def test_extra_fields_forbidden():
with pytest.raises(ValidationError):
StructuredCognitionResult(main_answer="...", invalidation_conditions=["X"], extra_field="value")

def test_non_instruction_notice_default():
r = StructuredCognitionResult(main_answer="..." * 10, invalidation_conditions=["X"])
assert "认知材料" in r.non_instruction_notice

4. tests/contract/(目录)

用途:契约级测试(跨模块接口稳定性)。

接口规范

tests/contract/
├── test_subsystem_interfaces.py # 6 子系统接口签名稳定
├── test_pydantic_schema_stability.py # 所有 BaseModel schema_version 字段存在 + 默认值匹配(1.0 主体 = 1;§3.5 *M0 系列 = "1.1-m0")
├── test_cli_exit_codes.py # 退出码语义不变
└── conftest.py # 共享 fixture

断言

def test_input_pipeline_normalize_signature():
from finbayes.io import normalize_input
import inspect
sig = inspect.signature(normalize_input)
assert "raw_input" in sig.parameters
assert sig.return_annotation == NormalizedInput

def test_all_pydantic_models_have_schema_version():
from finbayes import (
NormalizedInput, BoundaryResult, Task, ToolCall, LLMCall,
EvidencePacket, StructuredCognitionResult, Session, FinObject,
AuditEvent, ProviderReadiness, LLMResponse, ToolResponse,
ClarifyResponse, Chunk, ToolSpec,
)
# §3 主体(1.0):schema_version: int = 1
for model in [NormalizedInput, BoundaryResult, ...]:
assert "schema_version" in model.model_fields
assert model.model_fields["schema_version"].default == 1

# §3.5 1.1 最小子集(*M0 系列):schema_version: Literal["1.1-m0"] = "1.1-m0"
# 通过 _BaseM0 继承统一注入,C-1 阶段不再要求 default == 1。
from finbayes.cognition.v11 import (
PhaseEvidenceM0, NodeM0, EdgeM0, TransmissionGraphM0,
PillarApplicabilityM0, ApplicabilityFlagsM0,
PosteriorModeM0, BimodalPosteriorM0,
NarrativeNumberConsistencyM0, MCABucketM0,
StructuredCognitionResult11Minimal,
)
for model in [
PhaseEvidenceM0, NodeM0, EdgeM0, TransmissionGraphM0,
PillarApplicabilityM0, ApplicabilityFlagsM0,
PosteriorModeM0, BimodalPosteriorM0,
NarrativeNumberConsistencyM0, MCABucketM0,
StructuredCognitionResult11Minimal,
]:
assert "schema_version" in model.model_fields
assert model.model_fields["schema_version"].default == "1.1-m0"

5. tests/m0/test_credential_isolation.py

用途:验证凭证字符串不进入任何后续路径。

接口规范

"""
对每条 §9 凭证 fixture,跑完整 task 流程,验证:
1. boundary_result.passed == False
2. 凭证字符串不出现在 audit_events.payload(SQLite 扫描)
3. 凭证字符串不出现在 fixtures/llm/ 目录(grep)
4. 凭证字符串不出现在 logs/ 目录(grep)
5. 日志 redact_processor 正确替换为 [REDACTED_*]
"""

@pytest.mark.parametrize("fixture", load_credential_fixtures())
async def test_credential_not_in_audit_payload(fixture, tmp_db, audit_writer):
# 模拟用户输入含凭证
raw_input = fixture["input_text"]
boundary_result = run_input_boundary_hook(raw_input)
assert not boundary_result.passed
assert boundary_result.reject_category == fixture["expected_reject_category"]

# 写入审计(应为同步写入 boundary_rejected)
await audit_writer.append(...)

# SQLite 扫描
async with tmp_db.cursor() as cur:
await cur.execute("SELECT payload FROM audit_events")
rows = await cur.fetchall()
all_payload = "".join(r[0] for r in rows)
# 验证凭证子串不在 payload 中
assert "0xabcdef" not in all_payload # for private_key fixture
# ... 类似检查其他 fixture

6. tests/m0/test_synthesis_output_schema.py

断言

async def test_synthesis_output_passes_schema_validation():
"""每条 §8 样例输入跑完,输出应通过 Pydantic schema validation"""
for sample in load_samples():
task = await dispatch_task(...)
result = await run_task(task)
assert isinstance(result, StructuredCognitionResult)
assert result.schema_validation_passed
assert len(result.main_answer) >= 30
assert len(result.invalidation_conditions) >= 1

7. tests/m0/sample_inputs.yaml runner

接口规范

# tests/m0/test_samples_run.py
"""
读取 tests/m0/sample_inputs.yaml,对每条 sample:
1. 跑完整 task 流程(CLI ask 命令)
2. 拿到 StructuredCognitionResult 或 BoundaryResult
3. eval sample['expected_capabilities'][i]['pytest_check'] 表达式
4. 全部通过则 sample 通过
"""

import yaml

@pytest.mark.m0
@pytest.mark.parametrize("sample", load_samples_yaml())
async def test_sample(sample, cli_runner):
if sample["scenario"].startswith("S6") or sample["scenario"].startswith("边界拒收"):
# 边界拒收样例
boundary_result = await cli_runner.run_boundary(sample["input_text"])
task = None
result = None
else:
# 正常任务样例
result = await cli_runner.run_task(sample["input_text"])
boundary_result = None
task = result.task

audit_events = await cli_runner.get_audit_events()

for cap in sample["expected_capabilities"]:
# eval pytest_check 表达式
# 安全约束:必须显式 `__builtins__: {}` 封锁 Python 默认注入的内置命名空间,
# 否则 fixture 内的 pytest_check 字符串可通过 __import__("os").system(...) 等
# 路径逃逸 allowlist;本 runner 的 eval **仅用于 fixture 校验**,allowlist 函数
# 仅 any / all / len 三个。M1+ 若需复杂校验请切换到完整 DSL parser,不再用 eval。
safe_globals = {
"__builtins__": {}, # 显式封锁所有内置,allowlist 严格生效
"result": result,
"task": task,
"boundary_result": boundary_result,
"audit_events": audit_events,
"any": any, "all": all, "len": len,
}
check_result = eval(cap["pytest_check"], safe_globals)
assert check_result, f"{sample['sample_id']}: {cap['assertion']} failed"

安全说明(sample runner eval 沙箱)

  • eval 的 globals dict 必须显式声明 "__builtins__": {};否则 CPython 会自动注入完整 builtins,导致 __import__("os").system(...) 等逃逸路径仍可用,allowlist 名存实亡。
  • sample runner 的 eval 仅消费 §8 / §9 治理库内、经评审的 fixture YAML,不接受任何运行时用户输入;攻击面在「fixture 文件被恶意 PR 注入表达式」一个入口,由 PR review + npm run verify:kb 联防。
  • M1+ 若需要更复杂的字段断言(含跨字段语义、容差判定),切换到工程仓内独立的 DSL parser(不再用 eval)。

§14.6 M0 baseline 文件位置与格式(新增 P0)

audit_contract_regression.py 在 M0 与 M1+ 的行为:

阶段模式行为
M0--mode=generate扫描所有 Pydantic / DDL / CLI / ToolSpec,生成 .baseline/m0-contract.json 并 commit
M1+--mode=compare读取 .baseline/m0-contract.json + 当前 schema → diff

M0 baseline 文件位置:工程实施仓的 .baseline/m0-contract.json(gitignore = false,必须 commit)

示例 schema(不是完整内容,结构示意):

{
"schema_version": 1,
"milestone": "M0",
"generated_at": "2026-XX-XXT00:00:00Z",
"finbayes_version": "0.1.0",
"pydantic_models": {
"NormalizedInput": {
"fields": [
{"name": "schema_version", "type": "int", "required": true, "default": 1},
{"name": "user_id", "type": "str", "required": true},
{"name": "session_id", "type": "str", "required": true},
{"name": "entry", "type": "Literal[cli,tui,web,mcp,channel]", "required": true},
{"name": "raw_text", "type": "str", "required": true},
{"name": "attachments", "type": "list[str]", "required": false, "default": []},
{"name": "timestamp", "type": "datetime", "required": true},
{"name": "task_id", "type": "str", "required": true}
]
}
},
"ddl_tables": {
"sessions": {
"columns": [
{"name": "session_id", "type": "TEXT", "primary_key": true},
{"name": "user_id", "type": "TEXT", "not_null": true},
{"name": "name", "type": "TEXT"},
{"name": "state", "type": "TEXT", "check": "IN (...)"},
{"name": "created_at", "type": "TEXT", "not_null": true},
{"name": "last_active_at", "type": "TEXT", "not_null": true},
{"name": "turn_count", "type": "INTEGER", "default": 0},
{"name": "is_default", "type": "INTEGER", "default": 0}
],
"indexes": ["idx_sessions_user_state"]
}
},
"cli_commands": ["finbayes ask", "finbayes status", "finbayes init", "finbayes session list", "finbayes audit list"],
"exit_codes": {"0": "success", "10": "invalid_input", "20": "provider_not_ready", "30": "boundary_reject", "40": "task_failed", "70": "internal_error_unhandled", "71": "finbayes_error_default", "90": "audit_write_failed"},
"tool_specs": []
}

§15 与主架构 / ADR 的承接

  • 主架构 §25 M0 范围与本文档 §1 互为权威 —— 任何冲突以本文档 §1 表为准(特别是 M0 降级范围 L1 only)
  • 主架构 §17 边界与安全 + 本文档 §10 凭证正则基线互为承接 —— 主架构定义阻断契约,本文档给可执行 pattern
  • 主架构 §18 可观测性 + 本文档 §5 audit payload schema 互为承接 —— 主架构定义 trace ID 体系,本文档给字段集
  • ADR-001 工程范式(Walking Skeleton + 里程碑切片 + 三检 review gate)—— 本文档是 ADR-001 在 M0 层的 task-oriented 落地
  • ADR-003 工程协作(OpenSpec + Codex + Claude Code)—— 本文档 §12 task packet 字段是 ADR-003 实施承接
  • arch-rewrite/ADR-008 Provider 接口(accepted 2026-05-28)—— 本文档 §3 LLMResponse / ToolResponse 是 ADR-008 的 M0 minimum 版本
  • whitepaper-rewrite/ADR-008 supplement 机制层输出契约扩展(accepted 2026-05-28)—— 本文档 §3.5 是 supplement 的 M0 最小子集落地;完整 1.1 字段表与字段间约束见 StructuredCognitionResult 1.1 契约源
  • arch-rewrite/ADR-010 输出端凭证过滤位置(accepted 2026-05-28)—— 本文档 §10 复用同一套 patterns 反向扫描

演化路径

时机本文档变化
M0 完成进入 superseded 状态,但保留作历史参考 + M0 复盘资产
M1 起派生 m1-state-confirmation.md 等后续 milestone 工程包
M6 评估闭环上线§11 输出判定规则升级,LLM-as-judge 替代人工 checklist
M7 演化能力上线audit_contract_regression.py compare 模式上线,gate-2 真正生效

变更记录

日期变更
2026-05-29M0 验收加能力达成判定 + 补带方向判断样例(W2-1):§8 新增 m0_s6_eth_reduce_directional(锚定主架构 §6 S6/S7 条件化方向判断,crypto-only 落地),断言条件化方向 + 失效条件 + 反方 + 不确定性四要素齐备,并与 m0_s5 执行类拒收明确区分;§11 验收从单层 schema 闸门升为两层(schema 闸门保留 identity / 安全边界 + 新增能力达成闸门),m0_s6 条件化方向判断为 M0 唯一核心能力达成硬门;人工 checklist 同步加条件化方向倾向判定。