Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faba3ba78a | ||
|
|
915d719a64 | ||
|
|
7371efbf9a | ||
|
|
494c85e001 | ||
|
|
aa9767a5c2 |
@@ -24518,3 +24518,18 @@
|
||||
- 防复发:上述测试;合同 docstring 写明后续 #174–#178 的注册步骤。
|
||||
- 关联记录:占星师 2026-11-07 Q9、2026-11-08 第五轮第 9 条;FIX-ondemand-transit-date-1(产品拍板 C 方案)
|
||||
- 修复版本:分支 codex/ondemand-transit-date-20261109
|
||||
|
||||
## BUG-1833 | 出生时间只能校到唯一分钟:时间范围对当前问题已经够用时也停不下来
|
||||
|
||||
- 状态:fixed-pending-verify(分支 codex/purpose-gate-contract-20261109;**仅合同部分,未接入**)
|
||||
- 首次发现 / 最近更新:2026-11-09(占星师提出,产品负责人同意)/ 2026-11-09(合同与测试)
|
||||
- 影响面:新增 scripts/rectification_purpose_gate.py 与 tests/test_rectification_purpose_gate.py;本单不改任何现有文件的行为。
|
||||
- 现象:校时流程只认「唯一分钟」这一个终点,缺少「这个时间范围对当前要回答的问题是否够用」的判定,因此区间已经稳定也继续细分。
|
||||
- 触发条件:任何出生时间校时。
|
||||
- 根因:产品里没有用途决策合同:没有把「层覆盖、扫描覆盖、分支状态、领域是否完整、输出是否随代表时刻改变」这些事实综合成一个可复用的判定,也没有明确「通过也不代表生辰、也不许改出生记录」。
|
||||
- 修复(第一步,仅合同):移植占星师的参考决策器为 assess_use(assessment, expected_binding)。先验证两份绑定的七项字段完整,不一致即 stale。绑定一致时,必填层必须非空且覆盖三态合法、scan_state / branch_state 枚举合法、两个布尔必须真布尔、两个 claim 列表必须无重复且不能交叉;material_change 为真即 needs_refinement;层全 executed_complete 且领域完整、扫描 policy_covered、分支 resolved/bounded_equivalent 且只有稳定输出时 scope_ready;有稳定或条件输出时 conditional;否则 insufficient_evidence。刻意没有区间宽度与分数差门槛。can_use_interval 只在 scope_ready 为真,can_modify_birth_record 与 representative_is_birth_truth 恒为假。
|
||||
- 验收退回:stale 判定顺序(`494c85e0`,见 `docs/tasks/FIX-purpose-gate-contract-1.md`)。初版在比较绑定前校验其余字段,绑定变化且字段非法时会抛出 `ValueError`。现先校验两份绑定完整,变化时立即返回 `stale`,一致时才校验其余字段。
|
||||
- 验证:`tests/test_rectification_purpose_gate.py` 16 条通过。新增回归覆盖绑定变化同时扫描态非法、布尔为字符串或必填层为空。38 个参考对照场景的完整结果或异常一致。新模块没有生产入口。
|
||||
- 防复发:上述测试;后续单把 purpose_assessment 接进校时结果对象时,必须保留「不许改出生记录」「代表时刻不是生辰」两条恒为假。
|
||||
- 关联记录:占星师 2026-11-09 用途决策合同、docs/tasks/assets/purpose-gate-20261109/(README / PLAN / reference_gate / examples)、#184(校时可比性)
|
||||
- 修复版本:分支 codex/purpose-gate-contract-20261109
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# #186 验收退回:判定顺序与参考代码不一致(2026-11-09,Claude)
|
||||
|
||||
本文件补充 `TASK-purpose-gate-contract-20261109.md`,冲突时以本文件为准。验收的是 `494c85e0`。15 条测试、未接入、两个“永远为假”的许可都已核对通过。
|
||||
|
||||
## 问题
|
||||
|
||||
参考代码 `reference_gate.py` 的顺序是:两份绑定完整性 → **绑定不一致立即返回 `stale`** → 其余合同校验(必填层、scan_state、branch_state、两个布尔、两个 claim 列表)→ 判定。
|
||||
|
||||
产品的 `assess_use` 先跑完整 `_validate()`(含其余全部校验),再判断 `stale`。所以“绑定已变 + 其他字段不合法”时,参考返回 `stale`,产品抛 `ValueError`。Claude 实测三例(绑定改 `engine_version`,另把 `scan_state` 设为 `done` / `material_change` 设为 `"false"` / `required_layers` 设为 `[]`):参考都是 `stale`,产品都抛错。
|
||||
|
||||
PROGRESS 第 21 行写“绑定一致(不一致即 stale)→ 合同校验 …… 与参考代码逐行一致”,与代码不符;第 26 行的 12 场景对照没有覆盖这种组合。
|
||||
|
||||
## 要做的
|
||||
|
||||
1. 把顺序改成与参考一致:先校验两份绑定完整,然后比较绑定、不一致就返回 `stale`,再做其余校验。只改 `scripts/rectification_purpose_gate.py`。
|
||||
2. 在 `tests/test_rectification_purpose_gate.py` 加 1 条测试:上面三例都返回 `stale`,且 `can_continue_analysis` 为假。
|
||||
3. 把参考 / 产品对照扩到包含“绑定变化 + 各类非法字段”的组合(参考的 malformed 用例逐个配一次绑定变化),0 处不一致,结果写进 PROGRESS;改正第 21 行的说法。
|
||||
|
||||
## 不要碰
|
||||
|
||||
原任务书的“不要碰”清单继续有效;新模块仍不得被任何代码 import。不得自行写“产品决定/裁定”。
|
||||
|
||||
BUG-1833(在原条目补一行“验收退回:stale 判定顺序”,只改这一条)。
|
||||
@@ -0,0 +1,56 @@
|
||||
# PROGRESS · 出生时间按用途够用——第一步:用途决策合同(#186,BUG-1833,2026-11-09)
|
||||
|
||||
- 分支:`codex/purpose-gate-contract-20261109`(基线 `afac4ea9`)
|
||||
- 任务书:`docs/tasks/TASK-purpose-gate-contract-20261109.md`
|
||||
- 参考代码与规范:`docs/tasks/assets/purpose-gate-20261109/`
|
||||
- Bug:`docs/BUG_HISTORY.md` 末尾追加 BUG-1833
|
||||
|
||||
## 做了什么
|
||||
|
||||
| 文件 | 内容 |
|
||||
| --- | --- |
|
||||
| `scripts/rectification_purpose_gate.py`(新) | 移植 `assess_use(assessment, expected_binding)` 与它需要的常量、枚举、校验与结果构造 |
|
||||
| `tests/test_rectification_purpose_gate.py`(新) | 13 条参考测试 + 2 条初版新增 + 1 条验收回归,共 16 条 |
|
||||
| `docs/BUG_HISTORY.md` | 末尾追加 BUG-1833(写明只是合同、未接入、`scope_ready` 没有生产入口) |
|
||||
|
||||
**没有任何现有代码 import 新模块**(`grep -r rectification_purpose_gate scripts/` 无命中),本单不接入校时流程,也不改任何现有行为。
|
||||
|
||||
## 与参考代码的等价性
|
||||
|
||||
- 常量:`BINDING_KEYS` 七项一致;层覆盖三态、`scan_state` 三态、`branch_state` 四态、五种 `status` 全部照抄。
|
||||
- 初版 `494c85e0` 在比较绑定前校验了全部评估字段,导致绑定变化且字段不合法时抛出 `ValueError`。初版的 12 个对照场景没有覆盖这个组合。
|
||||
- 修复后的顺序与参考实现相同:先验证两份绑定完整,绑定不一致立即返回 `stale`,绑定一致才校验其余评估字段。后续仍按 `material_change` → `scope_ready` → `conditional` → `insufficient_evidence` 判定。
|
||||
- **没有**区间宽度门槛、**没有**分数差门槛(参考代码刻意不加;`legacy_scores = [8, 8]`、`historical_confidence = low` 仍可 `scope_ready`)。
|
||||
- `can_modify_birth_record` 与 `representative_is_birth_truth` 恒为 `False`,`can_use_interval` 只在 `scope_ready` 为真。
|
||||
- 产品模块把当前评估的校验和结果构造分别放在 `_validate_assessment` 与 `_result` 中,并以常量表示枚举值。
|
||||
|
||||
**实测对照**:用同一份合成 `ready_interval` 评估分别调用参考和产品模块,比较完整结果字典或 `ValueError` 的类型和消息。**38 个场景,0 处不一致**。
|
||||
|
||||
- 12 个原有场景覆盖正常评估、两种扫描态、三种分支态、实质变化、领域不完整、必填层均为 `partial`、含条件输出、无稳定输出和绑定变化。
|
||||
- 12 种非法字段分别搭配匹配或变化的绑定,共 24 个场景。其中包括参考测试里的 6 种非法输入。评估绑定不完整时,两种搭配均抛出 `ValueError`。绑定完整但已变化时,其余非法字段均返回 `stale`。
|
||||
- 2 个场景覆盖预期绑定不完整,分别搭配正常和非法评估字段,两种实现均抛出 `ValueError`。
|
||||
|
||||
## 自测
|
||||
|
||||
| 命令 | 结果 |
|
||||
| --- | --- |
|
||||
| `python -m pytest tests/test_rectification_purpose_gate.py -q -k stale_binding_takes_precedence`(修复前) | 3 个子例均复现 `ValueError`,退出码 1 |
|
||||
| `python -m pytest tests/test_rectification_purpose_gate.py -q`(修复后) | **16 条通过**,退出码 0 |
|
||||
| `python -m pytest tests/test_repo_privacy_markers.py -o addopts='' -q` | 62 条通过,退出码 0 |
|
||||
| `python scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45` | Gitea 远端已核对。整体退出码 1,复现台账中的 Windows 预检断言,独立工作树缺少历史 `.workbuddy` 镜像路径 |
|
||||
| `python -m pytest -o addopts='' -q tests/test_python_hygiene_budget.py` | 1 条通过,1 条失败。`scripts/` 顶层有 477 个 Python 文件,门禁上限为 476 |
|
||||
| `python scripts/run_quality_gate.py --profile quick`(Windows 沙箱内) | 1387 条通过、52 条失败、2 条跳过,退出码 1。失败包含本地回环 HTTP 的 `WinError 10013` |
|
||||
| `python -u scripts/run_quality_gate.py --profile quick`(沙箱外复跑) | 1435 条通过、4 条失败、2 条跳过,退出码 1。失败为目录预算和 `test_upstream_technique_layers.py` 的 3 条。用途合同测试全部通过 |
|
||||
|
||||
初版新增的两条:
|
||||
|
||||
1. `test_permissions_never_allow_editing_the_birth_record`:五种状态(含 `stale`)下 `can_modify_birth_record` 与 `representative_is_birth_truth` 都必须 `is False`;
|
||||
2. `test_every_synthetic_example_matches_its_expected_status`:读 `examples.json`,用 `ready_interval` 作底、叠上每个示例的改动,`status` 与标注一致(`scope_ready` / `needs_refinement`)。
|
||||
|
||||
验收新增 `test_stale_binding_takes_precedence_over_malformed_assessment`。覆盖 `engine_version` 变化分别搭配 `scan_state='done'`、`material_change='false'`、`required_layers=[]`,三例均要求返回 `stale` 且禁止继续分析。既有断言保持原样。
|
||||
|
||||
## 偏离与说明
|
||||
|
||||
- 测试从 `unittest` 风格原样移植(pytest 直接能跑),没有改成 pytest 函数式;这样与参考测试可以逐条对照。
|
||||
- `examples.json` 与参考测试直接从 `docs/tasks/assets/purpose-gate-20261109/` 读,没有复制进 `tests/fixtures/`:任务书只允许新建那两个文件,复制会多出一份可能走样的副本。
|
||||
- 合入前需确认模块路径。初版远端分支已有 477 个顶层 Python 文件,本轮核对的 `origin/staging` 有 476 个。原任务书指定的 `scripts/rectification_purpose_gate.py` 会使门禁超限。此次修复按修复单保留该路径和原门禁。建议迁入 `scripts/rectification_purpose_gate/__init__.py`,保留 Python 导入路径。
|
||||
@@ -0,0 +1,49 @@
|
||||
# TASK:出生时间“按用途够用”——第一步:用途决策合同和测试(2026-11-09)
|
||||
|
||||
- 分支:`codex/purpose-gate-contract-20261109`(基于 staging `afac4ea9`)
|
||||
- Bug 编号:BUG-1833
|
||||
- 负责验收:Claude(jyotisha-7e)
|
||||
|
||||
## 背景
|
||||
|
||||
占星师在 2026-11-09 提出,产品负责人已同意:出生时间不一定要精确到唯一的分钟。如果时间范围对当前要回答的问题足够稳定,就停止校时,保留这个区间继续解盘;只有当某个敏感界限会改变当前答案时,才继续细分。
|
||||
|
||||
完整规范、实施计划和参考代码都放在本分支的 `docs/tasks/assets/purpose-gate-20261109/` 下:
|
||||
|
||||
- `README.md`:规范
|
||||
- `PLAN.md`:实施计划
|
||||
- `reference_gate.py`:参考决策器
|
||||
- `test_reference_gate.py`:13 个合同测试
|
||||
- `examples.json`:合成接口示例
|
||||
|
||||
本单只做计划中的 **Task 1 前半部分**:把决策合同和它的测试移植进产品。不接入现有校时流程,不改任何现有文件的行为。
|
||||
|
||||
## 要做的
|
||||
|
||||
1. 新建 `scripts/rectification_purpose_gate.py`,移植 `reference_gate.py` 中的 `assess_use`,以及它所需的常量和枚举。
|
||||
- 逻辑与参考代码一致,不加入区间宽度或分数差之类的门槛。参考代码故意没有这些门槛。
|
||||
- 函数签名为 `assess_use(assessment: dict, expected_binding: dict) -> dict`。
|
||||
2. 新建 `tests/test_rectification_purpose_gate.py`,移植全部 13 个测试,把 import 改到产品模块。另外新增两条测试:
|
||||
- 无论输入是什么,返回值的 `can_modify_birth_record` 都必须是 `False`,`representative_is_birth_truth` 也必须是 `False`;
|
||||
- 用 `examples.json` 里的每个合成示例调用 `assess_use`,返回的 `status` 与示例标注一致。
|
||||
3. 在 `docs/BUG_HISTORY.md` 末尾追加 BUG-1833。写明这只是合同部分,尚未接入,`scope_ready` 没有任何生产入口。
|
||||
|
||||
## 不要碰
|
||||
|
||||
- `scripts/active_rectification_events.py`:#184 正在改。把 `purpose_assessment` 接进结果对象的工作由后续单完成。
|
||||
- `scripts/chart_choice/**`、`scripts/active_rectification_event_engine.py`、`scripts/narayana_rath.py`:不在本单范围,另有分支在改。
|
||||
- 不改旧的 `can_apply`,不改出生记录,不加 API 或前端。
|
||||
- 不改 `references/**`、`frontend/src/lib/consult-preset-prompts.ts`、冻结文件。
|
||||
- 不得自行写“产品决定”或“裁定”;任务书没写到的取舍,一律选“需拍板”。
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 15 个测试全部通过(13 个移植加 2 个新增)。
|
||||
2. `assess_use` 与参考代码逐行对照,逻辑等价。如有偏离,在 PROGRESS 里逐条写明原因。
|
||||
3. 新模块不被任何现有代码 import(本单不接入)。
|
||||
4. 写好 PROGRESS:`docs/tasks/PROGRESS-purpose-gate-contract-20261109.md`。
|
||||
|
||||
## 自测命令(Windows / PowerShell)
|
||||
|
||||
- `.venv\Scripts\python -m pytest tests/test_rectification_purpose_gate.py -q`
|
||||
- 本单不改前端,无需跑 tsc 和 lint。
|
||||
@@ -0,0 +1,153 @@
|
||||
# 按用途接受出生时间区间 Implementation Plan
|
||||
|
||||
> For agentic workers: use `superpowers:executing-plans` to implement task by task in the WEBSITE repository. This delivery does not require creating subagents. Each task has its own test and review boundary.
|
||||
|
||||
**Goal:** 用户时间范围对当前解盘/推运用途已足够稳定时停止校时,并保留区间;只针对会改变当前答案的敏感界限继续核实。
|
||||
|
||||
**Architecture:** 复用现有候选扫描、原生引擎、共同覆盖及事实证据链;新增用途评估,不重写评分或大运。时间证据置信度与用途稳定性并列;区间使用与出生记录修改分离。
|
||||
|
||||
**Tech Stack:** 网站现有Python计算/API与TypeScript咨询/前端;参考决策器仅Python标准库,测试使用unittest。部署继续网站已有staging流程。
|
||||
|
||||
**Spec:** [完整接入规范](../../../handoff/2026-11-09/interval_rectification/README.md)。执行前读该规范与本计划;状态合同见同目录reference_gate.py、examples.json。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 网站基线 `c032823f9e256625a1b346a70172b8d304854311`;资料基线 `bef2161f945e49632f33992bccaafefa1e3fc7c5`。先查看网站当前状态,保留已有修改。
|
||||
- 不改变旧can_apply语义、原始出生记录或资料仓个人报告冻结链。
|
||||
- 不把分钟宽度、分数并列、缺失相同当用途通过/失败依据;不增加校时概率。
|
||||
- 所有事实来自已执行的原生计算;参考决策器不生产这些事实。
|
||||
- Dasha调用既有引擎,不重构顺序/日期;R2未决与相关完整调用要求保持。
|
||||
- 私人输入只在受控测试环境,公开回传用匿名合成或获准公开盘。
|
||||
- 下列新增模块路径是网站侧建议,若已有同职责模块则扩展该模块并在回执写真实映射。不要复制资料仓UI覆盖网站前端。
|
||||
|
||||
## Task 1:新增用途决策合同,保持旧门控
|
||||
|
||||
**Files:**
|
||||
- Create website `scripts/rectification_purpose_gate.py`:使用交接reference_gate.py中的`assess_use`合同。
|
||||
- Test website `tests/test_rectification_purpose_gate.py`:移植交接13个合同测试,改import到网站模块。
|
||||
- Modify website `scripts/active_rectification_events.py`:结果增加可空purpose_assessment;旧字段不重命名。
|
||||
|
||||
**Interfaces:** `assess_use(assessment: dict, expected_binding: dict) -> dict`,参数和返回枚举完整定义在参考代码。ValueError映射为网站既有请求/内部验证错误,权限保持false,不向模型返回可用状态。
|
||||
|
||||
- [ ] 先移入测试与合成fixtures,运行以下命令确认新模块未实现时失败。
|
||||
|
||||
```bash
|
||||
python3 -m pytest -q -o addopts='' tests/test_rectification_purpose_gate.py
|
||||
```
|
||||
|
||||
- [ ] 移植参考函数,执行同命令,预期13个测试通过。额外参数legacy_scores不参与用途门控。
|
||||
- [ ] 为旧接口加回归:无purpose输入时旧结果原样;有purpose时仍不改旧can_apply。
|
||||
|
||||
```python
|
||||
legacy = adjudicate_candidate_rows(rows, **existing_arguments)
|
||||
assert legacy['can_apply'] == legacy_baseline['can_apply']
|
||||
assert purpose_result['can_modify_birth_record'] is False
|
||||
```
|
||||
|
||||
这里`rows/existing_arguments/legacy_baseline`使用现有`test_rectification_comparability_bug1801.py`的同一fixture,不新造计分口径。
|
||||
- [ ] 单独提交合同及测试,暂不开scope_ready生产入口。
|
||||
|
||||
## Task 2:绑定用途、候选集合和真实计算结果
|
||||
|
||||
**Files:**
|
||||
- Create/extend website `scripts/rectification_purpose_service.py`。
|
||||
- Modify website `scripts/chart_choice/astrologer.py`、`scripts/active_rectification_event_engine.py`:提供原生结果引用及依赖,避免重复计算。
|
||||
- Test website `tests/test_rectification_purpose_binding.py`。
|
||||
|
||||
**Interfaces:** 建议新适配入口:
|
||||
|
||||
```python
|
||||
def build_scope_request(question: dict, original_input: dict, remaining_ranges: list,
|
||||
profiles: dict, evaluated_at: str) -> dict: ...
|
||||
def collect_scope_outputs(scope_request: dict, native_results: list) -> dict: ...
|
||||
```
|
||||
|
||||
这两个签名是待实施接口,不是交接包中已运行的函数。返回schema按规范第三、六节。build_scope_request负责规范化用途与hash;collect_scope_outputs只消费已有原生结果,不计算任何新Dasha。
|
||||
|
||||
- [ ] 测试固定同输入hash一致;换题、预测窗、输入修订、profile、engine、precision policy、候选集合任一项hash变化。
|
||||
- [ ] 对每次native调用记录candidate_id、branch_id、profile、method_id、status、原始结果ref和依赖。缺失字段保留blocked,不补0、不把not_applicable塞入required层。
|
||||
- [ ] 合并范围时仅合并真正相邻且边界归属一致的区间;时间点与分钟槽分开。没有历史排除依据的候选继续保留。
|
||||
- [ ] 测试用户报告0–1200秒但只提供0–60秒扫描时domain_complete=false;不能只评最高分范围通过全域。
|
||||
- [ ] 测试无事件但用户提供范围时可评估用途稳定性;历史证据仍not_evaluated,不要求先凑够事件。
|
||||
- [ ] 单独提交并回传绑定样例;对外只用合成ID和范围。
|
||||
|
||||
## Task 3:按用途扫描内部敏感界限和应期包络
|
||||
|
||||
**Files:**
|
||||
- Extend website `scripts/rectification_purpose_service.py`。
|
||||
- 复用网站已有候选差异模块;若没有适配入口,Create website `scripts/rectification_purpose_scan.py`。
|
||||
- Test website `tests/test_rectification_purpose_scan.py`。
|
||||
- 资料参照:`scripts/active_rectification_candidates.py`已有groups/discriminators,分钟采样不能冒充连续边界。
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
```python
|
||||
def scan_scope(scope_request: dict, compute_at, boundary_providers: dict) -> dict: ...
|
||||
```
|
||||
|
||||
`compute_at(candidate_at, branch_id)`必须回调网站原生链,返回Task2的单候选结果;boundary_providers按相关层返回已界定边界/误差带或“无法提供”,不能返回猜测的精确秒。scan_scope输出scan_state、per_layer_scan_receipts、output_envelopes、material_differences、stable/conditional事实ID。每个事实保留范围和依赖覆盖。
|
||||
|
||||
- [ ] 先用确定性假函数测试内部A→B→A;用它检验扫描器,不把它当占星oracle。
|
||||
|
||||
```python
|
||||
def synthetic_feature(offset_seconds):
|
||||
return 'B' if 25 <= offset_seconds < 35 else 'A'
|
||||
assert synthetic_feature(0) == synthetic_feature(60)
|
||||
assert synthetic_feature(30) != synthetic_feature(0)
|
||||
```
|
||||
|
||||
- [ ] 没有边界提供器且只有首尾样本时预期sampled_only;提供[25,35]界限后必须得到三段,不能合并首尾A穿过B。
|
||||
- [ ] 对实际所需层粗扫→按可用边界/局部误差加密→两侧原生重算→记录最大未查间隔。没有覆盖依据不返回policy_covered;计算预算耗尽返回incomplete。
|
||||
- [ ] 计算时间窗口端点min/max、并集、交集及每分支原值。用户问题日期边界、窗口存在性、支持方向、事实身份按冻结policy比较;禁止仅比较字符串摘要或主星名。
|
||||
- [ ] 测试同名AD但端点跨目标日期应material_change=true;相同blocked不生成stable事实;不相关D60改变仅记录不影响当前用途。
|
||||
- [ ] 分别用真实引擎验证至少一个稳定宽区间、一个已知分盘边界、一个应期日期敏感例。人工检查原始结果,不依赖模型解释评判是否稳定。
|
||||
- [ ] 通过后单独提交。开放policy_covered必须附实际层级扫描证据,不以参考合同测试通过代替。
|
||||
|
||||
## Task 4:接入API、模型和页面,新增“使用区间”动作
|
||||
|
||||
**Files:**
|
||||
- 网站现有校时API处理器(用`rg`定位真实路径并写入提交说明)。
|
||||
- `frontend/src/mastra/consultation-tools.ts`
|
||||
- `frontend/src/lib/consultation-evidence-card.ts`
|
||||
- `frontend/src/lib/consultation-technique-evidence.ts`
|
||||
- `frontend/src/lib/consult-answer-basis.ts`
|
||||
- Test `frontend/tests/rectification-purpose-interval.test.ts`、网站API对应测试文件。
|
||||
|
||||
**Interfaces:** API返回规范第六节对象;前端只消费服务器决策,不从宽度、confidence或can_apply推导新区间权限。示例UI判断:
|
||||
|
||||
```typescript
|
||||
const canUseInterval = result.purpose_assessment?.decision?.can_use_interval === true;
|
||||
const canShowLimited = result.purpose_assessment?.decision?.can_continue_analysis === true;
|
||||
// 新动作提交range_set_id与purpose binding;不提交代表分钟作为出生记录。
|
||||
```
|
||||
|
||||
服务器收到新动作后重新比对binding;客户端上传can_use_interval不授予权限。请求不匹配返回stale及重新评估指令。
|
||||
|
||||
- [ ] 写反例:旧can_apply=false而scope_ready=true时可继续区间分析;仍不能修改出生档案。
|
||||
- [ ] 写反例:旧confidence=high但needs_refinement时不能显示“已满足本次精度”。
|
||||
- [ ] 模型工具返回加入稳定/条件事实和分支,不只传代表盘;使用规范第六节可粘贴提示合同。
|
||||
- [ ] UI测试显示范围、用途、限制、稳定部分;不强制用户追求唯一分钟。出现实质分歧时只追问有区分能力的事件,不把方法未实现转成出生时间追问。
|
||||
- [ ] 将第七轮D9参照/Yoga状态修正沿用至区间事实,防止“稳定候选”被渲染为“已证实事件”。
|
||||
- [ ] 单独提交,功能开关默认关闭;先staging调试。
|
||||
|
||||
## Task 5:跨入口持久化、缓存和回归发布
|
||||
|
||||
**Files:**
|
||||
- 网站现有咨询/报告/日历/年运的请求、缓存键和报告序列化模块(由Task4定位并列明具体修改文件)。
|
||||
- Test website `tests/test_rectification_purpose_persistence.py`及对应浏览器E2E。
|
||||
|
||||
**Interfaces:** 持久化范围、scope、binding、decision版本、结果引用;代表时刻必须携带representative_is_birth_truth=false。用途评估元数据仅保留必要ID和证据引用,个人出生负载留原受控记录,不复制进新日志/profile元数据。
|
||||
|
||||
- [ ] 用同一合成请求经咨询→报告→日历→年运,对照range_set_id、scope、profile及raw payload指纹;不适用的入口明确拒绝区间或提示限制,不默默选中点。
|
||||
- [ ] 刷新页面、旧报告重开和导出后仍显示原范围、原用途版本;今天日期变化不复用旧评估。
|
||||
- [ ] 执行规范U01–U16;合同测试只是其中一层,必须补真实计算与浏览器证据。
|
||||
- [ ] 复跑既有`test_rectification_comparability_bug1801.py`、`test_rectification_api_can_apply_guard_bug1802.py`、`test_active_rectification_api.py`、`test_rectification_reseal_round7_bug1809.py`,失败和跳过逐项回传,不删旧断言来过门。
|
||||
- [ ] staging开启新通道,记录提交、镜像、开关和回执;关闭开关验证旧流程可用且原始范围未变。生产按网站既有流程发布,资料侧提交不是网站部署。
|
||||
|
||||
## 本交接包本地验证
|
||||
|
||||
```bash
|
||||
python3 -m unittest discover -s handoff/2026-11-09/interval_rectification -p 'test_*.py' -v
|
||||
```
|
||||
|
||||
这条命令验证参考状态合同,不验证网站实现。交接完成后网站开发人员可直接从Task1执行;无需再选择实施模式或等待资料侧批准普通编码步骤。
|
||||
@@ -0,0 +1,180 @@
|
||||
# 按用途接受出生时间区间:网站开发交接
|
||||
|
||||
日期:2026-11-09。给独立网站开发人员直接使用。适配基线:第七轮验收包网站 `c032823f9e256625a1b346a70172b8d304854311`;资料仓 `bef2161f945e49632f33992bccaafefa1e3fc7c5`。网站之后若已更新,先按函数与字段核对差异,不覆盖新代码。
|
||||
|
||||
**目标:出生时间只需精确到足以稳定回答当前问题的范围。区间宽度、是否唯一分钟、候选分数是否并列,都不能单独决定是否继续校时。**
|
||||
|
||||
交付内容:本规范;[分步实施计划](../../../docs/superpowers/plans/2026-11-09-purpose-scoped-rectification.md);[可运行决策合同](reference_gate.py);[合同反例测试](test_reference_gate.py);[合成接口示例](examples.json)。参考代码仅消费已经计算出的评估事实,不计算星盘、不扫描敏感界限、不替代网站引擎。网站敏感性生产器、API、前端尚需按计划实施。
|
||||
|
||||
本地交付检查:13项参考合同测试通过,文档相对链接可解析。解压包根目录运行 `python3 -m unittest discover -s handoff/2026-11-09/interval_rectification -p 'test_*.py' -v` 即可复验,不依赖完整资料仓。原始第七轮验收包未打入本交接;随附review_packet.py需要开发人员另行提供原包目录,不能把缺原包当复验通过。
|
||||
|
||||
## 一、产品决定及与第七轮的衔接
|
||||
|
||||
本规范更新第七轮交接中容易被理解为“必须找唯一分钟”的部分。12:00–12:01的8:8可以是一组足够使用的候选;甚至更宽范围也可接受。反过来,只有20秒也可能跨过关键分盘或应期边界,需要继续辨别。
|
||||
|
||||
R2的未决规则继续有效:不能把未知当相等,不能用缺失当0分。**但“不能确定唯一出生时间”不再阻止用户使用整个区间的稳定部分解盘。** Narayana相关子层缺失时保留其他已计算且稳定的结果;如本次确需该子层,只把对应判断列为不足,不假装全量完成。
|
||||
|
||||
独立保留两个状态:
|
||||
|
||||
| 维度 | 回答的问题 | 依据 |
|
||||
|---|---|---|
|
||||
| 时间范围的证据支持 | 原始出生记录与历史事件能否支持这个范围、排除哪些候选? | 已有来源记录、事件质量、共同覆盖、留出验证 |
|
||||
| 当前用途的结果稳定性 | 如果真实时间在剩余范围任一点,本次哪些判断会变、变化是否实质? | 相关引擎结果、敏感界限、预测窗变化、当前用途精度 |
|
||||
|
||||
低历史置信度不自动禁止稳定结果的条件分析;高历史分数不自动证明预测稳定。稳定性不能把用户原始范围缩小成“历史已证实区间”,也不能把合成可用率包装成校时准确率。
|
||||
|
||||
## 二、复用已有实现,不新造评分系统
|
||||
|
||||
| 已有位置或字段 | 本次怎么接 |
|
||||
|---|---|
|
||||
| 网站 `scripts/active_rectification_events.py::adjudicate_candidate_rows` | 保留共同覆盖比较、provisional/comparable leader、can_apply;在其旁增加用途评估结果,不修改历史评分阈值来实现区间可用 |
|
||||
| 网站 `scripts/chart_choice/astrologer.py::scan / score_minute`(由附件脚本确认) | 复用候选计算和事件明细;不要把score_minute当结果稳定性检测器 |
|
||||
| 网站 `scripts/active_rectification_event_engine.py`、`scripts/narayana_rath.py` | 从现有原生函数提取每候选输出、缺失和分支;不在聊天或新模块重写Dasha |
|
||||
| 网站 `frontend/src/lib/consultation-evidence-card.ts` | 增加区间、用途、稳定事实和条件事实;不只传代表时刻单盘 |
|
||||
| 网站 `frontend/src/lib/consultation-technique-evidence.ts`、`consult-answer-basis.ts` | 用同一评估对象呈现“区间足够/仅部分可用/需进一步区分”,不自行推导状态 |
|
||||
| 网站 `frontend/src/mastra/consultation-tools.ts` | 工具输入绑定区间与用途;工具返回包含分支和限制,校验旧结果失效 |
|
||||
| 资料仓 `scripts/active_rectification_candidates.py::build_candidate_difference_contract` | 已有分组、signature、discriminators、dimension_sensitivity,可作适配参照;不是完整连续区间稳定性引擎 |
|
||||
| 资料仓 `scripts/active_rectification_questions.py` 与现有候选选择器 | 复用敏感层和问答路由,优先询问能分开实质分歧区间的事件 |
|
||||
|
||||
网站真实API路由和页面文件需在其源码用 `rg 'can_apply|comparable_leader|winning_segment|representative_time'` 定位;本包没有网站完整源码,不虚构具体路由。网站可能已有候选分组实现,先对照再决定是否新增适配模块。
|
||||
|
||||
资料仓候选扫描当前有分钟采样、240分钟上限、固定分盘签名;`boundary_time`是新组首个采样点,不是已求解的精确边界。部分默认参数为Raman/true,网站当前为Raman/mean:调用必须显式传用户确认的全套参数,禁止复制默认值。资料侧旧评分还含宽度5/15分钟门槛;它只属于原有审批流程,不能挪作本次用途评估的硬门槛。
|
||||
|
||||
## 三、先冻结用途合同
|
||||
|
||||
每次评估建立 `purpose_scope`,至少包含:
|
||||
|
||||
| 字段 | 内容 |
|
||||
|---|---|
|
||||
| question / domains / event_class | 原问题、主题;婚恋分开心动、成对、法律婚等目标 |
|
||||
| forecast_start / forecast_end / evaluated_at | 实际观察窗口、运行日期和时区;换日期必须重新评估 |
|
||||
| required_techniques / required_outputs | 由现有问题路由解析的全部关联可运行层及本次所需结果,不以“想通过”为由删技法 |
|
||||
| precision_policy_id / version | 输出到年、月、周、日或仅结构;窗口宽度和边界偏移的容许规则,固定在运行前 |
|
||||
| decision_predicates | 会改变回答的条件,如是否落入用户指定月、是否跨过事件窗口、关键论断方向是否改变 |
|
||||
| profile_fingerprint / engine_version | 岁差、交点、宫制、Dasha profile、年长、来源分支、引擎版本 |
|
||||
| candidate_set_fingerprint / input_revision | 全部仍合理的时间范围、范围来源、地点与时区解析版本、事件修订号 |
|
||||
|
||||
默认用户无需填一堆技术表单。系统从问题提取“关心什么、问多长时间、需要多细”,显示一句可更正的摘要;只有“这个月”和“具体哪天”这类会改变验收精度的歧义才追问。没有精度政策时先给条件结果,不能自设“差几天都无所谓”。
|
||||
|
||||
每个主题独立评估:同一出生区间可以“事业长期结构足够、婚恋月度窗口需细分”。不能因D60变化就阻塞所有主题;也不能只看D1同上升便说所有分盘和推运稳定。未纳入本次用途的层标not_applicable并说明原因,不删除原始数据。
|
||||
|
||||
## 四、从候选计算到用途稳定性
|
||||
|
||||
按以下顺序实现,始终调用原有计算链:
|
||||
|
||||
1. **保存原始范围与来源。** 同时保留用户报告范围、事件支持的候选集合、用于本次分析的集合。默认评估全部仍合理的候选,不能只拿当前分数最高的一段假装其他已排除。多个不连续范围保留为集合,不填补中间空隙。
|
||||
2. **获得相关输出。** 每个采样候选和每个必要的参数分支均保留输入指纹、计算状态、结构值、Dasha原始边界、窗口、证据ID和依赖。此前未执行的层必须明确缺失。
|
||||
3. **发现并细化界限。** 使用已有可定位的分盘、宫头、关系、度数阈值、Dasha及应期窗口敏感点;粗网格只做发现。界限附近加密、记录左右结果和未确定的边界带。只检查首尾或固定每分钟不足以排除内部A→B→A变化。
|
||||
4. **逐输出判定影响。** 原始结构变化必须保留;仅当该变化不会影响本次依赖它的判断时,才标“对本用途无实质影响”。由可追溯规则判定,不让模型比较两段相似文案代替数值和依赖比较。
|
||||
5. **输出稳定与差异两部分。** 只有覆盖全部剩余范围和相关分支的事实才进入 `stable_claim_ids`。有差异的判断保留分支条件及范围。分数相同、null相同、blocked相同、同一个模型给同一句话都不是稳定证据。
|
||||
6. **决定停止或追问。** 满足用途精度即可停止细分;发现会影响用途的界限才问针对性历史事件。缺算法、缺时区、网络或计算预算不足不是缺出生分钟,必须分开提示,不能反复逼用户回忆。
|
||||
|
||||
连续区间检验记录采用 `policy_covered / sampled_only / incomplete` 三态。`policy_covered`须逐层给出在声明分辨率下的扫描覆盖或界限界定依据、最大未检查间隔、边界误差与停止容差;不是要求所有技法有全局数学证明,也不是仅因“跑完循环”就通过。对无法排除的内部切换保留sampled_only;临界带宽大于用户用途允许值时不能提升为完整可用。
|
||||
|
||||
如果底层只接受分钟,本轮不得声称排除了分钟内部变化;精度要求超过引擎能力时报告限制。生日跨午夜、历史夏令时/重叠本地时刻、地点不确定都需转换成明确UTC区间或多分支;不在循环中猜时区。
|
||||
|
||||
### 应期变化怎样比较
|
||||
|
||||
原始窗口端点范围、窗口并集、交集、事件类、父段身份一起比较,不仅比较当前MD/AD名字。同名运的日期仍可能漂移。
|
||||
|
||||
- “问今年整体结构”与“问某月会不会发生”使用不同的已版本化policy;不能用一套固定分钟宽度。
|
||||
- 即使端点仅偏移一天,若跨过用户关心的月末或日期,也可构成实质差异;若仅需季度且所有相关判断与允许的窗口宽度均稳定,可保留区间。
|
||||
- 窗口并集只能写“各候选的可能范围”,不能写“整个范围一直有效”;交集为空不得生成共同确定窗口。
|
||||
- 起止、存在性、主星、方向、条件性等级任一实质变化均要保留;不把“多数候选一致”当全部稳定,不给均匀分钟权重冒充概率。
|
||||
- 不同原因区分 `birth_time_sensitive / method_unresolved / missing_input / calculation_failure / scan_incomplete`。R2规则未决不能靠把出生范围缩成一分钟消失。
|
||||
|
||||
## 五、状态与按钮:新增用途通道,不改旧can_apply含义
|
||||
|
||||
新增 `purpose_assessment`,其绑定字段必须由服务器重算,与当前请求逐项匹配。旧 `confidence`继续表示其既有候选证据评价;旧 `can_apply`不自动升级。**本次“使用区间继续分析”是新增动作,不是把出生记录改成代表分钟。**
|
||||
|
||||
| status | can_use_interval | can_continue_analysis | 用户动作 |
|
||||
|---|---:|---:|---|
|
||||
| scope_ready | true | true | 使用该区间继续;停止无收益的分钟细分 |
|
||||
| conditional | false | true | 继续看已核实稳定部分和明确条件结果;不声称全用途已稳定 |
|
||||
| needs_refinement | false | 取决于稳定/条件事实是否存在 | 回答针对界限的问题,或先看不受影响的部分 |
|
||||
| insufficient_evidence | false | false | 补必要输入、重试计算或缩小问题范围 |
|
||||
| stale | false | false | 按新用途/日期/参数/候选集合重新计算 |
|
||||
|
||||
`can_use_interval`表示当前请求全范围已按政策评估足够;不是允许修改出生档案。新通道的 `can_modify_birth_record`始终false。旧确认出生记录流程可独立保留,不由本功能触发。
|
||||
|
||||
代表时刻仅为现有单盘API兼容计算值。选择实际已计算、位于允许区间且不在未决边界带中的值;无必要不强取中心。API/缓存/报告同时携带区间与 `representative_is_birth_truth=false`。如果多个分支不等价,不能拿一个代表盘生成全体断言。
|
||||
|
||||
区间端点使用带时区的时间或服务器解析的UTC,明确闭/开边界。输入“12:00到12:01”是两个端点还是分钟槽由输入精度决定;不能把旧 `width_minutes=2`当120秒物理跨度。新字段用duration_seconds和sample_count分开保存。分段边界恰好落点的归属按计算函数规则记录,未决边界带单独保留。
|
||||
|
||||
## 六、返回对象和模型合同
|
||||
|
||||
`examples.json`给出合成接口示例;无真实出生数据。完整服务返回至少包含:
|
||||
|
||||
```text
|
||||
original_time_record(不修改)
|
||||
plausible_ranges[] + source/evidence_state
|
||||
purpose_scope + binding
|
||||
legacy_rectification_result(原有评分/共同覆盖/应用门控)
|
||||
purpose_assessment:
|
||||
required_layers + layer_coverage
|
||||
scan_state + per_layer_scan_receipts
|
||||
domain_complete + branch_state
|
||||
material_change + material_differences[]
|
||||
stable_claim_ids[] + conditional_claim_ids[]
|
||||
status + permissions + allowed_claim_ids[] + reason
|
||||
interval_results[]:
|
||||
range + boundary_policy + representative_at
|
||||
representative_is_birth_truth=false
|
||||
computed_payload_ref + output_envelopes + dependencies
|
||||
```
|
||||
|
||||
服务器必须先从真实结果构造评估事实,再调用决策合同;不能接受浏览器传来的 `domain_complete=true` 等字段授予可用状态。参考决策器只检查状态合同,不证明这些事实真实。
|
||||
|
||||
模型收到的规则可直接加入现有工具合同:
|
||||
|
||||
> 本次出生时间是区间。只使用allowed_claim_ids对应的证据。stable事实需在评估的全部候选及相关分支上成立;conditional事实必须带具体分支或缺项。代表时刻不是确认出生时刻。不得因为分数并列要求唯一分钟;只有material_differences会改变当前问题时才建议进一步校正。不得由缺失的技法推出结果。用途或预测日期变化后请求重新评估。
|
||||
|
||||
界面建议:
|
||||
|
||||
- scope_ready:“这个时间范围足够用于本次事业年度分析,无需继续细分。适用范围:……”。
|
||||
- conditional:“已有部分结论不受时间范围影响;以下小运窗口仍需区分或补算。”
|
||||
- needs_refinement:“范围内有一处变化会影响你问的月份。请核对下面这件已发生的事;暂不选定某一分钟。”
|
||||
- 不显示“校时准确率98%”“已校正为12:03”,除非另有完全独立且明确的依据;本功能不产生此类结论。
|
||||
|
||||
## 七、交付路线与停止条件
|
||||
|
||||
**阶段1:可解释的部分交付。** 在已有候选结果上增加区间用途对象、稳定/条件事实与新按钮;扫描尚未增强时如实sampled_only,不开放scope_ready。用户可以继续读取已核实的部分,不必等待C全部补齐。
|
||||
|
||||
**阶段2:用途敏感性扫描。** 在真实引擎上补相关界限、内部变化检测与窗口包络,达到policy_covered后才开放scope_ready。分数并列或区间超过15分钟不再单独阻塞此通道。
|
||||
|
||||
**阶段3:全链回归。** 咨询、报告、日历、年运、校时均保留同一范围与用途版本;前端刷新、重新打开、导出不能退回“唯一代表分钟”。旧报告不静默迁移,按原配置重开。
|
||||
|
||||
每阶段单独提交与部署到staging,保留功能开关 `purpose_scoped_rectification_v1`(建议新增名)。关闭功能仅撤回新通道,不删原始范围和证据,不改变旧can_apply。生产部署由网站现有流程决定。
|
||||
|
||||
性能采用缓存和分阶段计算:key包括输入修订、候选集合、用途、预测窗、精度政策、参数与算法版本;全量明细保留受控后端,模型拿相关包络和可追溯事实。预算耗尽返回incomplete,不以“超时即稳定”通过,不让缓存丢掉敏感层。
|
||||
|
||||
## 八、必须覆盖的真实验收场景
|
||||
|
||||
| 编号 | 场景 | 预期 |
|
||||
|---|---|---|
|
||||
| U01 | 20分钟范围,相关输出完整且对目标用途等价 | scope_ready;宽度不否决 |
|
||||
| U02 | 20秒范围,跨关键分盘且改变目标判断 | needs_refinement;窄不等于准确 |
|
||||
| U03 | 首尾相同,中间A→B→A | 检出内界限;未检查则sampled_only,禁止假通过 |
|
||||
| U04 | 8:8同分、多分钟但用途稳定 | scope_ready;不修改出生记录 |
|
||||
| U05 | 所有候选都缺Narayana_AD | 不以缺失相等判稳定;可交付其他有证据的稳定部分 |
|
||||
| U06 | AD缺失但MD有值;组合得分无法拆开 | 保留MD,排除共同覆盖不一致贡献,不填AD=0 |
|
||||
| U07 | D60变、当前用途经依赖审查不使用它 | 记录差异但不自动阻塞;换用途重算 |
|
||||
| U08 | 同名MD/AD,边界跨用户指定日期 | 仍判实质变化,不只比星名 |
|
||||
| U09 | 窗口并集很宽或交集为空 | 保留分支窗口;不生成共同确定日期 |
|
||||
| U10 | 旧结果换主题、日期、profile或候选集合 | stale,任何旧可用权限撤销直到重算 |
|
||||
| U11 | 两段不连续区间,各自稳定但结论不同 | 保留两个分支,不用中点、不合成连续范围 |
|
||||
| U12 | 跨午夜、DST歧义或分钟引擎被要求秒级 | 明确时间分支/能力边界,不猜解析或声称秒级稳定 |
|
||||
| U13 | 代表值单盘看似确定,区间实际有差异 | 模型/页面/导出保留差异,不丢区间 |
|
||||
| U14 | 低历史置信度但用户报告范围内结果稳定 | 条件于原范围使用;不升级真实出生时间置信度 |
|
||||
| U15 | R2方法未决、缩时也不能解决 | 标method_unresolved;不继续无效追问分钟 |
|
||||
| U16 | 扫描达到预算,尚有内部区间未覆盖 | incomplete,不授予scope_ready |
|
||||
|
||||
参考决策器测试覆盖其中的状态合同,**不等于上述实际引擎、浏览器和端到端测试已通过**。网站需补同名场景在真实调用链上的回执,特别是U03/U08/U12/U13。
|
||||
|
||||
验收指标改为:按用途可用区间率、敏感界限漏检率、错误宣布稳定数、过度要求继续校时数、稳定/条件事实保留率、重开与导出区间丢失数。历史真时刻覆盖率另报,按案例而非偏移次数统计;不把区间变宽换来的覆盖提高当校时提升。
|
||||
|
||||
## 九、开发人员回传格式
|
||||
|
||||
回传manifest列网站提交、资料提交、profile/policy/引擎版本、功能开关、构建及staging回执。每个U用例保留:输入区间来源→用途冻结→逐候选原始结果→敏感界限/误差带→输出包络与依赖→决策对象→模型实际输入→页面/导出。真实个人数据留在网站受控环境;对外用合成或公开获准案例。
|
||||
|
||||
本轮只有交接规范和可运行的状态参考合同,不修改网站服务器、不改变资料仓个人报告冻结导出合同。资料仓默认个人报告目前仍拒绝区间出生输入,本方案不能被用来绕过它;本次新增通道属于独立网站。
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"schema": "jyotish.purpose_interval_examples.v1",
|
||||
"boundary": "Synthetic contract examples, not computed charts or real personal records. Offsets are relative to a synthetic origin, not public API timestamps.",
|
||||
"ready_interval": {
|
||||
"range": {
|
||||
"start_offset_seconds": 0,
|
||||
"end_offset_seconds": 1200,
|
||||
"closed_start": true,
|
||||
"closed_end": true,
|
||||
"duration_seconds": 1200,
|
||||
"sample_count": 21,
|
||||
"representative_offset_seconds": 600,
|
||||
"representative_is_birth_truth": false
|
||||
},
|
||||
"assessment": {
|
||||
"binding": {
|
||||
"input_revision": "synthetic-input-v1",
|
||||
"candidate_set_fingerprint": "synthetic-range-0-1200",
|
||||
"purpose_fingerprint": "synthetic-career-yearly",
|
||||
"profile_fingerprint": "synthetic-explicit-profile",
|
||||
"engine_version": "synthetic-engine-v1",
|
||||
"precision_policy_version": "synthetic-purpose-policy-v1",
|
||||
"forecast_window_fingerprint": "synthetic-fixed-year-window"
|
||||
},
|
||||
"required_layers": ["D1", "D10", "Vimshottari", "Narayana_AD"],
|
||||
"layer_coverage": {
|
||||
"D1": "executed_complete",
|
||||
"D10": "executed_complete",
|
||||
"Vimshottari": "executed_complete",
|
||||
"Narayana_AD": "executed_complete"
|
||||
},
|
||||
"scan_state": "policy_covered",
|
||||
"domain_complete": true,
|
||||
"branch_state": "resolved",
|
||||
"material_change": false,
|
||||
"stable_claim_ids": ["synthetic:d1-common", "synthetic:career-window-common"],
|
||||
"conditional_claim_ids": []
|
||||
},
|
||||
"expected_status": "scope_ready",
|
||||
"note": "Layer list is deliberately abbreviated for a state-contract test; production must use the full relevant route. Scan coverage is stipulated by this fixture, not produced by the reference gate."
|
||||
},
|
||||
"sensitive_short_interval": {
|
||||
"range": {"start_offset_seconds": 0, "end_offset_seconds": 20, "duration_seconds": 20},
|
||||
"assessment_changes_from_ready": {
|
||||
"material_change": true,
|
||||
"stable_claim_ids": ["synthetic:d1-common"],
|
||||
"conditional_claim_ids": ["synthetic:before-boundary", "synthetic:after-boundary"]
|
||||
},
|
||||
"expected_status": "needs_refinement",
|
||||
"note": "Create a new candidate-set binding in a real request; this delta only demonstrates decision logic."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Purpose-use gate reference ONLY; never computes charts or grants record edits.
|
||||
|
||||
The server must derive all assessment fields from native computation evidence.
|
||||
Passing this gate cannot certify that a caller's supplied evidence is truthful.
|
||||
"""
|
||||
|
||||
BINDING_KEYS = (
|
||||
'input_revision', 'candidate_set_fingerprint', 'purpose_fingerprint',
|
||||
'profile_fingerprint', 'engine_version', 'precision_policy_version',
|
||||
'forecast_window_fingerprint',
|
||||
)
|
||||
|
||||
|
||||
def assess_use(assessment: dict, expected_binding: dict) -> dict:
|
||||
"""Return permissions for the evaluated scope, not birth-time confidence.
|
||||
|
||||
Deliberately has no interval-width or score-margin threshold.
|
||||
stable_claim_ids must cover the entire remaining candidate domain;
|
||||
conditional_claim_ids must point to explicit branch/limitation evidence.
|
||||
"""
|
||||
for binding in (assessment.get('binding'), expected_binding):
|
||||
if not isinstance(binding, dict) or any(
|
||||
not isinstance(binding.get(k), str) or not binding[k] for k in BINDING_KEYS
|
||||
):
|
||||
raise ValueError('complete server binding required')
|
||||
|
||||
def result(status, stable, conditional, reason):
|
||||
allowed = list(dict.fromkeys(stable + conditional))
|
||||
return {
|
||||
'schema': 'jyotish.purpose_use_decision.v1',
|
||||
'status': status,
|
||||
'can_use_interval': status == 'scope_ready',
|
||||
'can_continue_analysis': bool(allowed),
|
||||
'can_modify_birth_record': False,
|
||||
'representative_is_birth_truth': False,
|
||||
'stable_claim_ids': stable,
|
||||
'conditional_claim_ids': conditional,
|
||||
'allowed_claim_ids': allowed,
|
||||
'reason': reason,
|
||||
}
|
||||
|
||||
if any(assessment['binding'][k] != expected_binding[k] for k in BINDING_KEYS):
|
||||
return result('stale', [], [], 'binding_changed_recompute')
|
||||
|
||||
required = assessment.get('required_layers')
|
||||
coverage = assessment.get('layer_coverage')
|
||||
if not isinstance(required, list) or not required or any(
|
||||
not isinstance(x, str) or not x for x in required
|
||||
) or len(set(required)) != len(required) or not isinstance(coverage, dict):
|
||||
raise ValueError('explicit nonempty required layer set required')
|
||||
if any(coverage.get(layer) not in ('executed_complete', 'partial', 'blocked') for layer in required):
|
||||
raise ValueError('required layers cannot be absent or not_applicable')
|
||||
if assessment.get('scan_state') not in ('policy_covered', 'sampled_only', 'incomplete'):
|
||||
raise ValueError('invalid scan_state')
|
||||
if assessment.get('branch_state') not in ('resolved', 'bounded_equivalent', 'unresolved_material', 'not_evaluated'):
|
||||
raise ValueError('invalid branch_state')
|
||||
for flag in ('domain_complete', 'material_change'):
|
||||
if type(assessment.get(flag)) is not bool:
|
||||
raise ValueError(f'{flag} must be a boolean')
|
||||
for field in ('stable_claim_ids', 'conditional_claim_ids'):
|
||||
values = assessment.get(field)
|
||||
if not isinstance(values, list) or any(not isinstance(v, str) or not v for v in values):
|
||||
raise ValueError(f'invalid {field}')
|
||||
if len(set(values)) != len(values):
|
||||
raise ValueError(f'duplicate {field}')
|
||||
stable, conditional = assessment['stable_claim_ids'], assessment['conditional_claim_ids']
|
||||
if set(stable) & set(conditional):
|
||||
raise ValueError('one fact cannot be both stable and conditional')
|
||||
|
||||
if assessment['material_change']:
|
||||
return result('needs_refinement', stable, conditional, 'material_output_change')
|
||||
complete = all(coverage[layer] == 'executed_complete' for layer in required)
|
||||
if (complete and assessment['domain_complete']
|
||||
and assessment['scan_state'] == 'policy_covered'
|
||||
and assessment['branch_state'] in ('resolved', 'bounded_equivalent')
|
||||
and stable and not conditional):
|
||||
return result('scope_ready', stable, [], 'adequate_for_frozen_purpose')
|
||||
if stable or conditional:
|
||||
return result('conditional', stable, conditional, 'only_listed_claims_are_available')
|
||||
return result('insufficient_evidence', [], [], 'no_supported_outputs_for_purpose')
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Contract tests, not tests of a sensitivity scanner or the website runtime."""
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from reference_gate import assess_use, BINDING_KEYS
|
||||
|
||||
|
||||
class PurposeGateTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.case = json.loads(Path(__file__).with_name('examples.json').read_text())['ready_interval']
|
||||
self.a = copy.deepcopy(self.case['assessment'])
|
||||
self.binding = copy.deepcopy(self.a['binding'])
|
||||
|
||||
def test_wide_interval_can_be_used_without_confirming_birth(self):
|
||||
self.assertEqual(self.case['range']['duration_seconds'], 1200)
|
||||
r = assess_use(self.a, self.binding)
|
||||
self.assertEqual(r['status'], 'scope_ready')
|
||||
self.assertTrue(r['can_continue_analysis'])
|
||||
self.assertFalse(r['can_modify_birth_record'])
|
||||
|
||||
def test_short_interval_can_need_refinement(self):
|
||||
self.a['material_change'] = True
|
||||
self.assertEqual(assess_use(self.a, self.binding)['status'], 'needs_refinement')
|
||||
|
||||
def test_equal_scores_do_not_block_scope_use(self):
|
||||
self.a['legacy_scores'] = [8, 8]
|
||||
self.a['historical_confidence'] = 'low'
|
||||
self.assertTrue(assess_use(self.a, self.binding)['can_use_interval'])
|
||||
|
||||
def test_every_binding_change_invalidates_permission(self):
|
||||
for key in BINDING_KEYS:
|
||||
with self.subTest(key=key):
|
||||
changed = dict(self.binding, **{key: 'different'})
|
||||
r = assess_use(self.a, changed)
|
||||
self.assertEqual(r['status'], 'stale')
|
||||
self.assertFalse(r['can_continue_analysis'])
|
||||
self.assertFalse(r['can_use_interval'])
|
||||
|
||||
def test_sampling_does_not_certify_whole_interval(self):
|
||||
for state in ('sampled_only', 'incomplete'):
|
||||
with self.subTest(state=state):
|
||||
self.a['scan_state'] = state
|
||||
self.assertEqual(assess_use(self.a, self.binding)['status'], 'conditional')
|
||||
|
||||
def test_missing_layer_keeps_independent_stable_output(self):
|
||||
self.a['layer_coverage']['Narayana_AD'] = 'blocked'
|
||||
self.a['stable_claim_ids'] = ['synthetic:d1-common']
|
||||
r = assess_use(self.a, self.binding)
|
||||
self.assertEqual(r['status'], 'conditional')
|
||||
self.assertEqual(r['allowed_claim_ids'], ['synthetic:d1-common'])
|
||||
|
||||
def test_all_missing_is_not_stability(self):
|
||||
self.a['layer_coverage'] = {k: 'blocked' for k in self.a['required_layers']}
|
||||
self.a['stable_claim_ids'] = []
|
||||
r = assess_use(self.a, self.binding)
|
||||
self.assertEqual(r['status'], 'insufficient_evidence')
|
||||
self.assertFalse(r['can_continue_analysis'])
|
||||
|
||||
def test_unresolved_method_does_not_certify_scope(self):
|
||||
self.a['branch_state'] = 'unresolved_material'
|
||||
self.assertEqual(assess_use(self.a, self.binding)['status'], 'conditional')
|
||||
|
||||
def test_unchecked_remaining_ranges_prevent_full_permission(self):
|
||||
self.a['domain_complete'] = False
|
||||
self.assertFalse(assess_use(self.a, self.binding)['can_use_interval'])
|
||||
|
||||
def test_bounded_equivalent_branches_can_be_used(self):
|
||||
self.a['branch_state'] = 'bounded_equivalent'
|
||||
self.assertEqual(assess_use(self.a, self.binding)['status'], 'scope_ready')
|
||||
|
||||
def test_no_stable_outputs_cannot_pass_vacuously(self):
|
||||
self.a['stable_claim_ids'] = []
|
||||
self.assertEqual(assess_use(self.a, self.binding)['status'], 'insufficient_evidence')
|
||||
|
||||
def test_conditions_remain_visible(self):
|
||||
self.a['conditional_claim_ids'] = ['synthetic:branch-a']
|
||||
r = assess_use(self.a, self.binding)
|
||||
self.assertEqual(r['status'], 'conditional')
|
||||
self.assertIn('synthetic:branch-a', r['conditional_claim_ids'])
|
||||
|
||||
def test_malformed_contracts_fail_closed(self):
|
||||
for changes in ({'required_layers': []}, {'material_change': 'false'},
|
||||
{'scan_state': 'done'}, {'binding': {}},
|
||||
{'conditional_claim_ids': self.a['stable_claim_ids']},
|
||||
{'layer_coverage': {'D1': 'not_applicable'}}):
|
||||
with self.subTest(changes=changes):
|
||||
with self.assertRaises(ValueError):
|
||||
assess_use(dict(self.a, **changes), self.binding)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,127 @@
|
||||
"""出生时间「按用途够用」的用途决策合同(BUG-1833,#186 第一步)。
|
||||
|
||||
移植自占星师 2026-11-09 的参考决策器
|
||||
(``docs/tasks/assets/purpose-gate-20261109/reference_gate.py``):
|
||||
没有区间宽度门槛,也没有分数差门槛——这是参考代码刻意为之。
|
||||
|
||||
本模块只回答「这个已经算好的评估,可以在什么范围内被当作够用」,
|
||||
不做任何计算、不改出生记录、不证明调用方给的证据为真:
|
||||
|
||||
- ``can_use_interval`` 只有 ``scope_ready`` 才为真;
|
||||
- ``can_modify_birth_record`` 与 ``representative_is_birth_truth`` 永远为假。
|
||||
|
||||
**尚未接入**:本单只交付合同与测试,没有任何生产入口读它,
|
||||
``scope_ready`` 也不会影响任何现有流程。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Final, List, Mapping, Sequence, Tuple
|
||||
|
||||
PURPOSE_USE_SCHEMA: Final[str] = 'jyotish.purpose_use_decision.v1'
|
||||
|
||||
BINDING_KEYS: Final[Tuple[str, ...]] = (
|
||||
'input_revision', 'candidate_set_fingerprint', 'purpose_fingerprint',
|
||||
'profile_fingerprint', 'engine_version', 'precision_policy_version',
|
||||
'forecast_window_fingerprint',
|
||||
)
|
||||
|
||||
LAYER_COVERAGE_STATES: Final[Tuple[str, ...]] = ('executed_complete', 'partial', 'blocked')
|
||||
SCAN_STATES: Final[Tuple[str, ...]] = ('policy_covered', 'sampled_only', 'incomplete')
|
||||
BRANCH_STATES: Final[Tuple[str, ...]] = (
|
||||
'resolved', 'bounded_equivalent', 'unresolved_material', 'not_evaluated',
|
||||
)
|
||||
|
||||
STATUS_SCOPE_READY: Final[str] = 'scope_ready'
|
||||
STATUS_CONDITIONAL: Final[str] = 'conditional'
|
||||
STATUS_NEEDS_REFINEMENT: Final[str] = 'needs_refinement'
|
||||
STATUS_INSUFFICIENT_EVIDENCE: Final[str] = 'insufficient_evidence'
|
||||
STATUS_STALE: Final[str] = 'stale'
|
||||
|
||||
|
||||
def _binding_is_complete(binding: Any) -> bool:
|
||||
return isinstance(binding, dict) and all(
|
||||
isinstance(binding.get(key), str) and binding[key] for key in BINDING_KEYS
|
||||
)
|
||||
|
||||
|
||||
def _claim_ids(assessment: Mapping[str, Any], field: str) -> List[str]:
|
||||
values = assessment.get(field)
|
||||
if not isinstance(values, list) or any(not isinstance(value, str) or not value for value in values):
|
||||
raise ValueError(f'invalid {field}')
|
||||
if len(set(values)) != len(values):
|
||||
raise ValueError(f'duplicate {field}')
|
||||
return list(values)
|
||||
|
||||
|
||||
def _validate_assessment(assessment: Mapping[str, Any]) -> None:
|
||||
"""校验绑定一致的评估内容,字段不合法时抛出 ValueError。"""
|
||||
required = assessment.get('required_layers')
|
||||
coverage = assessment.get('layer_coverage')
|
||||
if (not isinstance(required, list) or not required
|
||||
or any(not isinstance(layer, str) or not layer for layer in required)
|
||||
or len(set(required)) != len(required)
|
||||
or not isinstance(coverage, dict)):
|
||||
raise ValueError('explicit nonempty required layer set required')
|
||||
if any(coverage.get(layer) not in LAYER_COVERAGE_STATES for layer in required):
|
||||
raise ValueError('required layers cannot be absent or not_applicable')
|
||||
if assessment.get('scan_state') not in SCAN_STATES:
|
||||
raise ValueError('invalid scan_state')
|
||||
if assessment.get('branch_state') not in BRANCH_STATES:
|
||||
raise ValueError('invalid branch_state')
|
||||
for flag in ('domain_complete', 'material_change'):
|
||||
if type(assessment.get(flag)) is not bool: # noqa: E721 - 只认真正的 bool,不认 0/1
|
||||
raise ValueError(f'{flag} must be a boolean')
|
||||
_claim_ids(assessment, 'stable_claim_ids')
|
||||
_claim_ids(assessment, 'conditional_claim_ids')
|
||||
if set(assessment['stable_claim_ids']) & set(assessment['conditional_claim_ids']):
|
||||
raise ValueError('one fact cannot be both stable and conditional')
|
||||
|
||||
|
||||
def _result(status: str, stable: Sequence[str], conditional: Sequence[str], reason: str) -> Dict[str, Any]:
|
||||
allowed = list(dict.fromkeys(list(stable) + list(conditional)))
|
||||
return {
|
||||
'schema': PURPOSE_USE_SCHEMA,
|
||||
'status': status,
|
||||
'can_use_interval': status == STATUS_SCOPE_READY,
|
||||
'can_continue_analysis': bool(allowed),
|
||||
'can_modify_birth_record': False,
|
||||
'representative_is_birth_truth': False,
|
||||
'stable_claim_ids': list(stable),
|
||||
'conditional_claim_ids': list(conditional),
|
||||
'allowed_claim_ids': allowed,
|
||||
'reason': reason,
|
||||
}
|
||||
|
||||
|
||||
def assess_use(assessment: dict, expected_binding: dict) -> dict:
|
||||
"""Return permissions for the evaluated scope, not birth-time confidence.
|
||||
|
||||
Deliberately has no interval-width or score-margin threshold.
|
||||
stable_claim_ids must cover the entire remaining candidate domain;
|
||||
conditional_claim_ids must point to explicit branch/limitation evidence.
|
||||
"""
|
||||
binding = assessment.get('binding')
|
||||
for candidate in (binding, expected_binding):
|
||||
if not _binding_is_complete(candidate):
|
||||
raise ValueError('complete server binding required')
|
||||
if any(binding[key] != expected_binding[key] for key in BINDING_KEYS):
|
||||
return _result(STATUS_STALE, [], [], 'binding_changed_recompute')
|
||||
|
||||
_validate_assessment(assessment)
|
||||
required = assessment['required_layers']
|
||||
coverage = assessment['layer_coverage']
|
||||
stable = assessment['stable_claim_ids']
|
||||
conditional = assessment['conditional_claim_ids']
|
||||
|
||||
if assessment['material_change']:
|
||||
return _result(STATUS_NEEDS_REFINEMENT, stable, conditional, 'material_output_change')
|
||||
complete = all(coverage[layer] == 'executed_complete' for layer in required)
|
||||
if (complete and assessment['domain_complete']
|
||||
and assessment['scan_state'] == 'policy_covered'
|
||||
and assessment['branch_state'] in ('resolved', 'bounded_equivalent')
|
||||
and stable and not conditional):
|
||||
return _result(STATUS_SCOPE_READY, stable, [], 'adequate_for_frozen_purpose')
|
||||
if stable or conditional:
|
||||
return _result(STATUS_CONDITIONAL, stable, conditional, 'only_listed_claims_are_available')
|
||||
return _result(STATUS_INSUFFICIENT_EVIDENCE, [], [], 'no_supported_outputs_for_purpose')
|
||||
@@ -0,0 +1,150 @@
|
||||
"""出生时间按用途够用:用途决策合同的 16 条测试(#186,BUG-1833)。
|
||||
|
||||
13 条移植自占星师的参考测试(`docs/tasks/assets/purpose-gate-20261109/test_reference_gate.py`),
|
||||
3 条新增(两条永远为假的许可、用合成示例驱动 status、过期绑定优先)。
|
||||
全部是合成数据,不含任何个人资料。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
for path in (str(ROOT), str(ROOT / 'scripts')):
|
||||
if path not in sys.path:
|
||||
sys.path.insert(0, path)
|
||||
|
||||
from scripts.rectification_purpose_gate import BINDING_KEYS, assess_use # noqa: E402
|
||||
|
||||
ASSETS = ROOT / 'docs' / 'tasks' / 'assets' / 'purpose-gate-20261109'
|
||||
EXAMPLES = json.loads((ASSETS / 'examples.json').read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
class PurposeGateTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.case = EXAMPLES['ready_interval']
|
||||
self.a = copy.deepcopy(self.case['assessment'])
|
||||
self.binding = copy.deepcopy(self.a['binding'])
|
||||
|
||||
def test_wide_interval_can_be_used_without_confirming_birth(self):
|
||||
self.assertEqual(self.case['range']['duration_seconds'], 1200)
|
||||
result = assess_use(self.a, self.binding)
|
||||
self.assertEqual(result['status'], 'scope_ready')
|
||||
self.assertTrue(result['can_continue_analysis'])
|
||||
self.assertFalse(result['can_modify_birth_record'])
|
||||
|
||||
def test_short_interval_can_need_refinement(self):
|
||||
self.a['material_change'] = True
|
||||
self.assertEqual(assess_use(self.a, self.binding)['status'], 'needs_refinement')
|
||||
|
||||
def test_equal_scores_do_not_block_scope_use(self):
|
||||
self.a['legacy_scores'] = [8, 8]
|
||||
self.a['historical_confidence'] = 'low'
|
||||
self.assertTrue(assess_use(self.a, self.binding)['can_use_interval'])
|
||||
|
||||
def test_every_binding_change_invalidates_permission(self):
|
||||
for key in BINDING_KEYS:
|
||||
with self.subTest(key=key):
|
||||
changed = dict(self.binding, **{key: 'different'})
|
||||
result = assess_use(self.a, changed)
|
||||
self.assertEqual(result['status'], 'stale')
|
||||
self.assertFalse(result['can_continue_analysis'])
|
||||
self.assertFalse(result['can_use_interval'])
|
||||
|
||||
def test_stale_binding_takes_precedence_over_malformed_assessment(self):
|
||||
changed_binding = dict(self.binding, engine_version='different')
|
||||
for changes in ({'scan_state': 'done'}, {'material_change': 'false'}, {'required_layers': []}):
|
||||
with self.subTest(changes=changes):
|
||||
result = assess_use(dict(self.a, **changes), changed_binding)
|
||||
self.assertEqual(result['status'], 'stale')
|
||||
self.assertIs(result['can_continue_analysis'], False)
|
||||
|
||||
def test_sampling_does_not_certify_whole_interval(self):
|
||||
for state in ('sampled_only', 'incomplete'):
|
||||
with self.subTest(state=state):
|
||||
self.a['scan_state'] = state
|
||||
self.assertEqual(assess_use(self.a, self.binding)['status'], 'conditional')
|
||||
|
||||
def test_missing_layer_keeps_independent_stable_output(self):
|
||||
self.a['layer_coverage']['Narayana_AD'] = 'blocked'
|
||||
self.a['stable_claim_ids'] = ['synthetic:d1-common']
|
||||
result = assess_use(self.a, self.binding)
|
||||
self.assertEqual(result['status'], 'conditional')
|
||||
self.assertEqual(result['allowed_claim_ids'], ['synthetic:d1-common'])
|
||||
|
||||
def test_all_missing_is_not_stability(self):
|
||||
self.a['layer_coverage'] = {key: 'blocked' for key in self.a['required_layers']}
|
||||
self.a['stable_claim_ids'] = []
|
||||
result = assess_use(self.a, self.binding)
|
||||
self.assertEqual(result['status'], 'insufficient_evidence')
|
||||
self.assertFalse(result['can_continue_analysis'])
|
||||
|
||||
def test_unresolved_method_does_not_certify_scope(self):
|
||||
self.a['branch_state'] = 'unresolved_material'
|
||||
self.assertEqual(assess_use(self.a, self.binding)['status'], 'conditional')
|
||||
|
||||
def test_unchecked_remaining_ranges_prevent_full_permission(self):
|
||||
self.a['domain_complete'] = False
|
||||
self.assertFalse(assess_use(self.a, self.binding)['can_use_interval'])
|
||||
|
||||
def test_bounded_equivalent_branches_can_be_used(self):
|
||||
self.a['branch_state'] = 'bounded_equivalent'
|
||||
self.assertEqual(assess_use(self.a, self.binding)['status'], 'scope_ready')
|
||||
|
||||
def test_no_stable_outputs_cannot_pass_vacuously(self):
|
||||
self.a['stable_claim_ids'] = []
|
||||
self.assertEqual(assess_use(self.a, self.binding)['status'], 'insufficient_evidence')
|
||||
|
||||
def test_conditions_remain_visible(self):
|
||||
self.a['conditional_claim_ids'] = ['synthetic:branch-a']
|
||||
result = assess_use(self.a, self.binding)
|
||||
self.assertEqual(result['status'], 'conditional')
|
||||
self.assertIn('synthetic:branch-a', result['conditional_claim_ids'])
|
||||
|
||||
def test_malformed_contracts_fail_closed(self):
|
||||
for changes in ({'required_layers': []}, {'material_change': 'false'},
|
||||
{'scan_state': 'done'}, {'binding': {}},
|
||||
{'conditional_claim_ids': self.a['stable_claim_ids']},
|
||||
{'layer_coverage': {'D1': 'not_applicable'}}):
|
||||
with self.subTest(changes=changes):
|
||||
with self.assertRaises(ValueError):
|
||||
assess_use(dict(self.a, **changes), self.binding)
|
||||
|
||||
# ---- 本单新增的两条 ----
|
||||
|
||||
def test_permissions_never_allow_editing_the_birth_record(self):
|
||||
"""无论评估通过到什么程度,都不许改出生记录,也不许把代表时刻当生辰。"""
|
||||
scenarios = []
|
||||
for status_case in ('scope_ready', 'conditional', 'insufficient_evidence', 'needs_refinement', 'stale'):
|
||||
assessment = copy.deepcopy(self.a)
|
||||
if status_case == 'needs_refinement':
|
||||
assessment['material_change'] = True
|
||||
elif status_case == 'conditional':
|
||||
assessment['scan_state'] = 'sampled_only'
|
||||
elif status_case == 'insufficient_evidence':
|
||||
assessment['stable_claim_ids'] = []
|
||||
scenarios.append((status_case, assessment, self.binding))
|
||||
scenarios.append(('stale', copy.deepcopy(self.a), dict(self.binding, engine_version='other')))
|
||||
for name, assessment, binding in scenarios:
|
||||
with self.subTest(case=name):
|
||||
result = assess_use(assessment, binding)
|
||||
self.assertIs(result['can_modify_birth_record'], False)
|
||||
self.assertIs(result['representative_is_birth_truth'], False)
|
||||
|
||||
def test_every_synthetic_example_matches_its_expected_status(self):
|
||||
for name, example in EXAMPLES.items():
|
||||
if not isinstance(example, dict) or 'expected_status' not in example:
|
||||
continue
|
||||
with self.subTest(example=name):
|
||||
assessment = copy.deepcopy(EXAMPLES['ready_interval']['assessment'])
|
||||
assessment.update(copy.deepcopy(example.get('assessment_changes_from_ready') or {}))
|
||||
result = assess_use(assessment, copy.deepcopy(assessment['binding']))
|
||||
self.assertEqual(result['status'], example['expected_status'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user