Compare commits

..
13 changed files with 111 additions and 167 deletions
-5
View File
@@ -1,10 +1,5 @@
# BLOCKED
## #237 · Windows 全量门禁与预检
- 快速门的三项 PyJHora 检查在同机基线复现。咨询 VedAstro 对照在门禁内波动,分支与基线单独重跑均通过。目录预检超时,定向预检复现既有历史镜像路径断言失败。
- 替代证据:报告与选盘定向回归、真实 PostgreSQL 报告档案测试和仓库隐私检查通过。详见 [#237 进度](docs/tasks/PROGRESS-reading-window-edge-20261113.md)。
## BUG-1865 · 选盘保存的计算时刻可能落在所选区间外(#227,2026-11-13)
- ~~旧流程区间缺少采纳凭据。~~ 修订任务书已明确优先读取档案的选盘采纳记录,并限定旧流程的凭据。
-4
View File
@@ -1,9 +1,5 @@
# 印度占星 Skill 更新日志
## 2026-11-13 — 个人报告按区间内部的换座核对证据
- 生时纠正选定的区间、填写范围及默认范围,只在区间内部发生换座时标记分盘敏感。报告摘要和证据等级按同一规则判定。沿用当前 Skill 版本(BUG-1871)。
## 2026-11-13 — 个人报告显示章节拦截原因
- 页面和 Markdown 导出会解释对应章节为何未通过核对。新解读保存生成快照和逐节转换记录,便于复查和回放。
-13
View File
@@ -25229,16 +25229,3 @@
- 防复发:测试分别锁定档案权限、请求转交、缓存隔离与公开盘的候选瑜伽结果。
- 关联记录:#228、#189、BUG-1063。
- 修复版本:`codex/report-gender-20261113`,待发布。
## BUG-1871 | 个人报告把区间端点当成区间内换座
- 状态:fixed-pending-verify(待独立验收)。
- 首次发现 / 最近更新:2026-11-13 / 2026-11-13。
- 影响面:个人报告的分盘换座摘要和证据等级上限。
- 现象:已采纳侧起点的换座线被报为区间内换座,依赖相应分盘的结论降级。
- 触发条件:距最近换座的时间恰等于出生时间区间一侧的宽度。
- 根因:BUG-1864 的区间检查用包含端点的比较,既有测试也锁定了该行为。BUG-1865 让已采纳区间到达报告后,选择过的换座线经常成为端点,摘要与证据等级两处都将其判入区间。
- 修复:两处共用区间判定,未确认分钟时只计严格落在端点容差以内的内部换座。确认分钟仍按原有前后各一分钟的规则。
- 验证及防复发:合成事实覆盖只在端点换座、区间内有后续换座和两侧容差,分别检查分盘状态、摘要及各领域上限。确认分钟的端点和端点外也有回归覆盖。
- 关联记录:#237、BUG-1864、BUG-1865、BUG-1858。详见 [执行进度](tasks/PROGRESS-reading-window-edge-20261113.md)。
- 修复版本:`codex/reading-window-edge-20261113`,待发布。
-4
View File
@@ -2,10 +2,6 @@
Purpose: read this file before substantial project work. It exists to stop repeat mistakes caused by multiple Codex windows, WorkBuddy mirrors, local drafts, backup folders, and partial cloud-git visibility.
## 2026-11-13 · #237 Windows 报告预检
- 远端可见性、Python 和适配器检查通过。首次目录扫描及定向测试超时。外层命令时限延长至 180 秒后,目录扫描仍在 90 秒超时,定向测试仅余既有 `.workbuddy` 镜像路径断言失败。见 [#237 进度](../tasks/PROGRESS-reading-window-edge-20261113.md)。
## 2026-11-13 · #220 报告解读快照
- Windows 预检可见 Gitea 远端,碎片扫描与适配器步骤通过。既有 `test_preflight_fragment_scan_reports_authority_layers_and_risk_buckets` 仍要求本机不存在的 `.workbuddy` 历史镜像路径。预检退出 1。
@@ -1,23 +0,0 @@
# #237 执行进度 · 分盘换座的区间端点
Codex 基于 `origin/staging` 的 `151159927` 实施。任务书提交 `c2f295fd8` 相对规划基线 `3e6077f75` 只新增任务书。交付版本中的任务书移除原案例的具体时刻,保留验收用的相对分钟数。
报告摘要和未确认分钟的证据等级共用 `crosses_birth_window`。采纳区间、填写范围和默认范围的两侧都使用 `换座距离 < 该侧宽度 − 0.01 分钟`。0.01 分钟约为 0.6 秒,可吸收小于一秒的取整差异。合成回归分别检查离端点 0.005 分钟和 0.02 分钟的位置,后者仍判为区间内部。已确认分钟仍按包含端点的前后各一分钟规则判断。
合成事实包中,D2、D6、D8 只有起点换座,状态为 `clear_at_tolerance`,摘要写「区间内不换座」。D10、D9 在区间内仍有换座,状态为 `boundary_sensitive`。财富收入、财富留存和健康压力的上限为 C,事业和婚恋结构仍为 D。
| 原值 | 新值 | 原因 |
| --- | --- | --- |
| 填写区间后端点的财富收入上限 `D` | `C` | 后端点的换座不属于区间内部。 |
| 摘要对前端点写「区间内会换座」 | 「区间内不换座」 | 前侧换座距离等于前侧宽度。 |
| 摘要对后端点写「区间内会换座」 | 「区间内不换座」 | 后侧换座距离等于后侧宽度,前侧在搜索上限内未换座。 |
| 默认区间端点的事业上限 `D` | `C` | 默认窗口同样排除端点,用途稳定性仍为 `not_evaluated`。 |
## 验证
- 新增两项回归均在修复前复现端点误判。
- Python 定向 53 项通过,覆盖任务书的四个文件和既有选盘测试。报告出生时间范围与 worker 的前端定向 34 项通过,真实 PostgreSQL 的报告档案测试 1 项通过。Node 两组测试均无失败和取消。
- Python 快速门退出 1,1,462 项通过、2 项跳过、29 项子测试通过、4 项失败。其中 3 项 PyJHora 检查在同机基线逐项复现。另一项是既有咨询 VedAstro 对照波动,分支和基线各单独重跑一次均通过。
- Windows 预检确认 Python、Gitea 远端和适配器可用。目录扫描超时。延长定向测试时限后,只有既有 `.workbuddy` 镜像路径断言失败。详见 [预检台账](../research/pre_work_error_ledger.md)。
- 仓库隐私检查 62 项通过。
- 同步含 #210 的 staging `e5d6c99da` 后,年运审计、分盘范围、证据等级、用途稳定性和隐私合跑 89 项通过。
-1
View File
@@ -33,7 +33,6 @@
| 任务书 | 进度 | 主题 | 状态 | 落点 |
| --- | --- | --- | --- | --- |
| [#237 任务书](TASK-reading-window-edge-20261113.md) | [执行进度](PROGRESS-reading-window-edge-20261113.md) | 个人报告区间端点换座判定(BUG-1871) | 待验收 | `codex/reading-window-edge-20261113` |
| [#220 修订任务书](FIX-reading-snapshot-binding-1.md) | [执行进度](PROGRESS-reading-snapshot-binding-20261112.md) | 个人报告生成快照、逐节拦截原因和离线验收包(BUG-1863) | 待验收 | `codex/reading-snapshot-binding-20261112` |
截至 2026-09-03,`main` 停在 2026-08-16 的 `7b620c7a`,下表**没有任何一项已上线**。
@@ -1,55 +0,0 @@
# TASK:生时纠正选定一侧的起点就是已解决的换盘线,不能再算成「区间内会换座」(2026-11-13)
- Gitea:#237
- 分支:`codex/reading-window-edge-20261113`,基于 staging `3e6077f752b606fee69a9eb15853fe328801901b`
- Bug 编号:BUG-1871
- 验收:Claude(jyotisha-e6)
不得自行写「产品决定/裁定」;任务书没写的取舍一律选需拍板。不得修改或删除既有断言来让测试通过;确需修改的,写明原值、新值、原因。
## 现象(产品负责人 2026-11-13 实测报告)
选盘页采纳换盘线后一侧后,#227 部署的报告正确识别了生时纠正选定的区间。下表用相对计算时刻的合成测试输入复现误判,窗口前侧 0.3 分钟、后侧 15 分钟。事业、财富、健康三节都降成 D:
| 分盘 | 提前多少分钟换座 | 推后多少分钟换座 | 实际情况 |
|---|---|---|---|
| 财富分盘 D2 | 0.3 | 147.167 | 线后一侧整段都不换座 |
| 健康分盘 D6 | 0.3 | 24.367 | 线后一侧整段都不换座 |
| D8 | 0.3 | 18.2 | 线后一侧整段都不换座 |
| 事业分盘 D10 | 0.3 | 14.5 | 区间内会再换一次,这次判断是对的 |
| 九分盘 D9 | 8.517 | 7.917 | 区间内会换,这次判断是对的 |
「提前 0.3 分钟就换座」指的正是区间起点,也就是用户在选盘页已经选过的那条线。区间就是从这条线开始的,线后的盘是固定的。可是代码判断用的是 `before <= window.before_minutes`(0.3 ≤ 0.3),把这条线本身也算成了「区间内换座」。
## 要改的地方
1. `scripts/report_render/evidence_grade.py::_varga_boundary` 和 `scripts/report_render/birth_precision_margins.py`(生成 `summary_zh` 里「区间内会/不会换座」那一句)用同一条规则。抽成一个共享函数,两处都调用它。
2. 规则:只有换座点**严格落在区间内部**,才算区间内会换座。
- 前侧:`before < window.before_minutes - ε`
- 后侧:`after < window.after_minutes - ε`
- ε 取 0.01 分钟,用来吸收秒级的取整误差。在 PROGRESS 里写明为什么取这个值。
- 这条规则对 `rectified_range`、`user_stated` 和默认窗口都适用:换座点正好落在区间端点上,本来就不算区间内。
3. 已确认到分钟(±1 分钟)的旧规则不受影响,原有测试保持通过。
## 不要碰
- `frontend/**`
- `references/**`
- 选盘页本身(#229 负责)
- 冻结文件
## 验收(新测试)
1. 按上表构造合成事实包(`rectified_range`,前 0.3、后 15):
- D2、D6、D8 判为 `clear_at_tolerance`,`summary_zh` 写「区间内不换座」;
- D10、D9 判为 `boundary_sensitive`;
- `wealth_earning`、`wealth_retention`、`health_pressure` 的上限回到 C,`career_promise` 和 `relationship_structure` 仍然是 D。
2. 换座点比区间端点早 0.02 分钟(落在区间内)时,判为会换座。
3. #224、#227、#215 的测试全部通过。
## 自测命令
```
python3 -m pytest -q tests/test_report_birth_window_20261113.py tests/test_report_evidence_grade_20261112.py tests/test_evidence_grade_purpose_stability_20261112.py tests/test_report_reading_facts.py
python3 scripts/run_quality_gate.py --profile quick --skip-frontend-runtime
```
@@ -0,0 +1,98 @@
# TASK:全站用户可见文字去掉「·」(UI 规则 8,2026-11-13)
- Gitea:#234
- 分支:`codex/remove-middle-dot-20261113`,从 staging `e5d6c99d` 切出(#220 已合入)。
- Bug 编号:BUG-1870。开工时核对 `docs/BUG_HISTORY.md` 末尾;若已被占用就顺延,并在 PROGRESS 写明。
- 负责验收:Claude(jyotisha-de)
任务书没写的取舍,不得写成"产品决定"或"裁定",一律标「需拍板」。
不得修改或删除既有断言来让测试通过。确需修改,在 PROGRESS 里逐条写明原值、新值和原因。只因「·」改成新写法而改断言的,算正常改动,也要列出。
## 背景
- 产品负责人 UI 规则 8(2026-11-06):不用中间点「·」。
- 产品负责人 2026-11-13 同意了按场景替换的规则,原型见 https://claude.ai/artifact/7nq8yt22n8L3KXiAFQXk4z,「全站去掉「·」」那一张。
- staging 实测:前端约 300 处、60 多个文件。后台导出文本里也有,主要在 `scripts/pl9_full_data_export.py`、`pl9_reader_export.py`、`pl9_reader_english.py`、`pl9_language_terms.py`、`scripts/report_render/audit_chapters.py`、`upstream_coverage.py`。
## 替换规则(已定)
| 场景 | 现在 | 改成 |
|---|---|---|
| 浏览器标签标题 | `星盘 · Jyotisha` | `星盘 - Jyotisha` |
| 中文的两段信息(日期、进度、来源等) | `生日年 · 2026-03-14 到 2027-03-14` | `生日年,2026-03-14 到 2027-03-14` |
| 宫位加星座 | `第 1 宫 · 白羊:太阳` | `第 1 宫(白羊):太阳` |
| 英文同类 | `House 1 · Aries` | `House 1 (Aries)` |
| 并列名称 | `金星 · 土星 · 月亮` | `金星、土星、月亮`;英文用 `, ` |
| 卡片里并排的小字参数 | `D9 · 整宫 · Lahiri` | 拆成并排的元素,靠间距分开,不加任何符号。间距用现有 gap 变量 |
- 不在上表里的情况,选最接近的一行,并在 PROGRESS 写明理由。
- 拿不准的,列进 PROGRESS 的「需拍板」小节,先不改。
- 不得改成其他点状符号,例如 `•` `・` `‧` `∙`,也不得改成 `|` `/`。
## 范围
**要改:用户看得到的文字**
- 网页:页面、组件、metadata title。
- 导出的 .md:个人报告、合盘、年运。
- 报告正文与附录,包括后台渲染出来给用户看的文本。
- `frontend/src/lib/personal-report-reading-contract.ts` 的 basisAuditLine(`${claim} · 上限 …`),来自 #218、#219。
- `frontend/src/components/annual/annual-report-page.tsx` 的头部和进度两处。
**只列清单、不改**
- 后台管理页:`components/admin/**`、`app/admin/**`、`app/(admin)/**`。
- 代码注释。
- 给模型的提示词和写作要求,例如 `consultation-methodology.ts`、`report-interpretation-packs/**`。改提示词会影响模型输出,要另评估。
- 其中有的文本既进提示词又直接显示给用户。这种列进「需拍板」,不改。
- `docs/**`、`references/**`、法律文本 `legal-documents.ts`(法律文本改动要单独过)。
**不要碰**
- 冻结文件:`scripts/jyotish_engine.py`、`jyotish_api_server.py`、`yoga_engine.py`、`consultation_native_layers.py`、`tajika_mudda.py`、`shadbala*.py`、`narayana_*.py`、`functional_benefics.py`、`special_lagnas.py`。其中有「·」的,列进清单。
- `frontend/src/lib/consult-preset-prompts.ts`
## 防复发
新增一个测试 `frontend/tests/no-middle-dot-20261113.test.ts`:
- 扫描 `frontend/src/**/*.{ts,tsx}`,排除上面「只列清单」的路径和注释;
- 字符串里出现「·」就失败,并报出文件和行号;
- 允许清单写在测试文件里,每条注明原因。清单只能放「只列清单」那几类。
后台加一个同样作用的 Python 测试 `tests/test_no_middle_dot_20261113.py`,只覆盖本单改过的后台渲染文件。
## 要改的文件
- 上述范围内的前端和后台文件,以及 CSS(卡片小字拆成并排元素时用)。
- 两个新测试
- `docs/BUG_HISTORY.md`:只在末尾追加一条。
- `docs/tasks/PROGRESS-remove-middle-dot-20261113.md`
## 验收标准
1. 新的两个测试通过,允许清单里只有「只列清单」那几类。
2. 抽查以下页面,没有「·」,文字没有挤压或截断:
- 星盘页
- 个人报告页和它导出的 .md
- 合盘报告
- 年运页
- 报告中心
- 侧栏
3. 卡片小字拆开后,在 390px 宽度下不换行错位;必要时可以换行,但不截断。
4. 现有测试全部通过(基线失败除外,在 PROGRESS 写明)。
5. tsc、eslint(0 错)、quick gate 通过。
## 自测命令
```
cd frontend && npx tsc --noEmit -p . && npx eslint . && npm test
PATH=<venv>/bin:$PATH PYTHONHASHSEED=0 python scripts/run_quality_gate.py --profile quick --skip-frontend-runtime
```
## 进度记录
写在 `docs/tasks/PROGRESS-remove-middle-dot-20261113.md`,包括:
- 每个文件改了几处、用了哪条规则;
- 「只列清单」的完整清单;
- 「需拍板」小节;
- 改过的断言;
- 测试结果。
@@ -8,8 +8,6 @@ from domain_calculation_service import swiss_ephemeris_lock
from jyotish_engine import _birth_hour_decimal, _sidereal_calculation_profile, swe
from varga import calc_varga
from .birth_precision_window import crosses_birth_window
_CAP_SECONDS = 240 * 60
_STEP_SECONDS = 30
@@ -55,7 +53,10 @@ def _margin_summary_zh(before: float | None, after: float | None, cap: float, wi
else f"出生时间提前 {cap:g} 分钟内上升未换座")
later = (f"推后 {after:g} 分钟才换座" if after is not None
else f"推后 {cap:g} 分钟内未换座")
crosses = crosses_birth_window(before, after, window)
crosses = (
before is not None and 0 < window["before_minutes"] and before <= window["before_minutes"]
or after is not None and 0 < window["after_minutes"] and after <= window["after_minutes"]
)
conclusion = "区间内会换座" if crosses else "区间内不换座"
return f"{earlier};{later}。{conclusion}。"
@@ -7,7 +7,6 @@ import re
CONFIRMED_TOLERANCE_MINUTES = 1.0
DEFAULT_TOLERANCE_MINUTES = 30.0
_WINDOW_EDGE_TOLERANCE_MINUTES = 0.01
WINDOW_POLICY = "birth_record_window_v1"
_DECLARED_SOURCES = {"user_stated", "shichen", "period", "rectified_range"}
_MAX_SIDE_MINUTES = 720
@@ -65,16 +64,6 @@ def report_birth_window(value: object, *, minute_confirmed: bool) -> dict:
return result
def crosses_birth_window(before: float | None, after: float | None, window: dict) -> bool:
"""Check for a crossing in either direction, keeping the confirmed-minute rule inclusive."""
before_limit, after_limit = window["before_minutes"], window["after_minutes"]
if window.get("source") == "confirmed_minute":
return (before is not None and before <= before_limit
or after is not None and after <= after_limit)
return (before is not None and before < before_limit - _WINDOW_EDGE_TOLERANCE_MINUTES
or after is not None and after < after_limit - _WINDOW_EDGE_TOLERANCE_MINUTES)
def birth_window_label_zh(window: dict) -> str:
source = window["source"]
before, after = window["before_minutes"], window["after_minutes"]
+3 -2
View File
@@ -30,7 +30,7 @@ from __future__ import annotations
import re
from typing import Any
from .birth_precision_window import CONFIRMED_TOLERANCE_MINUTES, WINDOW_POLICY, crosses_birth_window, report_birth_window
from .birth_precision_window import CONFIRMED_TOLERANCE_MINUTES, WINDOW_POLICY, report_birth_window
try:
from scripts.consult_card_domain_facts import PRECISION_VARGA
@@ -165,7 +165,8 @@ def _varga_boundary(facts: dict, code: str) -> tuple[str, dict[str, Any]]:
before = float(margin["before_minutes"])
after = float(margin["after_minutes"])
detail.update(before_change_minutes=before, after_change_minutes=after, nearest_change_minutes=min(before, after))
crosses = crosses_birth_window(before, after, window)
crosses = (0 < window["before_minutes"] and before <= window["before_minutes"]
or 0 < window["after_minutes"] and after <= window["after_minutes"])
return ("boundary_sensitive" if crosses else "clear_at_tolerance"), detail
if not isinstance(margin, dict):
return "missing", {"policy": PRECISION_POLICY, "reason": "no ascendant margin for this varga"}
+5 -45
View File
@@ -2,7 +2,7 @@
from scripts.professional_report_reference import _reading_request
from scripts.report_render.birth_precision_margins import _margin_summary_zh
from scripts.report_render.evidence_grade import _varga_boundary, evidence_grades
from scripts.report_render.evidence_grade import evidence_grades
from scripts.report_render.reading_facts import _birth_precision
from tests.test_report_evidence_grade_20261112 import _facts
@@ -38,7 +38,7 @@ def test_default_window_checks_required_and_optional_vargas():
assert _row(claims, "health_pressure", "D30")["state"] == "not_evaluated"
def test_declared_window_uses_each_side_and_excludes_the_endpoint():
def test_declared_window_uses_each_side_and_includes_the_endpoint():
five = {"before_minutes": 5, "after_minutes": 5, "source": "user_stated"}
claims = evidence_grades(_unconfirmed_facts(five))["claims"]
assert claims["relationship_structure"]["cap"] == "C"
@@ -51,7 +51,7 @@ def test_declared_window_uses_each_side_and_excludes_the_endpoint():
margin.update(before_minutes=0.3, after_minutes=21)
assert evidence_grades(facts)["claims"]["wealth_earning"]["cap"] == "C"
margin["after_minutes"] = 20
assert evidence_grades(facts)["claims"]["wealth_earning"]["cap"] == "C"
assert evidence_grades(facts)["claims"]["wealth_earning"]["cap"] == "D"
def test_shichen_request_reaches_facts_even_with_a_single_calculation_clock():
@@ -119,51 +119,11 @@ def test_rejected_rectified_range_stays_marked_for_intake_and_default():
def test_margin_summary_distinguishes_the_two_directions_and_search_cap():
assert _margin_summary_zh(0.3, 147, 240, {"before_minutes": 0.3, "after_minutes": 19}) == (
"出生时间提前 0.3 分钟上升就换座;推后 147 分钟才换座。区间内不换座。"
"出生时间提前 0.3 分钟上升就换座;推后 147 分钟才换座。区间内会换座。"
)
assert _margin_summary_zh(0.3, 147, 240, {"before_minutes": 0, "after_minutes": 19}) == (
"出生时间提前 0.3 分钟上升就换座;推后 147 分钟才换座。区间内不换座。"
)
assert _margin_summary_zh(None, 19, 240, {"before_minutes": 5, "after_minutes": 19}) == (
"出生时间提前 240 分钟内上升未换座;推后 19 分钟才换座。区间内不换座。"
"出生时间提前 240 分钟内上升未换座;推后 19 分钟才换座。区间内会换座。"
)
def test_rectified_window_only_caps_claims_for_crossings_inside_the_range():
window = {"before_minutes": 0.3, "after_minutes": 15, "source": "rectified_range"}
facts = _unconfirmed_facts(window)
margins = facts["birth_precision"]["ascendant_margins"]
for code, before, after in (
("D2", 0.3, 147.167), ("D6", 0.3, 24.367), ("D8", 0.3, 18.2),
("D10", 0.3, 14.5), ("D9", 8.517, 7.917),
):
margins[code] = {"before_minutes": before, "after_minutes": after}
claims = evidence_grades(facts)["claims"]
for code in ("D2", "D6", "D8", "D10", "D9"):
crosses = code in ("D10", "D9")
assert _varga_boundary(facts, code)[0] == ("boundary_sensitive" if crosses else "clear_at_tolerance")
margin = margins[code]
summary = _margin_summary_zh(margin["before_minutes"], margin["after_minutes"], 240, window)
assert summary.endswith("区间内会换座。" if crosses else "区间内不换座。")
assert {name: claims[name]["cap"] for name in (
"wealth_earning", "wealth_retention", "health_pressure", "career_promise", "relationship_structure",
)} == {"wealth_earning": "C", "wealth_retention": "C", "health_pressure": "C",
"career_promise": "D", "relationship_structure": "D"}
def test_window_edge_tolerance_applies_on_each_side_and_preserves_confirmed_minute():
for source, width in (("user_stated", 5), ("default_unknown_30", 30), ("rectified_range", 15), ("confirmed_minute", 1)):
window = {"before_minutes": width, "after_minutes": width, "source": source}
facts = _unconfirmed_facts(window)
facts["minute_confirmed"] = source == "confirmed_minute"
for offset, unconfirmed_crosses, confirmed_crosses in (
(0.02, False, False), (0, False, True), (-0.005, False, True), (-0.01, False, True), (-0.02, True, True),
):
crosses = confirmed_crosses if facts["minute_confirmed"] else unconfirmed_crosses
for before, after in ((width + offset, 73), (73, width + offset)):
facts["birth_precision"]["ascendant_margins"]["D2"] = {"before_minutes": before, "after_minutes": after}
assert _varga_boundary(facts, "D2")[0] == ("boundary_sensitive" if crosses else "clear_at_tolerance")
summary = _margin_summary_zh(before, after, 240, window)
assert summary.endswith("区间内会换座。" if crosses else "区间内不换座。")
+1 -1
View File
@@ -83,7 +83,7 @@ def test_unconfirmed_birth_time_at_default_boundary_is_not_assumed_stable():
facts = _facts(minute_confirmed=False)
claim = claim_grade(facts, "career_promise")
assert claim["ledger"]["purpose_stability"] == "not_evaluated"
assert claim["cap"] == "C"
assert claim["cap"] == "D"
def test_parameter_sensitive_timing_is_flagged_not_hidden():