feat(v2): T04 模拟案例生成与数据集划分
先边界 fixture 再批量生成;全程程序组合场景,**不做任何模型调用**(14 §8 末)。 新增 services/simulation_dataset.py: - generate(seed, scale):程序化生成虚构项目,同 seed 同版本**逐条一致** - split():按关联组 + 时间分桶(组内不拆),以组的最早发生时间排序 - assert_group_integrity / assert_source_allowed:组不跨集合;保留集来源禁止用于提炼 - manifest():构造 DatasetManifest 契约体 契约补 T04 才该有的部分(T00/04 当初有意推迟):EventOutcome 七值枚举、 OutcomeCount、DatasetManifest。含两条硬约束——合成数据必须 demo 域(BR-34)、 计数不得为 0(空结果用不适用表达)。 配比按 14 §8:通过 1800(直接 900 + 补件后 900)/ 未通过 600 / 不计入 600 (附条件 100 + 未送审 175 + 退出 175 + 待定 150)= 3000。 两个建模决定: - "补件后通过"是 approved 的**子类型**(outcome_subtype),不是独立 outcome —— 06 §3 要求区分首次提交与补件后,但设计 §9.1 的 EventOutcome 只有 7 个值,不自扩充 - conditional 是否计入目标由客户映射决定(OQ-05),故 label=None,绝不默认当通过 实现中被自己的检查抓到两个 bug: 1. 分组与近重复用了两套独立公式,导致 near_duplicate_of 指向别组记录, assert_group_integrity 直接报"近重复案例跨集合"。现组号与近重复由同一序号唯一推出 2. split() 末段取剩余可能为负(极端小样本),会静默切出错误的划分。已夹到非负 测试:新增 test_simulation_dataset.py 24 项(边界 fixture 10 + 生成 10 + 清单 4 + 落盘 2),含 AC-35 的可复现性与 AC-36 的组不跨集合/保留集来源拒绝。 全量 272 项 OK(248 → 272);verify.py 10/10 PASS。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,22 @@ class DataDomain(StrEnum):
|
||||
production = "production"
|
||||
|
||||
|
||||
class EventOutcome(StrEnum):
|
||||
"""评审会结果编码(docs/v2/03-detailed-design.md §9.1)。
|
||||
|
||||
`conditional`(附条件通过)是否计入目标由 MappingProfile 固定;P0 **不得**默认
|
||||
当作 approved(E-26 / BR-21)。
|
||||
"""
|
||||
|
||||
approved = "approved"
|
||||
rejected = "rejected"
|
||||
deferred = "deferred"
|
||||
withdrawn = "withdrawn"
|
||||
not_submitted = "not_submitted"
|
||||
conditional = "conditional"
|
||||
unknown = "unknown"
|
||||
|
||||
|
||||
class Ref(Contract):
|
||||
"""通用版本引用(docs/v2/03-detailed-design.md §9.1)。"""
|
||||
|
||||
@@ -311,3 +327,42 @@ class FactConflict(Contract):
|
||||
if stray or self.selected_evidence_ids:
|
||||
raise ValueError("unresolved 不得带解决字段")
|
||||
return self
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 数据集(T04)
|
||||
class OutcomeCount(Contract):
|
||||
outcome: EventOutcome
|
||||
count: int = Field(ge=0)
|
||||
|
||||
|
||||
class DatasetManifest(Contract):
|
||||
"""数据集清单(docs/v2/03-detailed-design.md §9.2「数据集服务」)。
|
||||
|
||||
用于证明"同 seed / 同版本生成两次,规范化清单一致"(AC-35),并承载划分信息。
|
||||
"""
|
||||
|
||||
id: Identifier
|
||||
version: str = Field(min_length=1, max_length=ID_MAX)
|
||||
data_domain: DataDomain
|
||||
seed: int | None = Field(default=None, ge=0)
|
||||
generator_version: str | None = Field(default=None, max_length=ID_MAX)
|
||||
template_version: str | None = Field(default=None, max_length=ID_MAX)
|
||||
source_hash: str = Field(pattern=_SHA256.pattern)
|
||||
split_id: str = Field(min_length=1, max_length=ID_MAX)
|
||||
split_policy_version: str = Field(min_length=1, max_length=ID_MAX)
|
||||
group_assignments_ref: str = Field(min_length=1, max_length=ID_MAX)
|
||||
counts: list[OutcomeCount] = Field(default_factory=list, max_length=32)
|
||||
created_at: AwareDatetime
|
||||
synthetic: bool
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_labels_and_domain(self) -> DatasetManifest:
|
||||
outcomes = [item.outcome for item in self.counts]
|
||||
if len(set(outcomes)) != len(outcomes):
|
||||
raise ValueError("同一结果编码不得重复计数")
|
||||
if any(item.count == 0 for item in self.counts):
|
||||
raise ValueError("计数为 0 的结果不应出现;空结果用不适用表达")
|
||||
# 合成数据必须标 demo:不得让模拟数据冒充生产域(BR-34)
|
||||
if self.synthetic and self.data_domain != DataDomain.demo:
|
||||
raise ValueError("合成数据必须为 demo 数据域")
|
||||
return self
|
||||
|
||||
Reference in New Issue
Block a user