Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7fe7e9dd5 | ||
|
|
14ad076fe4 | ||
|
|
187fa375f5 | ||
|
|
c7350bcafb | ||
|
|
fbf04f0b5f | ||
|
|
e96de24721 | ||
|
|
ef9ee62a6b | ||
|
|
5ac88e58f5 | ||
|
|
b9cbe9aa76 | ||
|
|
a7162db271 | ||
|
|
5d452a4ff1 | ||
|
|
d2296c2654 | ||
|
|
e4ff62c738 | ||
|
|
dbba127e71 |
+7
-2
@@ -9,13 +9,18 @@ VEDASTRO_SELF_HOST_ENDPOINT=https://your-domain.example.com/vedastro
|
||||
# VEDASTRO_API_KEY=
|
||||
# VEDASTRO_ENABLE_NETWORK=0
|
||||
|
||||
# Cache / TTL / free-tier queue. These keep ordinary users from calling upstream directly.
|
||||
# Cache / TTL. 网关离线排队与免费层限流是两件事,见下面两组变量。
|
||||
# 失败结果另有短 TTL(VEDASTRO_OFFICIAL_FULL_SNAPSHOT_NEGATIVE_TTL_SECONDS,默认 300 秒,且不超过成功 TTL)。
|
||||
# 这些缓存含出生资料,7 天为上限。
|
||||
VEDASTRO_CACHE_TTL_SECONDS=604800
|
||||
VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS=604800
|
||||
# 网关离线排队作业。这不是免费层限流开关。
|
||||
VEDASTRO_GATEWAY_QUEUE_ENABLED=1
|
||||
VEDASTRO_FREE_TIER_QUEUE=1
|
||||
# 免费层限流由 API key 与下面两项决定。没有 key、且两项都大于 0 时,官方公共端点会限流。
|
||||
# VEDASTRO_FREE_TIER_QUEUE 已废弃,设置它不会打开或关闭限流。
|
||||
# VEDASTRO_FREE_TIER_MAX_REQUESTS=5
|
||||
# VEDASTRO_FREE_TIER_WINDOW_SECONDS=60
|
||||
# VEDASTRO_FREE_TIER_WAIT_BUDGET_SECONDS=
|
||||
VEDASTRO_FAIL_OPEN_LOCAL=1
|
||||
|
||||
# Runtime guardrails.
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-16 — 大运接口不再编造周期,Ashtottari 计入出生余额
|
||||
|
||||
- 选择一套还没接上引擎的大运时,接口会明确回复未实现,不再返回编出来的周期。已经接上的大运与完整解读使用同一套算法。Vimshottari 的算法和返回字段不变。
|
||||
- 完整解读里的 Ashtottari 大运日期改为从出生时已经走过的那一段算起,后面的大运、小运和「当前大运」一起平移。Skill 版本不变(BUG-1341、BUG-1344)。
|
||||
|
||||
## 2026-10-15 — 免费层限流状态与真实行为对齐
|
||||
|
||||
- 诊断和审计里的「免费层是否在限流」改为按真实限流计算。旧的三个排队开关名字还在,但不再控制限流,诊断里会标明它们已废弃。
|
||||
- 限流本身没变:有没有 key、配额是多少,仍按原来的规则。
|
||||
- Skill 版本不变(BUG-1340)。
|
||||
|
||||
## 2026-10-15 — 内测容量:问得太密会停一下,人多时先排一小会儿
|
||||
|
||||
- 同一个人短时间问得太密、校正答得太密,或一小时里报告生成太多,会先停一下,这次不扣点。
|
||||
|
||||
+84
-6
@@ -17314,14 +17314,14 @@
|
||||
|
||||
## BUG-1284 | 旧 Shoola 从月亮星座起并且逐年缩短,注册表仍留着
|
||||
|
||||
- 状态:mitigated
|
||||
- 首次发现 / 最近更新:2026-10-09 / 2026-10-09
|
||||
- 状态:resolved(issue #69,分支 `codex/dasha-unify-20261013`:占位注册表已删除,`/api/dasha` 的 shoola 改走 `rashi_dashas_native` 的 `shoola_forward_md_direct_ad_v1`)
|
||||
- 首次发现 / 最近更新:2026-10-09 / 2026-10-16
|
||||
- 来源:`TASK-rashi-dashas-native-20261008`。
|
||||
- 影响面:`scripts/extended_dashas.py` 的 `calc_shoola_dasha` 与 `'shoola'` 注册项。接口层仍能按这个键调用。完整数据版报告不走这条。
|
||||
- 现象:旧函数从月亮星座起,年数从 9 减到 1,一年按 365.25636 日。所选 Rath 口径是上升或第 7 里较强的星座、一律顺行、每段 9 个日历年。
|
||||
- 触发条件:调用扩展大运注册表里的 `shoola`,或报告误用该函数。
|
||||
- 根因:早期简化实现留在注册表。本单不得改 `scripts/jyotish_api_server.py` 的方法体,所以注册项还在。
|
||||
- 修复:报告改走 `shoola_forward_md_direct_ad_v1`。注册表和接口方法体不动。
|
||||
- 修复:报告改走 `shoola_forward_md_direct_ad_v1`。2026-10-09 当时不改接口方法体。2026-10-16 issue #69 删除 `extended_dashas.py`,接口上的 shoola 改为直接复用同一段。
|
||||
- 验证:三个新模块的语法树不导入 `extended_dashas`。定向 pytest 退出码 0。
|
||||
- 防复发:上述导入断言。报告章不得再调用 `calc_shoola_dasha`。
|
||||
- 相关记录:BUG-1283。
|
||||
@@ -35412,14 +35412,14 @@
|
||||
|
||||
## BUG-1284 | 旧 Shoola 从月亮星座起并且逐年缩短,注册表仍留着
|
||||
|
||||
- 状态:mitigated
|
||||
- 首次发现 / 最近更新:2026-10-09 / 2026-10-09
|
||||
- 状态:resolved(issue #69,分支 `codex/dasha-unify-20261013`:占位注册表已删除,`/api/dasha` 的 shoola 改走 `rashi_dashas_native` 的 `shoola_forward_md_direct_ad_v1`)
|
||||
- 首次发现 / 最近更新:2026-10-09 / 2026-10-16
|
||||
- 来源:`TASK-rashi-dashas-native-20261008`。
|
||||
- 影响面:`scripts/extended_dashas.py` 的 `calc_shoola_dasha` 与 `'shoola'` 注册项。接口层仍能按这个键调用。完整数据版报告不走这条。
|
||||
- 现象:旧函数从月亮星座起,年数从 9 减到 1,一年按 365.25636 日。所选 Rath 口径是上升或第 7 里较强的星座、一律顺行、每段 9 个日历年。
|
||||
- 触发条件:调用扩展大运注册表里的 `shoola`,或报告误用该函数。
|
||||
- 根因:早期简化实现留在注册表。本单不得改 `scripts/jyotish_api_server.py` 的方法体,所以注册项还在。
|
||||
- 修复:报告改走 `shoola_forward_md_direct_ad_v1`。注册表和接口方法体不动。
|
||||
- 修复:报告改走 `shoola_forward_md_direct_ad_v1`。2026-10-09 当时不改接口方法体。2026-10-16 issue #69 删除 `extended_dashas.py`,接口上的 shoola 改为直接复用同一段。
|
||||
- 验证:三个新模块的语法树不导入 `extended_dashas`。定向 pytest 退出码 0。
|
||||
- 防复发:上述导入断言。报告章不得再调用 `calc_shoola_dasha`。
|
||||
- 相关记录:BUG-1283。
|
||||
@@ -36303,4 +36303,82 @@
|
||||
- 复发自:BUG-933(源码合同没在同一轮跟上)
|
||||
- 修复版本:分支 `codex/beta-capacity-20261014`。
|
||||
|
||||
## BUG-1340 | 免费层限流状态报的和实际相反
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-10-13
|
||||
- 最近更新:2026-10-15
|
||||
- 影响面:VedAstro 免费层的诊断和审计字段 `queue_enabled` / `free_tier_queue_enabled`。限流等待和降级本身没改。
|
||||
- 用户现象:运维看诊断时,没配旧开关会显示「没开限流」,其实官方公共端点仍在限流;配了旧开关、又把配额设成 0,会显示「开了」,其实没有限流。
|
||||
- 触发条件:命中官方公共端点。旧变量 `VEDASTRO_FREE_TIER_QUEUE`、`VEDASTRO_FREE_TIER_QUEUE_ENABLED`、`VEDASTRO_ENABLE_FREE_TIER_QUEUE` 只被写进报告,不参与限流。
|
||||
- 根因:限流在适配器里只看端点、有没有 API key、以及配额窗口是否大于 0。三个旧名字是更早留下的报告开关,从未接到这套判断上。BUG-720 的防复发只写了「前台不得无上限排队」,没有要求报告字段和真实限流一致。
|
||||
- 修复:报告改为读同一套判断。三个旧名字保留,只在诊断里列为已废弃,不能用来关掉限流。
|
||||
- 验证:`tests/test_vedastro_free_tier_state.py`。没配旧开关时期望与 `free_tier_queue_active` 一致;旧开关设为开且配额为 0 时期望为关;有 key 或自建端点时期望为关。
|
||||
- 防复发:上述测试,以及源码扫描:除诊断脚本外,生产代码不再出现这三个变量名。不得把这三个名字做成真正的关闭开关。
|
||||
- 相关记录:BUG-720
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/python-hygiene-20261013`。本号是任务书预留的空号;本分支最大已用编号是 BUG-1357,1340 未被占用,因此不顺延。
|
||||
|
||||
## BUG-1341 | 大运接口用占位周期冒充计算结果
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-10-13
|
||||
- 最近更新:2026-10-16
|
||||
- 影响面:`POST /api/dasha` 里除 Vimshottari 以外的体系。排盘接口里的大运名单和数量不变。
|
||||
- 用户现象:选 Yogini、Panchottari 等大运时,接口返回的主星和日期不随星盘变化,年数是按总数平均编的,却标成已经算过。
|
||||
- 触发条件:请求一个注册表里有、但不是 Vimshottari 的大运键。
|
||||
- 根因:`extended_dashas.py` 和 `conditional_dashas.py` 是早期为了凑体系数量写的占位。完整解读后来改走独立模块,接口还留在占位函数上。
|
||||
- 修复:接口改为调用完整解读使用的同一批模块。没有真实现的体系返回「未实现」。占位文件删除。Narayana 仍标明旧算法,不换成另一套。shoola 复用报告里已有的那一段。
|
||||
- 验证:`tests/test_dasha_api_engine_parity.py` 9 通过。任务书列出的 pytest 共 247 通过、0 失败。`tests/run_all.py` 102/102。虚构盘排盘响应除缓存时间戳外逐字段相同,`dasha_count` 仍为 35。
|
||||
- 防复发:上述对照测试。响应里不再出现 `precision` 为 `generic` 或 `calculator`。
|
||||
- 相关记录:BUG-1262、BUG-1284、BUG-1215、BUG-1218、BUG-1344
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/dasha-unify-20261013`。本号是任务书预留号;本分支最大已用编号是 BUG-1357,1341 与 1344 都未被占用,因此不顺延。Narayana 旧算法与 Rath 版仍未裁定。
|
||||
|
||||
## BUG-1344 | Ashtottari 大运从出生日满额起算,整条时间线偏了
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-10-14
|
||||
- 最近更新:2026-10-16
|
||||
- 影响面:完整解读和接口里的 Ashtottari 时间线。适用规则(哪些盘适用、两种规则族怎么判定)没改。另一套从 Mars 起算的星宿大运没有用户界面在读,这次不动。
|
||||
- 用户现象:适用 Ashtottari 时,第一段大运从出生时刻按整段年数起算,后面每一段都一起偏出一段出生前已经走过的时间。
|
||||
- 触发条件:盘面已被判定适用,而且月亮落在起运星所辖的星宿组里。月亮经度没传、或不在该组内时,时间线仍从出生起算。
|
||||
- 根因:余额只在一种规则族里计算,而且算完没有用来移动第一段的起点。完整解读调用时也没有把月亮经度传进去。
|
||||
- 修复:两种规则族在适用时都按月亮在该组里已走过的比例,把第一段起点改到出生时刻减去已走过的部分。后续大运、小运和当前大运一起平移。完整解读只给这次调用补月亮经度,不改与 Yogini、Kalachakra 共用的那份出生信息。
|
||||
- 验证:虚构盘与两张公开盘,起止日与 PyJHora 4.8.7 相差不超过 1 天,起运星一致;出生时刻落在第一段内;当前大运与按日期查表一致。全量解读除 Ashtottari 段和因此改变的结果哈希外,其余路径 diff 为空。
|
||||
- 防复发:`tests/test_dasha_api_engine_parity.py` 里的 Ashtottari 对照。PyJHora 缺失时对照部分跳过,与引擎自身的当前大运查表仍保留。
|
||||
- 相关记录:BUG-1341
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/dasha-unify-20261013`。
|
||||
|
||||
## BUG-1358 | 年运逐字函数被改成共享年数表,历史清单仍要求已删除的占位文件
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-10-16
|
||||
- 最近更新:2026-10-16
|
||||
- 影响面:`scripts/tajika.py` 的 `calc_mudda_with_dasha_balance`;历史调用矩阵对样本路径是否存在的检查。大运接口改造和 Ashtottari 出生余额修正没有改回去。
|
||||
- 用户现象:验收时 Python 全量比基线多了两条失败。年运这段用的年数没有变,但「一个字都不能改」的合同被破坏。一份历史清单还列着两个已经删掉的占位文件,检查因此变红。
|
||||
- 触发条件:运行 `tests/test_tajika_mudda.py` 的逐字合同,以及 `tests/test_owner_case_purge_contract.py` 的调用矩阵路径检查。
|
||||
- 根因:把各处 Vimshottari 年数表收成一份时,连被逐字锁定的函数里的字面量也换成了共享表。删除占位文件时按规矩没改历史矩阵,但测试仍要求矩阵里的每个路径都存在。BUG-1216 的哈希测试能发现第一处,当时的自测清单没有跑到它。BUG-1341 的防复发没有包含这份矩阵的路径检查,所以第二处要到全量对比才红。
|
||||
- 修复:该函数里的年数表和顺序表恢复成基线字面量。年数字面量计数的白名单加上 `tajika.py`,其余文件仍只能命中共享表。矩阵测试改为断言这两个路径已不存在,并由 `scripts/dasha_registry.py` 与三个条件大运模块接替。矩阵 JSON 没改,两个旧文件没有加回来。同一文件里另一处年运函数仍引用共享表。
|
||||
- 验证:见 `docs/tasks/PROGRESS-dasha-unify-fix-20261016.md`。函数文本哈希与基线合同一致。
|
||||
- 防复发:`test_ported_function_is_verbatim` 与 `test_invocation_matrix_paths_counts_hash_and_boundaries`。
|
||||
- 相关记录:BUG-1216、BUG-1341
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/dasha-unify-20261013`。开工时本分支最大已用编号是 BUG-1357,1358 未被占用。
|
||||
|
||||
## BUG-1359 | 拆类金样把本机路径算进指纹,测试还会删掉共用排盘缓存
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-10-16
|
||||
- 最近更新:2026-10-16
|
||||
- 影响面:`tests/test_api_service_extract_golden.py` 与采集脚本 `scripts/research/api_service_extract_golden.py`。计算和网页入口的拆分本身没有改。
|
||||
- 用户现象:没有用户能看见的变化。验收时这条金样在另一台机器上对不齐;全量并行时另一条排盘上下文测试变红,单独跑是绿的。
|
||||
- 触发条件:在采集金样的那台机器以外运行这条金样测试;或全量 pytest 并行时,别的测试正在用仓库里的 `scratch/local/api_chart_cache`。
|
||||
- 根因:咨询结果里的清单路径是仓库根目录的绝对路径,规范化只去掉了两个墙钟字段。采集脚本又把这个共用缓存目录整个删掉。缓存有效期为 0 时,现有读函数直接返回空、写函数不落盘。BUG-1060 已经要求采集时把有效期设为 0,因为命中缓存会改变列表顺序;这次新脚本没有沿用,改成了删目录。
|
||||
- 修复:所有字符串值里的仓库根目录绝对路径(反斜杠、正斜杠,以及 Windows 扩展前缀)换成 `<ROOT>`,后面的分隔符统一成 `/`。不按字段名挑选。采集期间设置 `JYOTISH_API_CHART_CACHE_TTL_SECONDS=0`,结束恢复原值,不再删除缓存目录。在基线 `fbf04f0b5f31cd8421bc4546840b1c833aaed810` 与本分支各采集一次,8 个哈希相同,用这份结果覆盖金样。方法数常量旁被盖住的 BUG-1341 三栏已恢复,本单说明另起一行。
|
||||
- 验证:有效期为 0 时读取结果为空,缓存目录文件数采集前后都是 2。基线与本分支 8 个哈希逐个相同。金样、增长合同、以及上次并行变红的那一条一起跑,11 通过、0 失败。没有改那条在改代码之前就已经失败的校正测试的断言。
|
||||
- 防复发:金样测试仍要求 sha256 与键路径类型全等,没有改成只比键或字节数。采集脚本里不再删除仓库内缓存目录。
|
||||
- 相关记录:BUG-1060
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/api-server-service-extract-20261013`。修复单写明无 BUG 编号;本条是验收打回的测试失败。开工时本分支最大已用编号是 BUG-1358,1351、1352、1353 已有条目,本条用 BUG-1359。
|
||||
|
||||
@@ -29,8 +29,13 @@ VEDASTRO_RANGE_SCAN_NETWORK_ENABLED=0
|
||||
|
||||
VEDASTRO_CACHE_TTL_SECONDS=604800
|
||||
VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS=604800
|
||||
# 网关离线排队作业,和下面的免费层限流不是一回事。
|
||||
VEDASTRO_GATEWAY_QUEUE_ENABLED=1
|
||||
VEDASTRO_FREE_TIER_QUEUE=1
|
||||
# 免费层限流由有没有 API key,以及 MAX_REQUESTS / WINDOW_SECONDS 是否大于 0 决定。
|
||||
# VEDASTRO_FREE_TIER_QUEUE 已废弃,留着也不会开关限流。
|
||||
# VEDASTRO_FREE_TIER_MAX_REQUESTS=5
|
||||
# VEDASTRO_FREE_TIER_WINDOW_SECONDS=60
|
||||
# VEDASTRO_FREE_TIER_WAIT_BUDGET_SECONDS=
|
||||
VEDASTRO_FAIL_OPEN_LOCAL=1
|
||||
VEDASTRO_FULL_CATALOG_SAMPLE_LIMIT=0
|
||||
|
||||
@@ -78,7 +83,8 @@ npm run dev --prefix frontend
|
||||
|
||||
- 不要让浏览器直连 VedAstro,也不要把 `VEDASTRO_API_KEY` 放进前端。
|
||||
- `VEDASTRO_CACHE_TTL_SECONDS` 和 `VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS` 用于缓存官方或自建服务结果。
|
||||
- `VEDASTRO_GATEWAY_QUEUE_ENABLED=1` / `VEDASTRO_FREE_TIER_QUEUE=1` 用于把昂贵或被限流的外部请求排队。
|
||||
- `VEDASTRO_GATEWAY_QUEUE_ENABLED=1` 只打开网关自己的离线排队作业。
|
||||
- 免费层限流不看 `VEDASTRO_FREE_TIER_QUEUE`(这个名字已废弃,不控制行为)。没有 API key,并且 `VEDASTRO_FREE_TIER_MAX_REQUESTS` 与 `VEDASTRO_FREE_TIER_WINDOW_SECONDS` 都大于 0 时,官方公共端点会限流。等待上限是 `VEDASTRO_FREE_TIER_WAIT_BUDGET_SECONDS`。
|
||||
- 如果 VedAstro 官方或自建服务不可达,`VEDASTRO_FAIL_OPEN_LOCAL=1` 会保持本地 Jyotish 引擎继续输出,并在 Technique Audit Table 里降级标注。
|
||||
- Gateway 不会默认声称跑完 641 项;它只把 capability catalog、dynamic selection、cache/queue/fallback 状态作为证据边界交给 strict workflow。
|
||||
|
||||
@@ -187,7 +193,10 @@ VEDASTRO_ENABLE_NETWORK=1
|
||||
VEDASTRO_TIMEOUT_SECONDS=20
|
||||
VEDASTRO_CACHE_TTL_SECONDS=600
|
||||
VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS=600
|
||||
VEDASTRO_FREE_TIER_QUEUE=1
|
||||
# 免费层限流由 API key 与 MAX_REQUESTS / WINDOW_SECONDS 决定。
|
||||
# VEDASTRO_FREE_TIER_QUEUE 已废弃,不控制限流。
|
||||
# VEDASTRO_FREE_TIER_MAX_REQUESTS=5
|
||||
# VEDASTRO_FREE_TIER_WINDOW_SECONDS=60
|
||||
# 可选
|
||||
# VEDASTRO_API_KEY=sk_live_xxx
|
||||
```
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# PROGRESS · API 计算与 HTTP 拆类(2026-10-16)
|
||||
|
||||
- 分支:`codex/api-server-service-extract-20261013`
|
||||
- 任务书里的基线 `2994588ad4d858dead4835943e5883c5aa2b03f8` 已过期。本分支从当前 `origin/staging` `fbf04f0b5f31cd8421bc4546840b1c833aaed810` 新建,没有上游。该提交已包含 #69 与 #68。
|
||||
- 没有新 BUG 编号。结构改造,不写 `docs/BUG_HISTORY.md`,不写 `CHANGELOG.md`。
|
||||
|
||||
## 开工重测
|
||||
|
||||
| 指标 | 任务书写作时 | 开工实测 |
|
||||
| --- | --- | --- |
|
||||
| `scripts/jyotish_api_server.py` 换行数 | 10970 | **10906** |
|
||||
| 四空格 `def`(合同口径) | 218 | **217**(BUG-1341 已下调,本单保持) |
|
||||
| `JyotishAPIHandler` 类方法 | 215 | **214** |
|
||||
| `JyotishAPIHandler.__new__` 合计 | 31 | **31**(`scripts/` 2 + `tests/` 29,不含合同文件) |
|
||||
| 行数基线 / 上限 | 10924 / 11224 | 基线不动。搬后换行数 **10920**,低于基线 |
|
||||
|
||||
HTTP 侧按「直接碰 HTTP 成员或覆写 `BaseHTTPRequestHandler`,再向上收到调用闭包」重算,仍是 15 个方法,外加 `server_version`:
|
||||
|
||||
- 直接:`_json`、`_html`、`_send_cors_headers`、`_enforce_request_security`、`_job_access_token`、`do_OPTIONS`、`do_GET`、`do_POST`、`_read_json_body`
|
||||
- 调用者:`_error_json`、`_require_dynamic_rectification_token`、`_get_high_rigor_job`、`_get_chart_job`、`_compute_dynamic_rectification_opportunities`、`_compute_dynamic_rectification_score`
|
||||
|
||||
类里没有 `self.xxx =` 赋值。
|
||||
|
||||
## 做了什么
|
||||
|
||||
- 同一文件里把原类改成 `JyotishComputeService`(199 个计算方法),新增 `JyotishAPIHandler(BaseHTTPRequestHandler, JyotishComputeService)`,放入上面 15 个方法和 `server_version`。`ThreadingHTTPServer` 仍用 `JyotishAPIHandler`。
|
||||
- 方法体与基线逐个 `ast.dump` 相等:旧类 214 个方法,新文件 214 个方法,不一致 **0**。换类的成员是这 15 个方法加 `server_version`,共 16 个。
|
||||
- `scripts/consultation_workflow_service.py` 和 `scripts/capture_report_blocked_repairs_golden.py` 改为 `JyotishComputeService()`。`scripts/` 里 `JyotishAPIHandler.__new__` 为 **0**。
|
||||
- 只用计算方法的 17 个测试文件同样改了构造。`tests/test_active_rectification_api.py` 里 3 处补丁从 HTTP 类改到计算类,否则补丁打不中实例。断言没改。
|
||||
- 增长合同:总伪造 31 → **5**,生产伪造 2 → **0**,方法数 217 不变,行数基线 10924 不动。新增两条守卫:计算类不是 HTTP 处理器子类(并钉住 MRO 与 `server_version`);计算类方法体不碰 HTTP 成员、也不调用上述 15 个方法。
|
||||
- 搬前 golden 连跑两次哈希一致,落在 `tests/golden/api_server_service_extract_v1.json`(只存 sha256 和键路径类型)。采集脚本:`scripts/research/api_service_extract_golden.py`。搬后对照由 `tests/test_api_service_extract_golden.py` 重算,通过。
|
||||
|
||||
构造语句三栏(17 个测试文件和 2 个生产文件,每处相同):
|
||||
|
||||
| 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| `JyotishAPIHandler.__new__(JyotishAPIHandler)` | `JyotishComputeService()` | 计算类可以正常实例化,不必再绕过 HTTP 构造 |
|
||||
|
||||
`tests/test_active_rectification_api.py` 另有三处:
|
||||
|
||||
| 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 补丁打在 `JyotishAPIHandler` | 补丁打在 `JyotishComputeService` | 实例已是计算类,补丁必须打在实例的类上。断言未改 |
|
||||
|
||||
未改的 HTTP 侧伪造(5 处,合同新基线就是这 5 处):`test_api_server_security.py`、`test_api_async_job_contract.py`、`test_runtime_security_p0.py`、`test_dynamic_rectification_scoring.py`、`test_professional_report_reference_api.py`。其中安全测试文件 `git diff` 为空。
|
||||
|
||||
## 命令和结果
|
||||
|
||||
| 检查 | 命令 | 结果数字 |
|
||||
| --- | --- | --- |
|
||||
| 搬前 golden 连跑两次 | 基线上采集 MCP / HTTP / 报告裁剪 / 证据助手 | 8 个哈希两次相同 |
|
||||
| 搬后 golden | `pytest tests/test_api_service_extract_golden.py`(含在下面那条里) | 通过,与搬前哈希一致 |
|
||||
| 任务书点名测试 + 改过的测试 + 两个调用了采集入口的测试 | `pytest` 上述文件 `-q --tb=line` | **383 通过,1 失败**,约 1046 秒 |
|
||||
| 方法体对比 | 基线文件与工作树 `ast.dump` | 214 对 214,不一致 0 |
|
||||
| 反向:计算类临时写 `self.headers` | `test_compute_service_methods_do_not_touch_http` | 失败后已撤回 |
|
||||
| 反向:`scripts/` 临时加一处伪造标记 | `test_scripts_handler_forgeries_must_not_grow` | 1 > 0,失败后已撤回 |
|
||||
| 安全测试文件 | `git diff -- tests/test_api_server_security.py` | 空 |
|
||||
| 生产伪造 | 在 `scripts/**/*.py` 搜伪造标记 | **0** |
|
||||
| 换行数 | 文件内 `\n` 个数 | 10906 → **10920**(上限 11224) |
|
||||
| 隐私标记 | `pytest tests/test_repo_privacy_markers.py -q` | 退出码 0 |
|
||||
|
||||
搬前哈希(去掉两个墙钟字段后的规范 JSON):
|
||||
|
||||
| 案例 | sha256 | 字节 |
|
||||
| --- | --- | ---: |
|
||||
| mcp.career | `ad38a539cf9036143333acd538567b7eb94528d537c46ef326905ebc47527a05` | 543868 |
|
||||
| http.career | `6a6ca421d942e2577e587647324ef9521ea14841a74a6850c11f629df4d9ba96` | 420555 |
|
||||
| mcp.wealth | `9aaacf64d08f17bca2d3f6b020b1dbb1ec07eaee7727b04bba90430fcbf594d3` | 542629 |
|
||||
| http.wealth | `f22368bbde38b76dc44e2995c34f7809ca96442405c1f13e39f53c4a2dd90a62` | 419316 |
|
||||
| mcp.relationship | `be70fd603b1de52026ad40f0d6582a27cb8b148018474fcd7d8c4134509b40ae` | 537785 |
|
||||
| http.relationship | `4405ae821aef81ec03660994d9f8f4104ae3388f339407bfe2721d05d662059b` | 414472 |
|
||||
| repairs | `41d48a453fbd5d616de083fe07bb8d318c3f60ad2e127764237ef6ae50cc6175` | 74700 |
|
||||
| evidence | `27975ba40a9ee89fc19543a18cb2ee60fb2477b8a6e8f14004ad411adfc408a2` | 2092 |
|
||||
|
||||
MCP 与 HTTP 彼此不相等:MCP 走 `surface=skill_mcp`,HTTP 方法默认 `api_web`,并且 HTTP 方法还会多挂一层咨询全文。这是原来的两条路径,不是这次拆类造成的。每条路径搬前搬后各自一致。
|
||||
|
||||
## 那 1 条失败
|
||||
|
||||
`tests/test_active_rectification_api.py::test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow` 期望时间窗 `05:07–05:08`,实际得到 `07:00–07:02`。把该测试文件还原到基线后再跑,失败内容相同。不是拆类引入的,也不是「用到了 HTTP」。按任务书,这种红不回退该文件。断言没改。同文件其余测试在本轮通过。
|
||||
|
||||
## 偏离
|
||||
|
||||
- 任务书交付物写了「删两处死代码」。硬红线写明零引用函数只记进度、本单不删。`_strict_adjudication_bundle_from_contract` 与 `_high_rigor_job_path` 仍只在定义处出现,没有删。
|
||||
- 后续「把咨询闭包搬出文件、改依赖方向」没做。那一步要改安全测试的补丁落点,任务书明确不做。
|
||||
- `AGENTS.md` §6 没有写死伪造数字 31 或 2,所以没改措辞。
|
||||
- `skills/jyotish-vedic-astrology/scripts/` 里的同名副本没改。任务书点名的是 `scripts/`。合同计数也不含技能副本。
|
||||
- 另有若干 `object.__new__(JyotishAPIHandler)`,不在合同计数里,调用的是计算方法,经继承仍可用。本单没改。
|
||||
- 没跑:Python 快速门、全量 pytest、前端 `npm test`、`test:db`。没动数据库,也没对生产或 staging 做压测或登录。
|
||||
@@ -0,0 +1,53 @@
|
||||
# PROGRESS · API 拆类金样修复(2026-10-16)
|
||||
|
||||
- 分支:`codex/api-server-service-extract-20261013`
|
||||
- 修复单:`docs/tasks/TASK-api-server-service-extract-fix-20261016.md`(来自 `origin/staging` `187fa375`)
|
||||
- 被打回的提交:`c7350bcafbff64d3c526c2dbf3995c9845b3b66e`
|
||||
- 基线(金样 `fixtureProvenance.baselineSha`):`fbf04f0b5f31cd8421bc4546840b1c833aaed810`
|
||||
- 先把只含修复单的 `origin/staging` 合并进本分支(合并提交 `14ad076f`)。没有带进别的代码。
|
||||
|
||||
## 做了什么
|
||||
|
||||
- 拆类没有重做。没有改 `scripts/jyotish_api_server.py`,也没有改 `tests/test_api_server_security.py`。
|
||||
- 金样规范化对所有字符串值去掉仓库根目录绝对路径(反斜杠、正斜杠,以及 Windows 的 `\\?\` 前缀),换成 `<ROOT>`,其后的分隔符统一成 `/`。不按字段名挑选。
|
||||
- 采集不再删除 `scratch/local/api_chart_cache`。采集期间把 `JYOTISH_API_CHART_CACHE_TTL_SECONDS` 设为 `0`,结束恢复原值。有效期为 0 时,现有读函数直接返回空,写函数不落盘。
|
||||
- 在基线提交上用同一套规范化重新采集,再在本分支采集。8 个哈希与键路径类型逐个相同。金样文件按这份结果覆盖。
|
||||
- `JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE` 上方恢复原来的三行:
|
||||
- 原值:218(删占位回退之前)
|
||||
- 新值:217
|
||||
- 原因:BUG-1341 删除 `JyotishAPIHandler._fallback_dasha_periods`,类方法少 1 个,没有新增方法
|
||||
- 本单「数值未变」另起一行写在这三行后面,没有盖住历史。
|
||||
- 没有改断言。`check_golden()` 仍要求 sha256 和键路径类型全等,没有改成只比键或字节数。上一轮在改代码之前就失败的校正测试没有放宽。
|
||||
- 记了 BUG-1359。修复单写的是无编号;这次是验收打回的测试失败,按 `AGENTS.md` §5 记。开工时本分支最大已用编号是 BUG-1358。BUG-1351、1352、1353 已有条目,没有重用。
|
||||
|
||||
## 两次采集的 8 个哈希
|
||||
|
||||
基线与本分支打印的是同一组数。六条咨询路径都比上一份金样短 122 字节:上一份还含本机绝对路径,并且排盘缓存有效期字段是默认的 900 秒。这次两边都按「缓存不生效」采集,有效期字段是 0。`repairs` 与 `evidence` 的哈希和字节数与上一份相同。没有再跑「删除缓存目录」那条旧采集。
|
||||
|
||||
| 案例 | 基线 sha256 | 基线字节 | 本分支 sha256 | 本分支字节 |
|
||||
| --- | --- | ---: | --- | ---: |
|
||||
| evidence | `27975ba40a9ee89fc19543a18cb2ee60fb2477b8a6e8f14004ad411adfc408a2` | 2092 | `27975ba40a9ee89fc19543a18cb2ee60fb2477b8a6e8f14004ad411adfc408a2` | 2092 |
|
||||
| http.career | `81a0c8429fdec0c7d5ad2ac768c0d2a0d1361c46a299fb79cdfbfa46459492a8` | 420433 | `81a0c8429fdec0c7d5ad2ac768c0d2a0d1361c46a299fb79cdfbfa46459492a8` | 420433 |
|
||||
| http.relationship | `db4e04f76c016cd9e44263329b4a763fc11a1573ed1910414c369f5e3f696525` | 414350 | `db4e04f76c016cd9e44263329b4a763fc11a1573ed1910414c369f5e3f696525` | 414350 |
|
||||
| http.wealth | `d75e8036c05ff36b10e7c53dad4203d1c819f60abfe8d6473ab347810fe0a1ab` | 419194 | `d75e8036c05ff36b10e7c53dad4203d1c819f60abfe8d6473ab347810fe0a1ab` | 419194 |
|
||||
| mcp.career | `14aca344f395106c761a785e22f9cbda1282f79bd81d6e4d3bde9ffd9a839196` | 543746 | `14aca344f395106c761a785e22f9cbda1282f79bd81d6e4d3bde9ffd9a839196` | 543746 |
|
||||
| mcp.relationship | `625b532ce57966f2decbb8b67ee9f8b3a2809834026ae8ed4047a13f720b786e` | 537663 | `625b532ce57966f2decbb8b67ee9f8b3a2809834026ae8ed4047a13f720b786e` | 537663 |
|
||||
| mcp.wealth | `1ad98c778ed2360e782fc8e61a20ffc34dcc07698207a6801c756b09699f6220` | 542507 | `1ad98c778ed2360e782fc8e61a20ffc34dcc07698207a6801c756b09699f6220` | 542507 |
|
||||
| repairs | `41d48a453fbd5d616de083fe07bb8d318c3f60ad2e127764237ef6ae50cc6175` | 74700 | `41d48a453fbd5d616de083fe07bb8d318c3f60ad2e127764237ef6ae50cc6175` | 74700 |
|
||||
|
||||
## 命令和结果
|
||||
|
||||
| 检查 | 命令 | 结果数字 |
|
||||
| --- | --- | --- |
|
||||
| 有效期为 0 时不读不写 | 调用现有的缓存读、写函数 | 读取结果为空;目录里的文件 2 个,调用前后相同;写回的有效期字段是 0,未命中 |
|
||||
| 两次采集时缓存目录 | 基线采集、本分支采集各看一次目录 | 两次都是采集前 2 个文件、采集后 2 个文件,名单相同 |
|
||||
| 金样、增长合同、上次并行变红的那一条 | `.venv\Scripts\python.exe -m pytest tests/test_api_service_extract_golden.py tests/test_api_server_growth_contract.py tests/test_capture_commercial_astrology_e2e_contexts.py::test_capture_writes_runtime_contexts_without_required_layer_echo -q --tb=line` | **11 通过,0 失败**,退出码 0,约 132 秒。其中金样 1、增长合同 9、上下文那一条 1 |
|
||||
| 历史三栏是否还在删除行里 | `git diff origin/staging -- tests/test_api_server_growth_contract.py` | 原来的三行没有出现在删除行里 |
|
||||
| 全量 pytest、Python 快速门、前端 `npm test` | 没跑 | 修复单的全量对照由验收方复跑 |
|
||||
| `npm run test:db` | 没跑 | 没改数据库 |
|
||||
|
||||
## 偏离
|
||||
|
||||
- 修复单写「无 BUG 编号」。本轮按仓库规则补了 BUG-1359,原因见上。
|
||||
- 没有把「删除共用缓存」和「有效期为 0」再各采一次来拆开那 122 字节。删除共用目录会再次打到别的测试,修复单不允许。金样以「缓存不生效」为准。
|
||||
- 没跑全量 `pytest -n 4`,所以不能在这里声称失败清单已与基线逐名一致。
|
||||
@@ -0,0 +1,98 @@
|
||||
# PROGRESS · 契约与小工具去重 · 2026-10-13
|
||||
|
||||
分支 `codex/contracts-helpers-dedupe-20261013`,基线 `origin/staging` `cd7e0a34933bd288ad7a529d51477c7099e1f6b1`。只搬定义和引用。哈希字节、状态字符串、主题枚举值不变。没有改表、迁移、依赖、`page.tsx`、`scripts/jyotish_api_server.py`。没有写 BUG_HISTORY、CHANGELOG、DESIGN.md。`docs/agents/domain.md` 与 `CONTEXT.md` 里没有 EvidenceDomain,未加术语句。本分支 BUG 最大号 1357,本单没有新缺陷,不占新号。
|
||||
|
||||
## 做了什么
|
||||
|
||||
- T1:报告创建请求的 zod、默认主题、深度枚举收进 `frontend/src/lib/contracts/personal-report-request.ts`。服务端类型用 `z.infer`,客户端发送类型用 `z.input`。原导出名在 route-core、plan、contract 里 re-export。按钮删掉手写 interface。
|
||||
- T2:校正状态列表与入口类型收进 `frontend/src/lib/contracts/rectification-entry.ts`。线上形状的 status 仍是 `string`。服务端类型在此之上收窄。`case-status.ts` 再导出这些数组,import 无环。客户端解析器仍接受任意字符串 status。
|
||||
- T3:旧引导流程的 `EvidenceDomain` / `evidenceDomains` 改名为 `LegacyGuidedEvidenceDomain` / `legacyGuidedEvidenceDomains`(数组里的字符串没变)。出生时间证据的 `CandidateResult` 改名为 `LegacyCandidateResult`。`ChoiceKey` 只留在 `probe-explain.ts`(`choice-card.ts` 已经 import 它,反向会成环),choice-card 再导出。`ClaimStatus` 只留在 `personal-report-contract.ts`。v9 的 EvidenceDomain 与快照的 CandidateResult 没改名。
|
||||
- T4:同义的对象判断和有限数判断收进 `frontend/src/lib/guards.ts`。`sha256Hex` 收进 `frontend/src/lib/hash.ts`(仅服务端)。长文、兑换码、消息正文改为调用它。
|
||||
|
||||
## 替换清单(文件 → 共享函数)
|
||||
|
||||
调用处多数保留原来的局部名字,用 import 别名接到共享函数。原因见文末「偏离」。别名不是另一份实现。
|
||||
|
||||
| 共享函数 | 文件 |
|
||||
| --- | --- |
|
||||
| `isRecord` | `generate-personal-report-button.tsx`、`personal-report-page.tsx`、`account-deletion.ts`、`skill-package-registry.ts`、`report-public-projection.ts`、`compose-receipt.ts` |
|
||||
| `asRecord`(调用名本来就是 asRecord) | `varga-observations.ts`、`refinement-packet.ts`、`inference-adapter.ts`、`confirmation-gate.ts`、`ephemeris-view.ts`、`daily-starlanguage.ts`、`consultation-step-timing.ts` |
|
||||
| `asRecord`(别名 `recordOf`) | `subject-birth.ts`、`consultation-subject-resolver.ts`、`chart-subject-write.ts` |
|
||||
| `asRecord`(别名 `record`,非数组否则 null) | `birth-profile-timezone.ts`、`chart-view-mapper.ts`、`consultation-route-service.ts`、`general-daily-panchanga.ts`、`personal-report-longform-generate.ts`、`rectification-adopted-chart-tiers.ts`、`reply-quality-capture.ts`、`report-birth-binding.ts`、`v9/engine-client.ts`、`v9/segment-contract.ts`、`personal-report-route-core.ts`、`personal-report-worker.ts`、`personal-report-generation.ts` |
|
||||
| `recordOrEmpty`(别名 `record`,非数组否则 `{}`) | `consultation-workflow.ts`、`chart-birth-truth.ts`、`consultation-evidence-card.ts`、`consultation-judgment-table.ts`、`admin/rectification-telemetry-summary.ts`、`report-fact-table-values.ts`、`report-fact-tables.ts` |
|
||||
| `finiteNumberOrNull`(别名 `finiteNumber`) | `personal-report-route-core.ts`、`personal-report-worker.ts`、`personal-report-generation.ts`、`rectification-candidate-result.ts` |
|
||||
| `sha256Hex` | `personal-report-generation.ts` 再导出;`hashLongformMarkdown`、`hashRedeemCode`、`messageContentHash` 改为调用它 |
|
||||
|
||||
## 未合并及原因
|
||||
|
||||
| 留下的私有函数 | 原因 |
|
||||
| --- | --- |
|
||||
| `rectification-candidate-result.ts` 的 `function record` | 数组也当对象,否则 null。与 `asRecord` 不同义 |
|
||||
| `geoapify-location-service.ts` 的 `function record` | 数组也当对象,否则 `{}` |
|
||||
| `server-owned-birth-profile.ts` 的 `function finiteNumber` | 接受数字字符串,失败返回 undefined |
|
||||
| `chart-profiles/route.ts`、`chart-profiles/[id]/route.ts`、`synastry-reports/route.ts`、`admin/admin-error-response.ts` 的 `errorMessage` | 三种错误提取方式,任务书要求不并 |
|
||||
| `personal-report-service-core.ts` 的 `PERSONAL_REPORT_DEPTHS` | 名字和用途都不是 T1 的 `REPORT_DEPTHS`,没有硬合 |
|
||||
|
||||
任务书 §2.3 把「含数组否则 null」记在 `rectification-adopted-chart-tiers.ts`。当前 staging 上该函数已经排除数组,所以并入 `asRecord`。真正含数组否则 null 的那份在 `rectification-candidate-result.ts`,保留。
|
||||
|
||||
## 断言
|
||||
|
||||
| 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| `workflow.indexOf("function record(")` | `workflow.indexOf("function rectificationBoundaryFromGate(")` | 非数组否则空对象的私有 `record` 已删除。切片终点改到紧随其后的既有函数。锁住的 schema 字段没改,`chart: z.record(z.unknown())` 仍在 |
|
||||
|
||||
文件:`frontend/tests/consultation-workflow-contract.test.ts`。其余点名测试没有改断言。
|
||||
|
||||
## 仅 import
|
||||
|
||||
这些文件只改了 import 行,esbuild 转译结果与 staging 相同:
|
||||
|
||||
`rectification-choice-card.tsx`、`rectification-message-entry.tsx`、`use-rectification-case-snapshot.ts`、`answer-choice.ts`、`block-scan.ts`、`choice-action.ts`、`turn-intent-classifier.ts`、`turn-question.ts`、`window-widen.ts`、`rectification-chat-choice-run.ts`。
|
||||
|
||||
## esbuild 差分
|
||||
|
||||
对 `frontend/src` 里每个改动文件,用 esbuild `transform`(`.ts` / `.tsx`,`format=esm`,不打包)对比 `origin/staging` 与工作区。结果:相同 26,有差异 49,新文件 4,转译失败 0。
|
||||
|
||||
新文件:`contracts/personal-report-request.ts`、`contracts/rectification-entry.ts`、`guards.ts`、`hash.ts`。
|
||||
|
||||
有差异的原因,按文件:
|
||||
|
||||
| 文件 | 原因 |
|
||||
| --- | --- |
|
||||
| `generate-personal-report-button.tsx` | 手写请求类型删除,默认主题改为从 contracts 取值 |
|
||||
| `personal-report-page.tsx`、`account-deletion.ts`、`compose-receipt.ts`、`report-public-projection.ts`、`skill-package-registry.ts` | 本地 `isRecord` 删除,改为 import |
|
||||
| `consultation-step-timing.ts`、`daily-starlanguage.ts`、`ephemeris-view.ts`、`confirmation-gate.ts`、`inference-adapter.ts`、`refinement-packet.ts`、`varga-observations.ts` | 本地 `asRecord` 删除,改为 import |
|
||||
| `subject-birth.ts`、`consultation-subject-resolver.ts`、`chart-subject-write.ts` | 本地 `recordOf` 删除,import `asRecord` 并别名为 `recordOf` |
|
||||
| `birth-profile-timezone.ts`、`chart-view-mapper.ts`、`consultation-route-service.ts`、`general-daily-panchanga.ts`、`personal-report-longform-generate.ts`、`rectification-adopted-chart-tiers.ts`、`reply-quality-capture.ts`、`report-birth-binding.ts`、`engine-client.ts`、`segment-contract.ts` | 本地「非数组否则 null」的 `record` 删除,import `asRecord` 并别名为 `record` |
|
||||
| `consultation-workflow.ts`、`chart-birth-truth.ts`、`consultation-evidence-card.ts`、`consultation-judgment-table.ts`、`rectification-telemetry-summary.ts`、`report-fact-table-values.ts`、`report-fact-tables.ts` | 本地「非数组否则空对象」的 `record` 删除,import `recordOrEmpty` 并别名为 `record`。`z.record(...)` 没改 |
|
||||
| `rectification-candidate-result.ts` | 只把「有限数否则 null」改为 import。含数组的 `record` 仍在文件里 |
|
||||
| `personal-report-route-core.ts`、`personal-report-worker.ts` | 同上,再加上 schema / 调用处的 guard import |
|
||||
| `personal-report-generation.ts` | guard 搬家,并且 `sha256Hex` 改为从 `hash.ts` 再导出 |
|
||||
| `personal-report-longform-appendix.ts`、`supabase/codes.ts`、`message-origin.ts` | 哈希函数体改为调用 `sha256Hex`。输入仍按 utf8 |
|
||||
| `personal-report-plan.ts`、`personal-report-contract.ts` | `REPORT_DEPTHS` 字面量搬到 contracts,本文件 re-export |
|
||||
| `case-status.ts` | 状态数组搬到 contracts,本文件 re-export。数组内容相同 |
|
||||
| `rectification-entry.ts` | 本地状态列表删除,Set 改用共享常量 |
|
||||
| `birth-time-question-planner.ts`、`birth-time-journey-turn.ts`、`birth-time-journey-transitions.ts`、`birth-time-journey-turn-persistence.ts`、`birth-time-journey-case-loader.ts` | 运行时常量改名 `legacyGuidedEvidenceDomains`。不打包,所以导出名出现在转译结果里。数组里的字符串没变 |
|
||||
|
||||
相同的 26 个文件里,上面「仅 import」10 个只动了 import。其余是类型改名或类型再导出,类型擦除后转译结果与 staging 相同(含 `open-request.ts`、`choice-card.ts`、`report-evidence-bundle-v2.ts`、`birth-time-evidence.ts` 及其余 Legacy* 引用)。
|
||||
|
||||
## 测试
|
||||
|
||||
开工没有跑全量 `npm test`(本轮不做全量,也没有新的全量基线数字)。完工数字:
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 类型 | `frontend` 下 `tsc --noEmit -p .` | 0 错 |
|
||||
| 规范 | `frontend` 下 `npm run lint` | 0 错,123 条警告。警告是原来就有的。本轮删过 `personal-report-worker.ts` 里不再使用的 `JsonRecord`,没有留下新警告 |
|
||||
| 改到的测试与任务书点名的测试 | `node node_modules/tsx/dist/cli.mjs --test`(guards、报告入口、首页校正解析、校正契约、报告生成与长文哈希、咨询 workflow 契约、出生时间改名相关) | 209 通过,0 失败,0 取消,0 跳过 |
|
||||
|
||||
没有跑前端全量 `npm test`、Python 快速门、`test:db`。`supabase/codes.ts` 只改了哈希函数体,没有改查询、表或迁移。
|
||||
|
||||
## 偏离
|
||||
|
||||
1. 调用处没有改成共享函数名,改用 import 别名。把调用处全部改名时,已跟踪 diff 为 621 增、852 删(合计 1473),加上当时的新文件 248 行,约 1721 行,超过硬红线 1500。别名之后已跟踪 diff 为 229 增、461 删。共享函数仍只有 `guards.ts` / `hash.ts` 里的那一份。
|
||||
2. 报告按钮是客户端组件,但它要取值 `DEFAULT_REPORT_THEMES`,而这份常量和 zod schema 在同一个文件。因此这个按钮会把 zod 带进自己的包。任务书允许「zod 已在该组件 bundle 内」的例外;首屏 gzip 由验收侧看。测试仍从按钮 re-export 引用该常量。
|
||||
3. `LegacyCandidateResult` 的引用比任务书点名的文件多(向导、旅程加载等)。不改的话 `tsc` 会报错,所以一并改了类型名,没有改运行时值。
|
||||
4. 含数组变体的文件名与任务书 §2.3 不一致,以当前 staging 的函数体为准,见上表。
|
||||
|
||||
完工 diff:已跟踪 229 增 + 461 删,新文件与本记录 345 行,合计 1035,低于 1500。
|
||||
@@ -0,0 +1,77 @@
|
||||
# PROGRESS · dasha-unify · 2026-10-16
|
||||
|
||||
分支 `codex/dasha-unify-20261013`,从 `origin/staging` `b9cbe9aa` 拉出。issue #69。
|
||||
|
||||
## 结论
|
||||
|
||||
`/api/dasha` 除 Vimshottari 外改为调用完整解读同一批模块。没有真实现的体系返回「未实现」,不再编周期。占位文件 `scripts/extended_dashas.py`、`scripts/conditional_dashas.py` 已删除。Ashtottari 在已判定适用、且月亮落在起运星宿组内时,第一段从出生时刻减去已走过的部分起算。Narayana 仍标「旧算法」,没有换成 Rath 版。
|
||||
|
||||
## 映射(最终)
|
||||
|
||||
| 注册键 | 实现 |
|
||||
| --- | --- |
|
||||
| vimshottari | 原 handler 分支,`domain_calculation_service.compute_vimshottari_timeline` |
|
||||
| yogini / ashtottari / kalachakra | 与完整解读相同的函数和入参。Ashtottari 额外带月亮经度 |
|
||||
| panchottari、satabdika、chaturaaseeti、shasti_hayani、shodasottari、dwadashottari、dwisaptati、shattrimsa | 独立模块 `calculate_*`,入参与 `conditional_birth_info` 相同,并带 `PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE`。不传强制出表档,不适用的盘主运为空 |
|
||||
| narayana | `narayana_dasha_full_report` + `label_legacy_narayana` |
|
||||
| shoola | `rashi_dashas_native.calculate_rashi_dashas` 的 `shoola` 段(`shoola_forward_md_direct_ad_v1`) |
|
||||
| chara | 400,消息含 `/api/dasha/chara` |
|
||||
| 其余 | 400,`dasha system not implemented` |
|
||||
|
||||
`precision`:权威实现为 `engine_module`。删掉 `generic` 和 `calculator`。
|
||||
|
||||
## 基线红 / 修后绿
|
||||
|
||||
开工时测试还没写,没有在旧代码上跑新 pytest。占位输出用 `git show HEAD:scripts/extended_dashas.py` 复现(虚构盘、月宿 22):
|
||||
|
||||
| 体系 | 占位前 3 段 |
|
||||
| --- | --- |
|
||||
| panchottari | Ketu 1990-01-01、Venus 2002-01-01、Sun 2014-01-01 |
|
||||
| satabdika | Ketu 1990-01-01、Venus 2001-01-01、Sun 2012-01-02 |
|
||||
| chaturaaseeti | Ketu 1990-01-01、Venus 1999-01-01、Sun 2008-01-02 |
|
||||
| narayana | Pisces 3 年、Capricorn 6 年、Aquarius 9 年 |
|
||||
|
||||
修后 `tests/test_dasha_api_engine_parity.py`:9 通过,0 失败。接口主运与完整解读同一模块逐项相等。Panchottari / Satabdika / Chaturshitisama 在标准出表档下,主运起始日与 PyJHora 4.8.7 逐日相同。Ashtottari 三张盘(虚构盘走默认规则族,一张公开盘走 Rahu 规则族,另一张公开盘走默认规则族)起止日与 PyJHora 相差不超过 1 天,起运星一致,出生时刻落在第一段内,当前大运与按日期查表一致。
|
||||
|
||||
## 自测
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 任务书 pytest 加年长对齐 | `.venv\Scripts\python.exe -m pytest` 任务书第 8 节文件 + `tests/test_dasha_year_length_alignment.py` | 247 通过,0 失败,退出码 0 |
|
||||
| release 门 | `.venv\Scripts\python.exe tests\run_all.py` | 102/102,失败 0,0.2 秒 |
|
||||
| 排盘字节 | 清空 `scratch/local/api_chart_cache` 后 `_compute_chart_sync`,对照开工前基线 | 94348 字节。8 处差异全是语义缓存的 `cache_created_at` / `cache_expires_at`。`available_dashas` 与 `dasha_count` 35 未变 |
|
||||
| 全量解读字节 | 同一虚构盘、`PYTHONHASHSEED=0`、`VEDASTRO_ENABLE_NETWORK=0`、today 与 transit-date 2026-01-01 | 去掉墙钟字段后 3492 条差异:3490 条在 `modules.ashtottari_dasha`;另外 2 条是顶层 `result_hash` 与 `result_binding.result_hash`(同一哈希,因 Ashtottari 进入绑定载荷)。`input_hash` 未变。其余模块 diff 为空 |
|
||||
|
||||
类方法数基线:原值 218,新值 217,原因是删除 `_fallback_dasha_periods`,没有新增方法。行数基线仍是 10924(实测 10906,净减少,不下调基线)。
|
||||
|
||||
## T4 年数表
|
||||
|
||||
已改为引用 `scripts/dasha_common.py` 的 `VIMSHOTTARI_YEARS`(顺序 Ketu、Venus、Sun、Moon、Mars、Rahu、Jupiter、Saturn、Mercury)。替换前用 `HEAD` 字面量核对 `list(items)` 一致的文件:
|
||||
|
||||
- `scripts/jyotish_engine.py` 的 `DASHA_YEARS`
|
||||
- `scripts/dasha_calculator.py`、`scripts/dasha_calculator_enhanced.py` 的 `VIMSHOTTARI_PERIODS`
|
||||
- `scripts/vimshottari_subperiod_timeline.py` 的 `DASHA_YEARS`
|
||||
- `scripts/event_prediction_model.py` 的 `DASHA_YEARS`
|
||||
- `scripts/nakshatra_dasha.py` 的 Vimshottari 年数表(`calc_ashtottari_dasha` 未改)
|
||||
- `scripts/prashna.py` 的 `VIM_DURATIONS`
|
||||
- `scripts/validate.py` 的 `DASHA_YEARS`
|
||||
- `scripts/kp_system.py` 的年限列表 `[7, 20, 6, 10, 7, 18, 16, 19, 17]`,按原 `KP_LORDS` 顺序取共享表
|
||||
- `scripts/tajika.py` 两处函数内字面量
|
||||
|
||||
未换、也未改:冻结的 `scripts/dasha_analyzer.py`;`scripts/divisional_charts_extended.py` 根上没有这份年数表;`scripts/research/offline_research_20260926_lib.py` 属于研究脚本。六个条件大运的 `_birth_lord_and_balance` 改为一行调用。2000 个固定种子月亮经度与源码副本公式 `==`,0 差异。`shastihayani`、`dwadashottari` 的余额函数未动。
|
||||
|
||||
## 冻结文件
|
||||
|
||||
`sealed_holdout_rerun` 的 PRODUCTION / RESEARCH 与 `minute_rectification_holdout_v3.json` 的 `frozen_scoring.files` 均未出现在本次 diff 中。未改 `narayana_dasha.py`、`narayana_rath.py`、`narayana_legacy_label.py`、`domain_calculation_service.py`、`dasha_analyzer.py`。
|
||||
|
||||
## 历史证据未改
|
||||
|
||||
`references/oracle/full_technique_invocation_matrix_2026_07_22.json`、`references/oracle/rectification_missing_layer_integration_plan_2026_07_19.json` 仍写着已删除文件的路径。`references/cross_project_contract/sync_ledger.json` 只提到测试文件名。这些 JSON 不改。
|
||||
|
||||
## 偏离
|
||||
|
||||
- 入参构造放在 `scripts/dasha_registry.py`,没有从 `cmd_full_reading` 抽出共享函数,避免改动全量解读里 Yogini / Kalachakra 的出生信息。等价由对照测试锁住。
|
||||
- `scripts/orchestrator_bridge.py` 的 Ashtottari 调用已经从盘面带上月亮经度,这次没有改代码。
|
||||
- `nakshatra_dasha.calc_ashtottari_dasha`(从 Mars 起算)前端没有读取。全量解读字节对照里它没有变化。留到后续阶段,不在本单替换。
|
||||
- 新 pytest 没有在改代码之前跑红。红的证据是上面从 `HEAD` 复现的占位输出,加上任务书第 2.1 节的对照表。
|
||||
- PyJHora 4.8.7 只装在本工作树的虚拟环境里做对照,没有写入依赖清单。
|
||||
@@ -0,0 +1,42 @@
|
||||
# PROGRESS · dasha-unify 修复 · 2026-10-16
|
||||
|
||||
分支 `codex/dasha-unify-20261013`。issue #69 第 1 次打回。先把 `origin/staging`(`ef9ee62a`,只有这份修复单)合并进来,没有从 staging 另起分支,也没有改写已经推送的 `5ac88e58`。
|
||||
|
||||
## 做了什么
|
||||
|
||||
年运里那个规定一个字都不能改的函数 `calc_mudda_with_dasha_balance`,已经改回基线里的年数表字面量。函数文本哈希回到 `8569dd862cf575ac8a670115206c68cd79c5b51bf53113c07bd0e21b07f0560f`。同一文件里另一个函数 `calc_mudda_dasha` 仍引用共享年数表,那一处不在逐字合同里,没有改回去。
|
||||
|
||||
`test_ported_function_is_verbatim` 只锁定这一个函数。旁边五分力量那 8 个逐字函数的哈希仍与合同一致,本单没有改到。
|
||||
|
||||
历史调用清单 JSON 没有改。两个已删除的占位文件没有加回来。测试改为:这两个路径必须不存在,并且现行模块还在。
|
||||
|
||||
`test_historical_artifacts_are_byte_preserved_not_refreshed` 只管另一份封存归档的字节,不管这份清单,所以按修复单的办法 a 改测试,没有重新生成清单。
|
||||
|
||||
大运接口改造和 Ashtottari 出生余额修正没有重做,也没有改回去。
|
||||
|
||||
## 断言变更
|
||||
|
||||
| 位置 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| 年数字面量白名单 | 只有 `dasha_analyzer.py`、`divisional_charts_extended.py`。其余文件的命中必须等于 `["dasha_common.py"]` | 白名单加上 `tajika.py`。命中列表的期望仍是 `["dasha_common.py"]` | `calc_mudda_with_dasha_balance` 被逐字合同锁定,函数里的年数字面量不能换成共享表 |
|
||||
| 调用矩阵路径 | 清单里每个样本路径都必须是现存文件 | `scripts/extended_dashas.py` 必须不存在,由 `scripts/dasha_registry.py` 接替。`scripts/conditional_dashas.py` 必须不存在,由 `scripts/dwisaptatisama_dasha.py`、`scripts/shattrimshatsama_dasha.py`、`scripts/dwadashottari_dasha.py` 接替。其余路径仍必须存在 | BUG-1341 删除了占位文件。清单是历史归档,不能改 JSON,也不能把旧文件加回来 |
|
||||
|
||||
## 自测
|
||||
|
||||
工作树 `.venv\Scripts\python.exe -m pytest tests/test_tajika_mudda.py tests/test_owner_case_purge_contract.py tests/test_dasha_api_engine_parity.py -q --tb=no -ra`。
|
||||
|
||||
| 文件 | 收集 |
|
||||
| --- | --- |
|
||||
| `tests/test_tajika_mudda.py` | 170 |
|
||||
| `tests/test_owner_case_purge_contract.py` | 9 |
|
||||
| `tests/test_dasha_api_engine_parity.py` | 9 |
|
||||
|
||||
合计 188。结果:187 通过,1 跳过,0 失败,退出码 0。跳过的是 `tests/test_tajika_mudda.py` 第 180 行:本机没有上游仓库。哈希比对写在跳过之前,对不上仍会失败;独立算出的哈希与基线合同相同。上次变红的调用矩阵那一条在这 9 条里,已通过。
|
||||
|
||||
隐私标记:`.venv\Scripts\python.exe -m pytest tests/test_repo_privacy_markers.py -q`,62 通过,0 失败,退出码 0。
|
||||
|
||||
没跑前端全量 `npm test`,没跑 Python 快速门,没跑 `test:db`(没动数据库)。
|
||||
|
||||
## 偏离
|
||||
|
||||
无超出修复单的产品改动。本机缺少上游仓库,所以上游逐字比对这一步在本机记为跳过,不记为通过。
|
||||
@@ -0,0 +1,48 @@
|
||||
# PROGRESS · 报告渲染函数拆分第一阶段
|
||||
|
||||
- 日期:2026-10-15
|
||||
- 分支:`codex/pl9-render-split-20261013`
|
||||
- 基线:`origin/staging` `32b238cf8619cfab4b1107059ec997c974eb0d13`
|
||||
- 范围:只做任务书第一阶段。不改 BUG_HISTORY(开工最大号 BUG-1357,本单不占号)、CHANGELOG、前端、API 服务器。
|
||||
|
||||
## 做了什么
|
||||
|
||||
把 `render_pl9_markdown` 里不依赖外层数据的 34 个嵌套函数原样搬到 `scripts/report_render/`。函数体只把 `planet_label_cn` 改成模块常量 `PLANET_LABEL_CN`。`SIGNS` / `SIGNS_CN` 按原字面量抄到 `text.py`,避免新包顶层导入引擎。
|
||||
|
||||
| 模块 | 函数 |
|
||||
| --- | --- |
|
||||
| envelope.py | `_unwrap` `_envelope_list` `_envelope_dict` `_bool_text` `_source_dict` `_has_executed_payload` |
|
||||
| text.py | `_humanize_reader_token` `_humanize_reader_group` `_humanize_tajika_candidate` `_humanize_tajika_motion` `_display_rashi_from_value` `_humanize_reader_summary_line` `_humanize_annual_highlight_line` `_humanize_annual_narrative_highlight` `_humanize_domain_title` `_workbook_readable_label` `_workbook_readable_sentence` `_workbook_reading_paragraph` `_md_cell` `_render_compact_payload` `_sign_text` `_value_text` `_compose_narrative_paragraph` `_append_unique_text` `_planet_list_text` |
|
||||
| sections_misc.py | `_render_boundary_packet_summary_line` `_status_from_report_pack_section` `_section_evidence_boundary` `_domi_pl9_current_gap_appendix` `_chart_planets_from_positions` `_kp_house_focus_text` `_high_value_theme_authority_sentence` `_kp_conflict_adjudication_sentence` `_prashna_exact_cusp_boundary_sentence` |
|
||||
|
||||
## 排除
|
||||
|
||||
- `_degree_text`:函数内定义两次,本阶段不搬。
|
||||
- `_sign_degree_text`、`_longitude_text`:任务书点名与度数簇一起排除。
|
||||
- `_profile_aware_benchmark_appendix`:要读引擎里的 `_blocked_planned_section` 和 `ROOT_DIR`。新包不能顶层导入引擎,否则成环,也过不了干净进程导入测试。
|
||||
- 依赖外层数据、或调用上述未搬函数的嵌套函数:留给下一阶段。
|
||||
|
||||
## 前后
|
||||
|
||||
| 项 | 搬之前 | 搬之后 |
|
||||
| --- | --- | --- |
|
||||
| `render_pl9_markdown` 行数 | 6917 | 6506 |
|
||||
| 嵌套函数 | 148 | 114 |
|
||||
|
||||
行数少了 411,嵌套函数少了 34。没有改既有断言。
|
||||
|
||||
## 检查
|
||||
|
||||
| 检查 | 命令 | 结果数字 |
|
||||
| --- | --- | --- |
|
||||
| 未搬移,不屏蔽 | `report_render_split_diff.py` | 退出码 0,24 项 |
|
||||
| 未搬移,屏蔽 | 同上 `--mask-pyjhora` | 退出码 0,24 项 |
|
||||
| 搬移后,不屏蔽 | 同上 | 退出码 0,24 项 |
|
||||
| 搬移后,屏蔽 | 同上 `--mask-pyjhora` | 退出码 0,24 项 |
|
||||
| 指定 pytest + 隐私扫描 | `.venv\Scripts\python.exe -m pytest` 任务书列表与 `tests/test_repo_privacy_markers.py` | 退出码 0;236 收集,233 通过,3 跳过,0 失败 |
|
||||
|
||||
同一轮里,基线工作树和新代码渲染同一份 pickle,24 个 sha 全部相同。完整数据版中英 8 个 sha 在四轮里不变。屏蔽那轮曾有一次基线对基线不一致:夜间盘接口响应里年主重放一会儿是超时、一会儿是导入失败。这是屏蔽后的外部重放抖动,不是这次搬函数引入的。原样重跑后退出码 0。
|
||||
|
||||
## 偏离
|
||||
|
||||
任务书预期行数下降约 600 到 900。实际 411,因为上面三个排除项里有一份 142 行的附录。没有做第二阶段及以后。
|
||||
@@ -0,0 +1,95 @@
|
||||
# PROGRESS · 引擎卫生:限流状态、静默失败、功能吉凶兜底
|
||||
|
||||
- 日期:2026-10-15
|
||||
- 分支:`codex/python-hygiene-20261013`
|
||||
- 基线:`origin/staging` `e4ff62c73856d1e10d44f42f998e94835b8acdf0`
|
||||
- 前置:`dbba127e715759f0e225fc57faf12508a01d2380` 是该基线的祖先
|
||||
- BUG:BUG-1340。本分支最大已用编号是 BUG-1357,1340 空着,按任务书使用,不顺延。
|
||||
|
||||
## 做了什么
|
||||
|
||||
1. 免费层「是否在限流」改成读适配器里的真实判断:官方公共端点、没有 API key、配额窗口大于 0。三个旧变量名还在,只在诊断里标成已废弃,不能拿来关掉限流。网关离线排队仍用原来的另一组变量。
|
||||
2. 功能吉凶兜底收成 `scripts/functional_layer.py` 一处。引擎、接口、MCP 都转发到它。正常成功的结果与原来逐字节一致。`functional_benefics.py` 未改。
|
||||
3. 会让结果少一块的静默失败改为记下模块名、函数名和错误类型。预期内的失败保持安静,并写明原因。裸 `except` 里任务点名的两处引擎、星宿大运和星宿命令已改成 `except Exception`。
|
||||
4. 快速门加入静默失败计数和 `scripts/` 根目录 `.py` 数量两条只许下降的守卫。
|
||||
|
||||
## T3 分类
|
||||
|
||||
写作时口径是 29 处。本基线多了 `scripts/api_scratch_retention.py::_loop`(前序单新增),收尾 30 处。A 类 14 处不计入守卫;B 类 16 处改为记日志。未收窄 A 类的异常类型:不能证明只会抛某一种。
|
||||
|
||||
| 类 | 位置 | 处理 |
|
||||
| --- | --- | --- |
|
||||
| A | `annual_tajika_pack._iana_timezone_id` | 时区名解析失败保持安静,`# noqa: BLE001` |
|
||||
| A | `api_scratch_retention._loop` | 清理循环必须活着,保持安静 |
|
||||
| A | `jyotishganit_shadbala_surface_probe.clean` | 探针清理失败保持安静 |
|
||||
| A | `jyotish_engine.write_pl9_pdf` 字体循环 | 换下一个字体,最后仍可用 Helvetica |
|
||||
| A | `local_oss_observation_inventory.git_commit` | 读不到提交号保持安静 |
|
||||
| A | `muhurta` 导入 swisseph | Windows 上动态库失败不一定是 ImportError,保持安静 |
|
||||
| A | `nakshatra_dasha.calc_nakshatra_dasha_breakdown` | 日期文本解析失败不改变年龄明细;裸 `except` 已改为 `except Exception` |
|
||||
| A | `oracle_boundary_audit._date_delta_days` | 日期文本解析失败保持安静 |
|
||||
| A | `tajika._resolve_external_pyjhora_python` | 外部解释器探测失败保持安静 |
|
||||
| A | `vedastro_gateway._sdk_version` | 可选依赖版本读不到保持安静 |
|
||||
| A | `vedastro_python_bridge` 序列化兜底 | 对象转 JSON 失败再走原兜底 |
|
||||
| A | `yoga_engine` 自定义表达式 3 处 | 每条规则都可能失败,保持安静 |
|
||||
| B | `cmd_muhurta._get_sun_moon_lons` | 记下错误类型后仍走近似日月位置 |
|
||||
| B | `cmd_nakshatra_adv.cmd_nakshatra_dasha` | 过境位置失败记日志;裸 `except` 已改为 `except Exception` |
|
||||
| B | `daily_guidance_service._current_mahadasha` | 记日志,返回值不变 |
|
||||
| B | `jyotish_engine._conflict_arbitration` | 记日志;裸 `except` 已改为 `except Exception` |
|
||||
| B | `jyotish_engine.cmd_planetary_congregation` | 记日志 |
|
||||
| B | `jyotish_engine.cmd_full_reading` 过境位置 | 记日志;裸 `except` 已改为 `except Exception` |
|
||||
| B | `muhurta` 日出、儒略日、日月位置 3 处 | 记日志,返回值不变 |
|
||||
| B | `pl9_full_data_export` 中英 Sudarshana 各 1 处 | 记日志。报告正文不写「本段数据缺失」(需拍板) |
|
||||
| B | `reference_transparency_contract._case_chart` | 记日志 |
|
||||
| B | `solar_return._get_sun_lon_jd` | 记日志 |
|
||||
| B | `tajika._year_lord_is_daytime` | 记日志 |
|
||||
| B | `transit_trigger._get_transit_lon_precise` | 记日志 |
|
||||
| B | `mcp_server.strict_workflow` | 记日志 |
|
||||
|
||||
引擎里另外三处裸 `except` 会赋值(记忆统计、两处年龄推算),不在本口径,未改。
|
||||
|
||||
## 断言
|
||||
|
||||
| 位置 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| `tests/test_api_server_security.py` 高严谨官方摘要 `queue_enabled` | False | True | 没设置已废弃开关时,官方免费层默认仍在限流 |
|
||||
| `tests/test_vedastro_user_entrypoint.py` 两处 `free_tier_queue_enabled` / `queue_enabled` | True | True | 该用例没有 key、端点是官方地址、配额为正。字段跟随真实限流,不再跟随废弃开关 |
|
||||
|
||||
## 全量解读对照
|
||||
|
||||
同一虚构盘、同一随机种子、关闭网络。退出码 0,标准错误 20 字节。标准输出 5236527 字节,摘要哈希与开工前基线不同。逐项对比只有 29 处差异:缓存创建和过期时间、耗时、最慢阶段的名次、结果哈希、一份大运的参照时钟。`modules.functional_benefic_malefic` 排序后逐字节相同,长度 1442。
|
||||
|
||||
## 反向验证
|
||||
|
||||
人为在 `scripts/functional_layer.py` 加一处 `except Exception: pass`,并在根目录加一个空 `.py`,然后立刻删掉。
|
||||
|
||||
```
|
||||
FAILED tests/test_python_hygiene_budget.py::test_silent_except_budget_does_not_grow
|
||||
AssertionError: ['scripts/functional_layer.py:46']
|
||||
assert 1 <= 0
|
||||
FAILED tests/test_python_hygiene_budget.py::test_scripts_root_py_count_does_not_grow
|
||||
AssertionError: 447
|
||||
assert 447 <= 446
|
||||
```
|
||||
|
||||
撤回后根目录仍是 446。守卫测试 2 通过。
|
||||
|
||||
## 检查
|
||||
|
||||
| 检查 | 命令 | 结果数字 |
|
||||
| --- | --- | --- |
|
||||
| 定向测试 | 任务书 Grok 自测清单,外加功能层快照 3 条、改过的安全断言 1 条 | 77 项,76 通过,1 失败 |
|
||||
| 失败项 | `test_gateway_completion_archives_official_raw_response` | `st_mode & 511` 实得 438,期望 384 |
|
||||
| 静默守卫 | `tests/test_python_hygiene_budget.py` | 2 通过;未标记静默 0;带 noqa 的 A 类 14 |
|
||||
| 功能层快照 | `tests/test_functional_layer_snapshot.py` | 3 通过 |
|
||||
| 根目录 `.py` | `scripts/*.py` | 446(含新建的 `functional_layer.py`) |
|
||||
|
||||
失败的那条断言检查归档文件权限必须是 `0o600`。本单在 `vedastro_gateway.py` 只加了一行静默原因注释,`os.chmod(..., 0o600)` 未改。Windows 回报的权限位是 `0o666`。断言不放宽。
|
||||
|
||||
## 偏离
|
||||
|
||||
- T4 测试脚本归位未做。任务书让步顺序第一项,避免搬动被证据索引或冻结清单钉住的文件。
|
||||
- T5 第 2 条做了:根目录上限按收尾实测 446,只许下降。
|
||||
- A 类没有收窄异常类型。
|
||||
- 报告正文不增加「本段数据缺失」。
|
||||
- `skills/jyotish-vedic-astrology/scripts/` 里的旧排队开关解析未改。任务书文件清单不含它,也没有字节对照测试。
|
||||
- 不改前端、依赖、`.gitea/**`、生产配置、冻结文件。
|
||||
@@ -494,6 +494,7 @@
|
||||
| `TASK-pl9-render-split-20261013.md` | — | render_pl9_markdown 第一阶段:搬出无依赖内部函数 | 待领取(排队:api-chart-subprocess-gate) | `codex/pl9-render-split-20261013` |
|
||||
| `TASK-python-hygiene-20261013.md` | — | 免费层排队开关报反、静默吞异常、功能吉凶兜底合一、测试专用脚本归位(BUG-1340) | 待领取(排队:pl9-render-split) | `codex/python-hygiene-20261013` |
|
||||
| `TASK-dasha-unify-20261013.md` | — | /api/dasha 改走引擎同一套实现、删占位注册表、Ashtottari 出生余额(BUG-1341、1344) | 待领取(排队:python-hygiene) | `codex/dasha-unify-20261013` |
|
||||
| `TASK-dasha-unify-fix-20261016.md` | `PROGRESS-dasha-unify-fix-20261016.md` | **#69 第 1 次打回**:`5ac88e58` 主体通过;逐字移植的 `calc_mudda_with_dasha_balance` 被改;调用矩阵仍列已删文件 | 待验收 | `codex/dasha-unify-20261013` |
|
||||
| `TASK-api-server-service-extract-20261013.md` | — | API 单体第一阶段:拆出不带 HTTP 的计算类,生产伪造 handler 归零 | 待领取(排队:dasha-unify) | `codex/api-server-service-extract-20261013` |
|
||||
| `TASK-data-layer-types-20261013.md` | `PROGRESS-data-layer-types-20261013.md` | 数据访问层改用自己的 DataClient 类型第一阶段(BUG-1351;任务书预留的 1343 已被更大编号越过) | 待验收 | `codex/data-layer-types-20261013` |
|
||||
| `TASK-contracts-helpers-dedupe-20261013.md` | — | 前后端契约与小工具去重第一阶段 | 待领取(排队:frontend-dead-code、consult-route-split) | `codex/contracts-helpers-dedupe-20261013` |
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# TASK · API 单体拆类 · 修复单(issue #70,第 1 次打回)
|
||||
|
||||
- 日期:2026-10-16
|
||||
- 被验提交:`c7350bcafbff64d3c526c2dbf3995c9845b3b66e`(分支 `codex/api-server-service-extract-20261013`)
|
||||
- 原任务书:`docs/tasks/TASK-api-server-service-extract-20261013.md`
|
||||
- 执行方:Grok;在**同一分支**上追加提交(基于 `c7350bcaf`,不改写已推历史);若 `origin/staging` 已前进,用 `git merge origin/staging`,不要 rebase
|
||||
- BUG 编号:无(仍属结构改造)
|
||||
|
||||
## 验收结论(Claude,Linux)
|
||||
|
||||
拆类本身**通过**;新增的金样测试**未通过**,因此整单打回。
|
||||
|
||||
| 项 | 结论 | 证据 |
|
||||
| --- | --- | --- |
|
||||
| T2 纯移动 | 通过 | AST 对比:基线与本分支 215 个方法名集合相同、方法体逐个 `ast.dump` 相等;模块级函数与其他顶层语句全等;`JyotishComputeService` 200 个方法,`JyotishAPIHandler` 15 个 HTTP 方法 + `server_version` + docstring |
|
||||
| T3 生产伪造归零 | 通过 | `grep -rn "JyotishAPIHandler.__new__" scripts` 0 命中 |
|
||||
| T3 结果等价 | 通过(Claude 自证) | 基线工作树与本分支各跑 `capture_mcp('career')`(清缓存),逐字段比对只差 3 个 `manifest_path` 绝对路径(两个工作树目录名不同);基线连跑两次 0 差异;`repairs`、`evidence` 两路哈希两边相同 |
|
||||
| T4 测试伪造下调 | 通过 | 17 个文件只改构造语句与类型注解;伪造合计 31 → 5 |
|
||||
| T5 合同与守卫 | 通过(附一处文字要求,见 F3) | 两条新守卫在本机绿 |
|
||||
| `test_api_server_security.py` 零改动 | 通过 | `git diff --stat` 空 |
|
||||
| 本机 API 冒烟 | 通过 | 基线与本分支并排:`/api/health` 200、`/api/chart` 虚构盘 200(只差时间戳字段)、非 JSON 400、`text/plain` 415、3 MB 请求体 400、未知路径 404、OPTIONS 200、`Server: JyotishAPI/6.9.14` 头,响应体逐字相同 |
|
||||
| **T1 金样测试** | **未通过** | 见 F1、F2 |
|
||||
| 全量 pytest(`-n 4`)与基线逐名比对 | **未通过** | 本分支比基线多 2 条失败:`test_api_service_extract_golden.py::test_api_service_extract_golden_matches_baseline`(单独跑也红)、`test_capture_commercial_astrology_e2e_contexts.py::test_capture_writes_runtime_contexts_without_required_layer_echo`(单独跑绿,并行红,见 F2) |
|
||||
|
||||
## F1 · 金样哈希含本机绝对路径,换机器必红(必做)
|
||||
|
||||
**现象**:本机单独跑 `pytest tests/test_api_service_extract_golden.py`,7 条哈希全部不等,如 `http.career sha256 6a6ca4… != 0c8ef6… bytes 420555 != 441296`。
|
||||
|
||||
**根因**:咨询工作流响应里有 `external_parity_gate.manifest_path`、`real_case_calibration.holdout_replay_manifest.manifest_path`、`real_case_calibration.outcome_replay_manifest.manifest_path` 等字段,值是运行机器上的绝对路径。`canonical_text` 只去掉了两个墙钟字段,没处理路径,所以金样只在采集它的那台 Windows 机器、那个目录下成立。CI 和任何其他检出都会红。
|
||||
|
||||
**修法**:
|
||||
|
||||
1. 在 `scripts/research/api_service_extract_golden.py` 的规范化里,把所有字符串值中的仓库根目录绝对路径(`str(ROOT)`,以及 Windows 下的正斜杠 / 反斜杠两种写法)替换成固定占位符 `<ROOT>`,并把其后的路径分隔符统一成 `/`。不要按字段名白名单去删,避免以后新增路径字段又漏。
|
||||
2. 在**基线**(`origin/staging` 上本单开工时的 SHA,写进 `fixtureProvenance`)重新采集金样,覆盖 `tests/golden/api_server_service_extract_v1.json`。
|
||||
3. 在本分支上用 `check_golden()` 对照,全部相等。
|
||||
|
||||
**验收标准**:Claude 在 Linux 上、任意检出目录下单独跑这条测试为绿;Grok 在 Windows 上同样为绿。PROGRESS 里贴两次采集(基线、本分支)的 8 个哈希。
|
||||
|
||||
## F2 · 测试过程中删除共用排盘缓存目录,并行时打红别的测试(必做)
|
||||
|
||||
**现象**:全量 `pytest -n 4` 时 `test_capture_commercial_astrology_e2e_contexts.py::test_capture_writes_runtime_contexts_without_required_layer_echo` 变红;单独跑为绿,基线全量也为绿。
|
||||
|
||||
**根因**:`clear_chart_cache()` 对仓库内的 `scratch/local/api_chart_cache` 做 `shutil.rmtree`。全量测试多进程并行,其他测试正在读写同一个目录,被中途删掉。研究脚本手动跑时清缓存没问题,但放进 pytest 就成了共享状态破坏。
|
||||
|
||||
**修法**(二选一,推荐 a):
|
||||
|
||||
- a. 不删目录,改为在采集期间设置环境变量 `JYOTISH_API_CHART_CACHE_TTL_SECONDS=0` 让排盘缓存不生效(现有测试 `tests/test_commercial_domain_calculation_contract.py` 已用这个办法),采集结束恢复原值。先确认 TTL 为 0 时确实既不读也不写缓存;若仍会写入,则同时用 `monkeypatch` / 临时目录把 `_api_chart_cache_dir` 指向 `tmp_path`。
|
||||
- b. 测试里用 `monkeypatch.setattr(jyotish_api_server, "_api_chart_cache_dir", lambda: tmp_path / "cache")`,研究脚本的 `clear_chart_cache` 只清这个临时目录。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- 金样测试里不再出现对仓库内 `scratch/local/api_chart_cache` 的删除(`grep -n rmtree` 只允许作用于临时目录)。
|
||||
- 本分支全量 `pytest -n 4` 的失败清单与基线逐名一致(Claude 复跑)。
|
||||
- 金样结论不变(缓存不生效时与清空缓存时的输出相同;如有差异,在 PROGRESS 写明并以「缓存不生效」为准重采)。
|
||||
|
||||
## F3 · 方法数常量旁的旧三栏被改写(必做,小)
|
||||
|
||||
`tests/test_api_server_growth_contract.py` 里 `JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE` 上方原有的三栏(`原值:218(删占位回退之前)/ 新值:217 / 原因:BUG-1341 …`)被改成了 `原值:217 / 新值:217 / 原因:…`,丢了 BUG-1341 那次下调的记录。
|
||||
|
||||
**修法**:恢复原来三行原样;本单的说明(数值未变)如需保留,另起一行注释写在其后,不覆盖历史。
|
||||
|
||||
**验收标准**:`git diff origin/staging -- tests/test_api_server_growth_contract.py` 中,原有三栏三行不再出现在删除行里。
|
||||
|
||||
## 不要做
|
||||
|
||||
- 不改拆类本身(T2/T3/T4 已通过),不动 `scripts/jyotish_api_server.py`。
|
||||
- 不改 `tests/test_api_server_security.py`。
|
||||
- 不放宽金样比对(不许改成只比键路径或字节数)。
|
||||
|
||||
## 回报
|
||||
|
||||
在 issue #70 回报新 SHA,表格里写:金样单独跑结果(Windows)、全量 pytest 失败清单相对基线的差(若 Windows 能跑)、F3 的 diff 片段。
|
||||
@@ -0,0 +1,32 @@
|
||||
# TASK · 大运接口统一修复单(issue #69,第 1 次打回)
|
||||
|
||||
- 日期:2026-10-16
|
||||
- 原任务书:`docs/tasks/TASK-dasha-unify-20261013.md`
|
||||
- 被验 SHA:`5ac88e58f50b451f860482ea6de107c846780ae5`(`codex/dasha-unify-20261013`)
|
||||
- 执行方:Grok,同一分支续做(先 merge 最新 `origin/staging`,不改写已推历史)
|
||||
|
||||
## 1. 验收结论:主体通过,Python 全量新增 2 条真失败
|
||||
|
||||
已确认成立:冻结身份两份清单内的文件一个字节没改;Claude 独立用三张虚构盘对比新旧全量解读包,Ashtottari 之外只有随时间变化的引用号与仓库路径不同,无其它差异;Ashtottari 与 PyJHora 起止相差 ≤1 天(以 Grok 回报为准)。
|
||||
|
||||
| 新增失败(单独重跑仍失败;基线上通过) | 证据 |
|
||||
| --- | --- |
|
||||
| `tests/test_tajika_mudda.py::test_ported_function_is_verbatim[calc_mudda_with_dasha_balance]` | 函数源码哈希 `8569dd86…` → `8862bb1f…`。这条合同要求从上游移植的函数**逐字保持原样**;本单 T4 把 `scripts/tajika.py` 里这个函数内的 Vimshottari 年数字面量换成了常量引用 |
|
||||
| `tests/test_owner_case_purge_contract.py::test_invocation_matrix_paths_counts_hash_and_boundaries` | 断言 `references/oracle/full_technique_invocation_matrix_2026_07_22.json` 每行 `sample_files` 的路径都存在;本单删了 `scripts/extended_dashas.py`、`scripts/conditional_dashas.py`,矩阵里仍列着它们 |
|
||||
|
||||
## 2. 修法要求
|
||||
|
||||
1. `tajika.py::calc_mudda_with_dasha_balance`:**恢复成与基线逐字相同**(年数表字面量留在函数里)。原任务书 T4 已写「顺序或数值不同的不换」——逐字移植的函数同样不换;把它加进 T4 新测试「Vimshottari 年数字面量计数」的白名单,并在 PROGRESS 写明理由(`test_ported_function_is_verbatim` 锁定)。再 grep 一遍 `test_ported_function_is_verbatim` 覆盖的其它函数,确认本单没有改到。
|
||||
2. 调用矩阵:`references/oracle/full_technique_invocation_matrix_2026_07_22.json` 属于历史归档类文件,**不要直接改它的内容**。先看该测试与仓库里是否有既有的「归档引用已删除文件」处理办法(如 `test_historical_artifacts_are_byte_preserved_not_refreshed` 的约定);若没有,选其一并在 PROGRESS 写清理由:
|
||||
a. 让测试对矩阵里指向已删除的 `extended_dashas.py` / `conditional_dashas.py` 的条目改为断言「这些路径已按 BUG-1341 删除,且由 `<现行模块>` 接替」(三栏写明);
|
||||
b. 若归档文件允许更新,则用生成它的脚本重新生成,不得手改。
|
||||
**不得**为了让测试过而把那两个文件加回来。
|
||||
3. 合并最新 staging 后在本机跑:`python -m pytest tests/test_tajika_mudda.py tests/test_owner_case_purge_contract.py tests/test_dasha_api_engine_parity.py -q`。
|
||||
|
||||
## 3. 验收标准
|
||||
|
||||
Claude 全量 pytest 与 staging 基线逐条比对无新增;`tajika.py` 中 `calc_mudda_with_dasha_balance` 与基线 `git diff` 为空。
|
||||
|
||||
## 4. 让步顺序
|
||||
|
||||
无。第 1 次打回。
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import { lifeEventSchema } from "@/lib/birth-time-evidence";
|
||||
import type { EvidenceDraft } from "@/lib/birth-time-journey-turn";
|
||||
import type { EvidenceDatePrecision, EvidenceDomain } from "@/lib/birth-time-question-planner";
|
||||
import type { EvidenceDatePrecision, LegacyGuidedEvidenceDomain } from "@/lib/birth-time-question-planner";
|
||||
|
||||
const domainLabels = {
|
||||
education: "学业与学习环境",
|
||||
@@ -12,7 +12,7 @@ const domainLabels = {
|
||||
career: "工作与身份变化",
|
||||
finance: "收入、资产或资源变化",
|
||||
health_pressure: "健康或生活压力",
|
||||
} as const satisfies Readonly<Record<EvidenceDomain, string>>;
|
||||
} as const satisfies Readonly<Record<LegacyGuidedEvidenceDomain, string>>;
|
||||
|
||||
type DraftCardProps = {
|
||||
readonly draft: EvidenceDraft;
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { isRecord } from "@/lib/guards";
|
||||
import {
|
||||
DEFAULT_REPORT_THEMES,
|
||||
type PersonalReportCreateRequestInput,
|
||||
} from "@/lib/contracts/personal-report-request";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { readCurrentSubjectId } from "@/lib/current-subject";
|
||||
import { navigateAppPath } from "@/lib/app-navigation";
|
||||
@@ -24,16 +28,7 @@ import { notifyError } from "@/lib/notify";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export const DEFAULT_REPORT_THEMES = ["career", "marriage", "wealth", "timing", "health"] as const;
|
||||
|
||||
export interface PersonalReportCreateRequest {
|
||||
requestId: string;
|
||||
reportType: "personal_full";
|
||||
presentationMode: "default";
|
||||
depth: "standard";
|
||||
themes: readonly string[];
|
||||
chartProfileId?: string;
|
||||
}
|
||||
export { DEFAULT_REPORT_THEMES };
|
||||
|
||||
/**
|
||||
* uuid v4 via crypto.randomUUID (secure contexts). Returns null when the
|
||||
@@ -52,7 +47,11 @@ export function createReportRequestId(): string | null {
|
||||
/** Request shape for POST /api/reports. Never includes birth data or chat text. */
|
||||
export function buildPersonalReportCreateRequest(
|
||||
requestId: string,
|
||||
): PersonalReportCreateRequest {
|
||||
): PersonalReportCreateRequestInput & {
|
||||
reportType: "personal_full";
|
||||
presentationMode: "default";
|
||||
depth: "standard";
|
||||
} {
|
||||
const subjectId = readCurrentSubjectId();
|
||||
return {
|
||||
requestId,
|
||||
@@ -68,10 +67,6 @@ export type CreateReportOutcome =
|
||||
| { kind: "navigate"; reportId: string }
|
||||
| { kind: "hint"; message: string };
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function readReportId(json: unknown): string | null {
|
||||
if (!isRecord(json) || !isRecord(json.report)) {
|
||||
return null;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { isRecord } from "@/lib/guards";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -195,10 +196,6 @@ export function classifyReportEnvelope(statusCode: number, json: unknown): Repor
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function readSubjectDisplayName(json: unknown): string | undefined {
|
||||
if (!isRecord(json) || typeof json.subjectDisplayName !== "string") return undefined;
|
||||
const trimmed = json.subjectDisplayName.trim();
|
||||
|
||||
@@ -4,9 +4,9 @@ import { useState } from "react";
|
||||
import { InlineSpinner } from "@/components/inline-spinner";
|
||||
import {
|
||||
CHOICE_STOP_LABEL,
|
||||
type ChoiceKey,
|
||||
type RectificationChoiceCard as ChoiceCard,
|
||||
} from "@/lib/rectification-agentic/v9/choice-card";
|
||||
import type { ChoiceKey } from "@/lib/rectification-agentic/v9/choice-card";
|
||||
|
||||
type RectificationChoiceCardProps = Readonly<{
|
||||
card: ChoiceCard;
|
||||
|
||||
@@ -22,9 +22,9 @@ import {
|
||||
CHOICE_SKIP_QUESTION_MESSAGE,
|
||||
CHOICE_STOP_LABEL,
|
||||
CHOICE_STOP_MESSAGE,
|
||||
type ChoiceKey,
|
||||
type RectificationChoiceCard as ChoiceCardModel,
|
||||
} from "@/lib/rectification-agentic/v9/choice-card";
|
||||
import type { ChoiceKey } from "@/lib/rectification-agentic/v9/choice-card";
|
||||
import {
|
||||
questionIsAnswered,
|
||||
questionIsDeadUnanswered,
|
||||
|
||||
@@ -22,9 +22,9 @@ import {
|
||||
caseSnapshotState,
|
||||
currentQuestionFromSnapshot,
|
||||
questionSourceFromSnapshot,
|
||||
type CandidateResult,
|
||||
type CurrentQuestionModel,
|
||||
} from "@/lib/rectification-chat-snapshot";
|
||||
import type { CandidateResult } from "@/lib/rectification-chat-snapshot";
|
||||
|
||||
/**
|
||||
* Case snapshot sync for the rectification chat surface: the state that the
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isRecord } from "@/lib/guards";
|
||||
/**
|
||||
* Self-service account deletion (2026-09-30). Shared by the settings entry,
|
||||
* the frozen-account screen, the API route and the purge worker. Pure: no
|
||||
@@ -52,10 +53,6 @@ export type AccountDeletionStatus =
|
||||
| Readonly<{ status: "none" }>
|
||||
| Readonly<{ status: "pending"; requestedAt: string | null; scheduledFor: string }>;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isoOrNull(value: unknown): string | null {
|
||||
if (typeof value !== "string" && !(value instanceof Date)) return null;
|
||||
const date = new Date(value);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { recordOrEmpty as record } from "@/lib/guards";
|
||||
/**
|
||||
* Admin view of the anonymous rectification telemetry
|
||||
* (TASK-rectification-telemetry-20260926, D3).
|
||||
@@ -113,12 +114,6 @@ export const TELEMETRY_GAP_BUCKET_LABEL: Readonly<Record<string, string>> = {
|
||||
|
||||
export const TELEMETRY_PERCENT_SHOWN_BUCKETS: readonly string[] = ["5-9", "10+"];
|
||||
|
||||
function record(value: unknown): Readonly<Record<string, unknown>> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: {};
|
||||
}
|
||||
|
||||
function numberOrNull(value: unknown): number | null {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) return Number(value);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asRecord as record } from "@/lib/guards";
|
||||
type RecordValue = Record<string, unknown>;
|
||||
|
||||
export class BirthProfileTimezoneError extends Error {
|
||||
@@ -7,12 +8,6 @@ export class BirthProfileTimezoneError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function record(value: unknown): RecordValue | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as RecordValue
|
||||
: null;
|
||||
}
|
||||
|
||||
function valueFor(profile: RecordValue, snakeKey: string, camelKey: string): unknown {
|
||||
return profile[snakeKey] ?? profile[camelKey];
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
timeRangeSchema,
|
||||
validateOptionSet,
|
||||
} from "./birth-time-dynamic-choice.ts";
|
||||
import type { CandidateResult } from "./birth-time-evidence.ts";
|
||||
import type { LegacyCandidateResult } from "./birth-time-evidence.ts";
|
||||
import type { DynamicActionReceipt } from "./birth-time-dynamic-action-receipt.ts";
|
||||
import type { PublicChoiceKind, PublicDynamicChoiceQuestion, TimeRange } from "./birth-time-dynamic-choice.ts";
|
||||
|
||||
@@ -99,7 +99,7 @@ export type ServerChoiceEvidence = {
|
||||
};
|
||||
|
||||
export type DynamicChoiceScoringResult = {
|
||||
readonly candidate: CandidateResult;
|
||||
readonly candidate: LegacyCandidateResult;
|
||||
readonly evidenceMode: "dynamic_choice";
|
||||
readonly effectiveAnswerCount: number;
|
||||
readonly dimensionCount: number;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { parseJourneyResponse } from "./birth-time-journey-client.ts";
|
||||
import type { DynamicJourneyClientResponse } from "./birth-time-journey-response-schema.ts";
|
||||
import type { DynamicNextAction } from "./birth-time-journey-turn-protocol.ts";
|
||||
import type { CandidateResult } from "./birth-time-evidence.ts";
|
||||
import type { LegacyCandidateResult } from "./birth-time-evidence.ts";
|
||||
|
||||
const caseId = "7299894c-10a8-4b45-91d1-339007282c50";
|
||||
const resultId = "345087cc-7e7f-4b37-90e5-f0c0a6e5b7a7";
|
||||
@@ -52,7 +52,7 @@ const candidate = {
|
||||
reasons: [],
|
||||
evidence: [],
|
||||
algorithmVersion: "birth-time-dynamic-scoring-v2",
|
||||
} satisfies CandidateResult;
|
||||
} satisfies LegacyCandidateResult;
|
||||
|
||||
const highCandidate = {
|
||||
...candidate,
|
||||
@@ -62,7 +62,7 @@ const highCandidate = {
|
||||
eventCount: 4,
|
||||
domainCount: 3,
|
||||
marginPercent: 24,
|
||||
} satisfies CandidateResult;
|
||||
} satisfies LegacyCandidateResult;
|
||||
|
||||
const terminalLowCandidate = {
|
||||
...candidate,
|
||||
@@ -75,7 +75,7 @@ const terminalLowCandidate = {
|
||||
},
|
||||
eventCount: 4,
|
||||
domainCount: 3,
|
||||
} satisfies CandidateResult;
|
||||
} satisfies LegacyCandidateResult;
|
||||
|
||||
function response(input: {
|
||||
readonly nextAction: DynamicNextAction;
|
||||
@@ -83,7 +83,7 @@ function response(input: {
|
||||
readonly turnVersion?: number;
|
||||
readonly answeredCount?: number;
|
||||
readonly effectiveAnswerCount?: number;
|
||||
readonly candidateResult?: CandidateResult | null;
|
||||
readonly candidateResult?: LegacyCandidateResult | null;
|
||||
readonly snapshotState?: "rectifying" | "candidate" | "confirming" | "ready";
|
||||
readonly activeTime?: string | null;
|
||||
}): DynamicJourneyClientResponse {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { CandidateResult } from "./birth-time-evidence.ts";
|
||||
import type { LegacyCandidateResult } from "./birth-time-evidence.ts";
|
||||
|
||||
export type DynamicStopInput = {
|
||||
readonly result: CandidateResult | null;
|
||||
readonly result: LegacyCandidateResult | null;
|
||||
readonly effectiveAnswer: boolean;
|
||||
readonly previousResult: CandidateResult | null;
|
||||
readonly previousResult: LegacyCandidateResult | null;
|
||||
readonly priorPlateauCount: number;
|
||||
readonly usefulOpportunityCount: number;
|
||||
readonly repeatedOnly: boolean;
|
||||
@@ -25,8 +25,8 @@ export type DynamicStopDecision =
|
||||
| { readonly kind: "continue"; readonly plateauCount: number };
|
||||
|
||||
export function materiallyChanged(
|
||||
previousResult: CandidateResult | null,
|
||||
result: CandidateResult,
|
||||
previousResult: LegacyCandidateResult | null,
|
||||
result: LegacyCandidateResult,
|
||||
): boolean {
|
||||
if (previousResult === null) return true;
|
||||
const previousRange = previousResult.winningSegment;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { withCandidateResult } from "./birth-time-evidence.ts";
|
||||
import type { CandidateResult } from "./birth-time-evidence.ts";
|
||||
import type { LegacyCandidateResult } from "./birth-time-evidence.ts";
|
||||
import { decideDynamicStop } from "./birth-time-dynamic-stop-policy.ts";
|
||||
import { toPublicDynamicChoiceQuestion } from "./birth-time-dynamic-choice-internal.ts";
|
||||
import type {
|
||||
@@ -152,7 +152,7 @@ export function answerTransition(input: {
|
||||
|
||||
export function completeDynamicScoreTransition(input: {
|
||||
readonly stored: DynamicStoredRectificationCase;
|
||||
readonly candidate: CandidateResult;
|
||||
readonly candidate: LegacyCandidateResult;
|
||||
readonly usefulOpportunityCount: number;
|
||||
readonly repeatedOnly: boolean;
|
||||
readonly nextVersion: number;
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
withCandidateResult,
|
||||
withConfirmedCandidate,
|
||||
} from "./birth-time-evidence.ts";
|
||||
import type { CandidateResult, LifeEvent } from "./birth-time-evidence.ts";
|
||||
import type { LegacyCandidateResult, LifeEvent } from "./birth-time-evidence.ts";
|
||||
import type {
|
||||
BirthTimeJourneyPorts,
|
||||
JourneyResponse,
|
||||
@@ -61,7 +61,7 @@ export function assertLegacyJourneyMutation(
|
||||
function response(
|
||||
stored: StoredRectificationCase,
|
||||
lifeEvents: readonly LifeEvent[],
|
||||
candidateResult: CandidateResult | null,
|
||||
candidateResult: LegacyCandidateResult | null,
|
||||
): JourneyResponse {
|
||||
return {
|
||||
caseId: stored.id,
|
||||
|
||||
@@ -174,11 +174,11 @@ export const candidateResultSchema = z.object({
|
||||
}
|
||||
});
|
||||
|
||||
export type CandidateResult = z.infer<typeof candidateResultSchema>;
|
||||
export type LegacyCandidateResult = z.infer<typeof candidateResultSchema>;
|
||||
|
||||
function highCandidateMeetsSafetyGates(value: {
|
||||
readonly confidence: CandidateResult["confidence"];
|
||||
readonly winningSegment: CandidateResult["winningSegment"];
|
||||
readonly confidence: LegacyCandidateResult["confidence"];
|
||||
readonly winningSegment: LegacyCandidateResult["winningSegment"];
|
||||
readonly eventCount: number;
|
||||
readonly domainCount: number;
|
||||
readonly marginPercent: number;
|
||||
@@ -197,7 +197,7 @@ export class CandidateConfirmationError extends Error {
|
||||
|
||||
export function withCandidateResult(
|
||||
snapshot: JourneySnapshot,
|
||||
result: CandidateResult,
|
||||
result: LegacyCandidateResult,
|
||||
): JourneySnapshot {
|
||||
switch (result.confidence) {
|
||||
case "low":
|
||||
@@ -246,7 +246,7 @@ export function withCandidateResult(
|
||||
|
||||
export function withConfirmedCandidate(
|
||||
snapshot: JourneySnapshot,
|
||||
result: CandidateResult,
|
||||
result: LegacyCandidateResult,
|
||||
confirmedTime: string,
|
||||
): JourneySnapshot {
|
||||
if (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
import { lifeEventSchema } from "./birth-time-evidence.ts";
|
||||
import type {
|
||||
EvidenceDatePrecision,
|
||||
EvidenceDomain,
|
||||
LegacyGuidedEvidenceDomain,
|
||||
QuestionSpec,
|
||||
} from "./birth-time-question-planner.ts";
|
||||
|
||||
@@ -71,7 +71,7 @@ export const guideDraftEnvelopeSchema = z.object({
|
||||
export type GuideQuestionResponse = z.infer<typeof guideQuestionResponseSchema>;
|
||||
|
||||
export type ParsedEvidenceDraft = {
|
||||
readonly domain: EvidenceDomain;
|
||||
readonly domain: LegacyGuidedEvidenceDomain;
|
||||
readonly precision: EvidenceDatePrecision | null;
|
||||
readonly date: string | null;
|
||||
readonly needsReview: boolean;
|
||||
@@ -100,7 +100,7 @@ const subjectByDomain = {
|
||||
career: "一次明显的工作、职业方向或身份变化",
|
||||
finance: "一次收入、资产、负债或资源渠道的明显变化",
|
||||
health_pressure: "一次持续的健康压力或生活压力变化",
|
||||
} as const satisfies Readonly<Record<EvidenceDomain, string>>;
|
||||
} as const satisfies Readonly<Record<LegacyGuidedEvidenceDomain, string>>;
|
||||
|
||||
function precisionQuestion(question: QuestionSpec): string {
|
||||
const requested = new Set(question.requestedPrecision);
|
||||
@@ -187,7 +187,7 @@ function dateIsGrounded(date: string, sourceMessage: string): boolean {
|
||||
export function parseEvidenceDraftOutput(
|
||||
value: unknown,
|
||||
context: {
|
||||
readonly requiredDomain: EvidenceDomain;
|
||||
readonly requiredDomain: LegacyGuidedEvidenceDomain;
|
||||
readonly sourceMessage?: string;
|
||||
},
|
||||
): ParsedEvidenceDraft {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
candidateResultSchema,
|
||||
type BirthTimeAssessment,
|
||||
} from "./birth-time-journey.ts";
|
||||
import type { CandidateResult } from "./birth-time-evidence.ts";
|
||||
import type { LegacyCandidateResult } from "./birth-time-evidence.ts";
|
||||
import type {
|
||||
RectificationAnswer,
|
||||
RectificationQuestion,
|
||||
@@ -260,11 +260,11 @@ export function parseRectificationAnswer(value: unknown): RectificationAnswer {
|
||||
return z.enum(["A", "B", "C", "D"]).parse(value);
|
||||
}
|
||||
|
||||
export function parseCandidateResult(value: unknown): CandidateResult {
|
||||
export function parseCandidateResult(value: unknown): LegacyCandidateResult {
|
||||
return adaptCandidateResult(candidateResultApiSchema.parse(value));
|
||||
}
|
||||
|
||||
function adaptCandidateResult(parsed: z.infer<typeof candidateResultApiSchema>): CandidateResult {
|
||||
function adaptCandidateResult(parsed: z.infer<typeof candidateResultApiSchema>): LegacyCandidateResult {
|
||||
return candidateResultSchema.parse({
|
||||
resultId: parsed.result_id,
|
||||
confidence: parsed.confidence,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
lifeEventSchema,
|
||||
} from "./birth-time-journey.ts";
|
||||
import type { StoredRectificationCase } from "./birth-time-journey-service.ts";
|
||||
import { evidenceDomains } from "./birth-time-question-planner.ts";
|
||||
import { legacyGuidedEvidenceDomains } from "./birth-time-question-planner.ts";
|
||||
import {
|
||||
dynamicJourneyTurnStateSchema,
|
||||
evidenceDraftSchema,
|
||||
@@ -73,7 +73,7 @@ const storedCaseSchema = z.object({
|
||||
evidence_draft: z.unknown().nullable().default(null),
|
||||
processed_action_ids: z.array(z.string().uuid()).default([]),
|
||||
adaptive_round: z.number().int().min(0).max(3).default(0),
|
||||
asked_domains: z.array(z.enum(evidenceDomains)).default([]),
|
||||
asked_domains: z.array(z.enum(legacyGuidedEvidenceDomains)).default([]),
|
||||
});
|
||||
const eventLocationSchema = z.object({
|
||||
latitude: z.number(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
withCandidateResult,
|
||||
type CandidateResult,
|
||||
type LegacyCandidateResult,
|
||||
} from "./birth-time-evidence.ts";
|
||||
import { terminalJourneySnapshot } from "./birth-time-journey-response.ts";
|
||||
import {
|
||||
@@ -11,13 +11,13 @@ import type { LegacyStoredRectificationCase as StoredRectificationCase } from ".
|
||||
|
||||
type CompleteScoreTransitionInput = {
|
||||
readonly stored: StoredRectificationCase;
|
||||
readonly candidateResult: CandidateResult;
|
||||
readonly candidateResult: LegacyCandidateResult;
|
||||
readonly nextVersion: number;
|
||||
};
|
||||
|
||||
function completedTurn(
|
||||
stored: StoredRectificationCase,
|
||||
candidateResult: CandidateResult,
|
||||
candidateResult: LegacyCandidateResult,
|
||||
nextVersion: number,
|
||||
): JourneyTurnState {
|
||||
return projectJourneyTurn({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { assessBirthTime, withRectificationScoring, type BirthTimeAssessment, type JourneySnapshot, type RectificationScoring } from "./birth-time-journey.ts";
|
||||
import { createJourneyTurnActions } from "./birth-time-journey-actions.ts";
|
||||
import { assertLegacyJourneyMutation, createBirthTimeEvidenceActions } from "./birth-time-evidence-service.ts";
|
||||
import type { CandidateResult, LifeEvent } from "./birth-time-evidence.ts";
|
||||
import type { LegacyCandidateResult, LifeEvent } from "./birth-time-evidence.ts";
|
||||
import type { CandidateVargaSample } from "./birth-time-question-planner.ts";
|
||||
import { projectJourneyResponse, storedDynamicJourneyResponse, storedJourneyResponse } from "./birth-time-journey-response.ts";
|
||||
import type { JourneyTurnState } from "./birth-time-journey-turn.ts";
|
||||
@@ -97,7 +97,7 @@ export type DynamicChoiceScoreInput = Pick<DifferencePacketInput, "birthDate" |
|
||||
export interface BirthTimeJourneyEngine {
|
||||
scan(input: JourneyScanInput): Promise<{ readonly questionnaire: RectificationQuestionnaire }>;
|
||||
score(input: JourneyScoreInput): Promise<RectificationScoringResult>;
|
||||
scoreEvents(input: JourneyEventScoreInput): Promise<CandidateResult>;
|
||||
scoreEvents(input: JourneyEventScoreInput): Promise<LegacyCandidateResult>;
|
||||
buildDifferencePacket(input: DifferencePacketInput): Promise<CandidateDifferenceBuild>;
|
||||
scoreChoices(input: DynamicChoiceScoreInput): Promise<DynamicChoiceScoringResult>;
|
||||
}
|
||||
@@ -127,7 +127,7 @@ type StoredRectificationCaseBase = {
|
||||
readonly ayanamsa?: AyanamsaName;
|
||||
};
|
||||
readonly lifeEvents?: readonly LifeEvent[];
|
||||
readonly candidateResult?: CandidateResult | null;
|
||||
readonly candidateResult?: LegacyCandidateResult | null;
|
||||
};
|
||||
|
||||
export type LegacyStoredRectificationCase = StoredRectificationCaseBase & LegacyStoredFields;
|
||||
@@ -189,7 +189,7 @@ export type JourneyResponseBase = {
|
||||
readonly scoring: RectificationScoringResult | null;
|
||||
readonly answers: Readonly<Record<string, RectificationAnswer>>;
|
||||
readonly lifeEvents: readonly LifeEvent[];
|
||||
readonly candidateResult: CandidateResult | null;
|
||||
readonly candidateResult: LegacyCandidateResult | null;
|
||||
};
|
||||
|
||||
export type VersionedJourneyResponse = JourneyResponseBase & JourneyTurnState
|
||||
|
||||
@@ -6,11 +6,11 @@ import type {
|
||||
} from "./birth-time-dynamic-choice-internal.ts";
|
||||
import type { DynamicJourneyTurnState } from "./birth-time-journey-turn-protocol.ts";
|
||||
import type { EvidenceDraft, JourneyTurnState } from "./birth-time-journey-turn.ts";
|
||||
import type { EvidenceDomain } from "./birth-time-question-planner.ts";
|
||||
import type { LegacyGuidedEvidenceDomain } from "./birth-time-question-planner.ts";
|
||||
|
||||
type LegacyProgress = {
|
||||
readonly adaptiveRound: number;
|
||||
readonly askedDomains: readonly EvidenceDomain[];
|
||||
readonly askedDomains: readonly LegacyGuidedEvidenceDomain[];
|
||||
};
|
||||
|
||||
export type LegacyStoredFields = {
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
type NextAction,
|
||||
} from "./birth-time-journey-turn.ts";
|
||||
import {
|
||||
evidenceDomains,
|
||||
legacyGuidedEvidenceDomains,
|
||||
planEvidenceQuestion,
|
||||
type EvidenceDomain,
|
||||
type LegacyGuidedEvidenceDomain,
|
||||
type EvidenceQuestionPhase,
|
||||
type QuestionSpec,
|
||||
} from "./birth-time-question-planner.ts";
|
||||
@@ -22,19 +22,19 @@ import type { LegacyStoredRectificationCase as StoredRectificationCase } from ".
|
||||
type PlannedTurnInput = {
|
||||
readonly stored: StoredRectificationCase;
|
||||
readonly phase: EvidenceQuestionPhase;
|
||||
readonly askedDomains: readonly EvidenceDomain[];
|
||||
readonly askedDomains: readonly LegacyGuidedEvidenceDomain[];
|
||||
readonly adaptiveRound: number;
|
||||
readonly turnVersion: number;
|
||||
};
|
||||
|
||||
export type EvidenceQuestionIdentity = Pick<QuestionSpec, "questionId" | "phase" | "domain">;
|
||||
|
||||
function coveredDomains(events: readonly LifeEvent[]): readonly EvidenceDomain[] {
|
||||
return evidenceDomains.filter((domain) => events.some((event) => event.domain === domain));
|
||||
function coveredDomains(events: readonly LifeEvent[]): readonly LegacyGuidedEvidenceDomain[] {
|
||||
return legacyGuidedEvidenceDomains.filter((domain) => events.some((event) => event.domain === domain));
|
||||
}
|
||||
|
||||
export function canonicalAskedDomains(domains: readonly EvidenceDomain[]): readonly EvidenceDomain[] {
|
||||
return evidenceDomains.filter((domain) => domains.includes(domain));
|
||||
export function canonicalAskedDomains(domains: readonly LegacyGuidedEvidenceDomain[]): readonly LegacyGuidedEvidenceDomain[] {
|
||||
return legacyGuidedEvidenceDomains.filter((domain) => domains.includes(domain));
|
||||
}
|
||||
|
||||
function progress(
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { DataClient } from "@/lib/db/data-client";
|
||||
import { z } from "zod";
|
||||
import type { JourneySnapshot } from "./birth-time-journey.ts";
|
||||
import type { LegacyStoredRectificationCase, StoredRectificationCase } from "./birth-time-journey-service.ts";
|
||||
import { evidenceDomains } from "./birth-time-question-planner.ts";
|
||||
import type { EvidenceDomain } from "./birth-time-question-planner.ts";
|
||||
import { legacyGuidedEvidenceDomains } from "./birth-time-question-planner.ts";
|
||||
import type { LegacyGuidedEvidenceDomain } from "./birth-time-question-planner.ts";
|
||||
import type { EvidenceDraft, JourneyTurnState } from "./birth-time-journey-turn.ts";
|
||||
import {
|
||||
BirthTimeJourneyStoreError,
|
||||
@@ -17,7 +17,7 @@ export { BirthTimeJourneyStoreError, StaleJourneyTurnError } from "./birth-time-
|
||||
|
||||
export type PersistedJourneyProgress = {
|
||||
readonly adaptiveRound: number;
|
||||
readonly askedDomains: readonly EvidenceDomain[];
|
||||
readonly askedDomains: readonly LegacyGuidedEvidenceDomain[];
|
||||
};
|
||||
|
||||
export type PersistedJourneyTurn = {
|
||||
@@ -51,8 +51,8 @@ export function profileStatus(snapshot: JourneySnapshot) {
|
||||
return caseStatus(snapshot);
|
||||
}
|
||||
|
||||
function canonicalAskedDomains(input: readonly EvidenceDomain[]): readonly EvidenceDomain[] {
|
||||
return evidenceDomains.filter((domain) => input.includes(domain));
|
||||
function canonicalAskedDomains(input: readonly LegacyGuidedEvidenceDomain[]): readonly LegacyGuidedEvidenceDomain[] {
|
||||
return legacyGuidedEvidenceDomains.filter((domain) => input.includes(domain));
|
||||
}
|
||||
|
||||
export function createJourneyTurnPersistence(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CandidateResult, LifeEvent } from "./birth-time-evidence.ts";
|
||||
import { evidenceDomains, planEvidenceQuestion, type CandidateVargaSample, type EvidenceDomain, type QuestionSpec } from "./birth-time-question-planner.ts";
|
||||
import type { LegacyCandidateResult, LifeEvent } from "./birth-time-evidence.ts";
|
||||
import { legacyGuidedEvidenceDomains, planEvidenceQuestion, type CandidateVargaSample, type LegacyGuidedEvidenceDomain, type QuestionSpec } from "./birth-time-question-planner.ts";
|
||||
import type { JourneySnapshot } from "./birth-time-journey.ts";
|
||||
import type { EvidenceDraft, JourneyPermissions, JourneyProgress, JourneyTurnState, NextAction } from "./birth-time-journey-turn-protocol.ts";
|
||||
|
||||
@@ -20,7 +20,7 @@ export type {
|
||||
|
||||
export type DeriveNextActionInput = {
|
||||
readonly progress: JourneyProgress;
|
||||
readonly candidateResult?: CandidateResult | null;
|
||||
readonly candidateResult?: LegacyCandidateResult | null;
|
||||
readonly nextQuestion?: QuestionSpec | null;
|
||||
readonly confirmedTime?: string | null;
|
||||
readonly evidenceDraft?: EvidenceDraft | null;
|
||||
@@ -114,7 +114,7 @@ export function deriveNextAction(input: DeriveNextActionInput): NextAction {
|
||||
}
|
||||
|
||||
export function deriveJourneyPermissions(
|
||||
candidateResult: CandidateResult | null,
|
||||
candidateResult: LegacyCandidateResult | null,
|
||||
confirmedTime: string | null,
|
||||
): JourneyPermissions {
|
||||
return {
|
||||
@@ -158,14 +158,14 @@ export type JourneyTurnProjectionInput = {
|
||||
readonly questionnaire: { readonly samples: readonly CandidateVargaSample[] } | null;
|
||||
readonly persistedProgress?: {
|
||||
readonly adaptiveRound: number;
|
||||
readonly askedDomains: readonly EvidenceDomain[];
|
||||
readonly askedDomains: readonly LegacyGuidedEvidenceDomain[];
|
||||
} | null;
|
||||
readonly candidateResult: CandidateResult | null;
|
||||
readonly candidateResult: LegacyCandidateResult | null;
|
||||
readonly serverSelectedQuestion?: { readonly id: string; readonly domain?: string } | null;
|
||||
readonly lifeEvents: readonly LifeEvent[];
|
||||
};
|
||||
|
||||
const serverDomainMap: Readonly<Record<string, EvidenceDomain>> = {
|
||||
const serverDomainMap: Readonly<Record<string, LegacyGuidedEvidenceDomain>> = {
|
||||
education: "education",
|
||||
relocation: "relocation",
|
||||
residence: "relocation",
|
||||
@@ -220,7 +220,7 @@ export function projectJourneyTurn(input: JourneyTurnProjectionInput): JourneyTu
|
||||
const confirmedTime = input.snapshot.state === "ready" ? input.snapshot.activeTime : null;
|
||||
const coveredDomains = [...new Set(input.lifeEvents.map((event) => event.domain))];
|
||||
const adaptiveRound = input.persistedProgress?.adaptiveRound ?? 0;
|
||||
const askedDomains = evidenceDomains.filter((domain) => coveredDomains.includes(domain) || input.persistedProgress?.askedDomains.includes(domain) === true);
|
||||
const askedDomains = legacyGuidedEvidenceDomains.filter((domain) => coveredDomains.includes(domain) || input.persistedProgress?.askedDomains.includes(domain) === true);
|
||||
const displayedAdaptiveRound = input.candidateResult?.confidence === "low"
|
||||
&& adaptiveRound < 3
|
||||
? adaptiveRound + 1
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export const evidenceDomains = [
|
||||
export const legacyGuidedEvidenceDomains = [
|
||||
"education", "relocation", "relationship", "career", "finance", "health_pressure",
|
||||
] as const;
|
||||
|
||||
export type EvidenceDomain = (typeof evidenceDomains)[number];
|
||||
export type LegacyGuidedEvidenceDomain = (typeof legacyGuidedEvidenceDomains)[number];
|
||||
export type EvidenceQuestionPhase = "baseline" | "adaptive";
|
||||
export type EvidenceDatePrecision = "day" | "month" | "year";
|
||||
|
||||
@@ -22,15 +22,15 @@ export type CandidateVargaSample = {
|
||||
export type QuestionPlannerInput = {
|
||||
readonly phase: EvidenceQuestionPhase;
|
||||
readonly samples: readonly CandidateVargaSample[];
|
||||
readonly askedDomains: readonly EvidenceDomain[];
|
||||
readonly coveredDomains: readonly EvidenceDomain[];
|
||||
readonly askedDomains: readonly LegacyGuidedEvidenceDomain[];
|
||||
readonly coveredDomains: readonly LegacyGuidedEvidenceDomain[];
|
||||
readonly adaptiveRound: number;
|
||||
};
|
||||
|
||||
export type QuestionSpec = {
|
||||
readonly questionId: string;
|
||||
readonly phase: EvidenceQuestionPhase;
|
||||
readonly domain: EvidenceDomain;
|
||||
readonly domain: LegacyGuidedEvidenceDomain;
|
||||
readonly requestedPrecision: readonly EvidenceDatePrecision[];
|
||||
readonly allowUnknown: true;
|
||||
readonly purposeCode: string;
|
||||
@@ -47,7 +47,7 @@ const layerByDomain = {
|
||||
} as const;
|
||||
|
||||
function questionSpecFor(
|
||||
domain: EvidenceDomain,
|
||||
domain: LegacyGuidedEvidenceDomain,
|
||||
phase: EvidenceQuestionPhase,
|
||||
adaptiveRound: number,
|
||||
): QuestionSpec {
|
||||
@@ -63,7 +63,7 @@ function questionSpecFor(
|
||||
}
|
||||
|
||||
export function planEvidenceQuestion(input: QuestionPlannerInput): QuestionSpec | null {
|
||||
const available = evidenceDomains.filter((domain) => !input.askedDomains.includes(domain)
|
||||
const available = legacyGuidedEvidenceDomains.filter((domain) => !input.askedDomains.includes(domain)
|
||||
&& (domain !== "finance" || (input.phase === "adaptive" && input.coveredDomains.length >= 2)));
|
||||
const ranked = available.map((domain) => ({
|
||||
domain,
|
||||
@@ -71,7 +71,7 @@ export function planEvidenceQuestion(input: QuestionPlannerInput): QuestionSpec
|
||||
coverageBonus: input.coveredDomains.includes(domain) ? 0 : 1,
|
||||
})).sort((left, right) => right.split - left.split
|
||||
|| right.coverageBonus - left.coverageBonus
|
||||
|| evidenceDomains.indexOf(left.domain) - evidenceDomains.indexOf(right.domain));
|
||||
|| legacyGuidedEvidenceDomains.indexOf(left.domain) - legacyGuidedEvidenceDomains.indexOf(right.domain));
|
||||
const winner = ranked[0];
|
||||
return winner ? questionSpecFor(winner.domain, input.phase, input.adaptiveRound) : null;
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ import type {
|
||||
BirthTimeJourneyPorts,
|
||||
LegacyStoredRectificationCase as StoredRectificationCase,
|
||||
} from "./birth-time-journey-service.ts";
|
||||
import type { CandidateResult } from "./birth-time-evidence.ts";
|
||||
import type { LegacyCandidateResult } from "./birth-time-evidence.ts";
|
||||
|
||||
function requireMatchingEvidenceCounts(
|
||||
stored: StoredRectificationCase,
|
||||
candidate: CandidateResult,
|
||||
candidate: LegacyCandidateResult,
|
||||
): void {
|
||||
const events = stored.lifeEvents ?? [];
|
||||
const domainCount = new Set(events.map((event) => event.domain)).size;
|
||||
@@ -80,7 +80,7 @@ export function createBirthTimeScoringService(ports: BirthTimeJourneyPorts) {
|
||||
if (claim.algorithmVersion !== birthTimeScoringAlgorithmVersion) {
|
||||
throw new BirthTimeScoringJobError("algorithm_mismatch");
|
||||
}
|
||||
let candidateResult: CandidateResult;
|
||||
let candidateResult: LegacyCandidateResult;
|
||||
try {
|
||||
candidateResult = await ports.engine.scoreEvents(requireEventContext(stored));
|
||||
if (candidateResult.algorithmVersion !== claim.algorithmVersion) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { resolveAyanamsa } from "./ayanamsa.ts";
|
||||
import { birthCalendarDate } from "./effective-birth-date.ts";
|
||||
import { globalBirthProfileFromAccountRow } from "./server-owned-birth-profile.ts";
|
||||
import { recordOrEmpty as record } from "@/lib/guards";
|
||||
|
||||
export const CHART_BIRTH_FAILURE_REASONS = [
|
||||
"profile_query_error",
|
||||
@@ -50,12 +51,6 @@ const ADOPTION_CLEAR_KEYS = [
|
||||
"timezone_offset",
|
||||
] as const;
|
||||
|
||||
function record(value: unknown): Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: {};
|
||||
}
|
||||
|
||||
function text(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isAyanamsaName, resolveAyanamsa } from "./ayanamsa.ts";
|
||||
import { parseProfileGender } from "./profile-gender.ts";
|
||||
import { MAX_OTHER_SUBJECTS } from "./subject-birth.ts";
|
||||
import { asRecord as recordOf } from "@/lib/guards";
|
||||
|
||||
const CLOCK = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
||||
const DATE = /^(?:19|20)\d{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;
|
||||
@@ -13,12 +14,6 @@ export type ChartSubjectWrite = {
|
||||
readonly profile: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function recordOf(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
function text(value: unknown, max: number): string {
|
||||
return typeof value === "string" ? value.trim().slice(0, max) : "";
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
type ChartViewWesternOk,
|
||||
type NorthIndianChart,
|
||||
} from "./chart-view-contract.ts";
|
||||
import { asRecord as record } from "@/lib/guards";
|
||||
|
||||
export type ChartViewProfileInput = {
|
||||
name?: string;
|
||||
@@ -64,12 +65,6 @@ export type ChartViewEngineBundle = {
|
||||
asOf: string;
|
||||
};
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
function text(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ConsultationDomain } from "./consultation-domain-registry.ts";
|
||||
import { consultationDomainDefinition } from "./consultation-domain-registry.ts";
|
||||
import { assembleJudgmentTable, judgmentTableModelView, participationModelView } from "./consultation-judgment-table.ts";
|
||||
import type { ProfileGender } from "./profile-gender.ts";
|
||||
import { recordOrEmpty as record } from "@/lib/guards";
|
||||
|
||||
/**
|
||||
* The evidence card: what the answer-writing model reads about the chart
|
||||
@@ -379,10 +380,6 @@ export type EvidenceCardSource = Readonly<{
|
||||
context: unknown;
|
||||
}>;
|
||||
|
||||
function record(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
function isJson(value: unknown): value is Json {
|
||||
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
||||
if (typeof value === "number") return Number.isFinite(value);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { recordOrEmpty as record } from "@/lib/guards";
|
||||
/**
|
||||
* One judgment table per consult domain (TASK-consult-card-full-source-20261007).
|
||||
*
|
||||
@@ -34,10 +35,6 @@ const MALEFIC_BODIES = ["Saturn", "Mars", "Rahu", "Ketu"] as const;
|
||||
const ASPECT_MALEFICS = ["Saturn", "Mars"] as const;
|
||||
const DUSTHANA = new Set([6, 8, 12]);
|
||||
|
||||
function record(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
function isJson(value: unknown): value is Json {
|
||||
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
||||
if (typeof value === "number") return Number.isFinite(value);
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
type ConsultationSubjectBinding,
|
||||
type OwnedChartProfile,
|
||||
} from "./consultation-subject-resolver.ts";
|
||||
import { asRecord as record } from "@/lib/guards";
|
||||
|
||||
export type ConsultationProfileTruthErrorCode =
|
||||
| "profile_unavailable"
|
||||
@@ -213,12 +214,6 @@ export function declaredBirthAccuracyFromProfile(input: {
|
||||
return { declaredAccuracy: "15min", timeSource: "family_vague" };
|
||||
}
|
||||
|
||||
function record(value: unknown): RecordValue | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as RecordValue
|
||||
: null;
|
||||
}
|
||||
|
||||
function requiredText(profile: RecordValue, key: string): string {
|
||||
const value = profile[key];
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asRecord } from "@/lib/guards";
|
||||
/**
|
||||
* Per-step clocks for one ordinary consultation.
|
||||
*
|
||||
@@ -49,12 +50,6 @@ function tokenOrNull(value: unknown): number | null {
|
||||
return truncated > 1_000_000_000 ? null : truncated;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): UsageRecord | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? value as UsageRecord
|
||||
: null;
|
||||
}
|
||||
|
||||
/** Read the provider's cached-input count. Absent means null, including when only a sibling field exists. */
|
||||
export function cachedInputTokensFromUsage(usage: UsageRecord): number | null {
|
||||
if ("cachedInputTokens" in usage) return tokenOrNull(usage.cachedInputTokens);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ConsultationSubjectError,
|
||||
resolveSubjectBirth,
|
||||
} from "./subject-birth.ts";
|
||||
import { asRecord as recordOf } from "@/lib/guards";
|
||||
|
||||
export {
|
||||
ConsultationSubjectError,
|
||||
@@ -58,12 +59,6 @@ function textOrNull(value: unknown): string | null {
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
function recordOf(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
function pickText(row: Record<string, unknown>, keys: readonly string[]): string | null {
|
||||
for (const key of keys) {
|
||||
const value = textOrNull(row[key]);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const REPORT_DEPTHS = ["concise", "standard", "deep", "research"] as const;
|
||||
export type ReportDepth = (typeof REPORT_DEPTHS)[number];
|
||||
|
||||
export const reportRequestThemes = z.enum([
|
||||
"career",
|
||||
"marriage",
|
||||
"wealth",
|
||||
"health",
|
||||
"timing",
|
||||
"general",
|
||||
]);
|
||||
|
||||
/** Same strings as the schema default for `themes`. */
|
||||
export const DEFAULT_REPORT_THEMES = [
|
||||
"career",
|
||||
"marriage",
|
||||
"wealth",
|
||||
"timing",
|
||||
"health",
|
||||
] as const;
|
||||
|
||||
export const personalReportCreateRequestSchema = z.object({
|
||||
requestId: z.string().uuid(),
|
||||
sessionId: z.string().uuid().nullable().optional(),
|
||||
chartProfileId: z.string().uuid().nullable().optional(),
|
||||
reportType: z.enum(["personal_full", "personal_thematic"]),
|
||||
presentationMode: z.enum(["default", "research"]).default("default"),
|
||||
depth: z.enum(REPORT_DEPTHS).default("standard"),
|
||||
themes: z.array(reportRequestThemes).min(1).max(6).default([...DEFAULT_REPORT_THEMES]),
|
||||
}).strict();
|
||||
|
||||
/** Parsed server request. Defaulted fields are present. */
|
||||
export type PersonalReportCreateRequest = z.infer<typeof personalReportCreateRequestSchema>;
|
||||
|
||||
/** Client payload. Defaulted fields stay optional so callers can omit them. */
|
||||
export type PersonalReportCreateRequestInput = z.input<typeof personalReportCreateRequestSchema>;
|
||||
@@ -0,0 +1,78 @@
|
||||
export const RECTIFICATION_CASE_STATUSES = [
|
||||
"draft",
|
||||
"collecting_evidence",
|
||||
"candidate_ready",
|
||||
"candidate_accepted",
|
||||
"needs_rebaseline",
|
||||
"paused",
|
||||
"confirmed",
|
||||
"closed",
|
||||
"abandoned",
|
||||
"superseded",
|
||||
] as const;
|
||||
|
||||
export type RectificationCaseStatus = (typeof RECTIFICATION_CASE_STATUSES)[number];
|
||||
|
||||
/** Statuses that may be resumed by the user. */
|
||||
export const RESUMABLE_CASE_STATUSES: readonly RectificationCaseStatus[] = [
|
||||
"draft",
|
||||
"collecting_evidence",
|
||||
"candidate_ready",
|
||||
"candidate_accepted",
|
||||
"needs_rebaseline",
|
||||
"paused",
|
||||
];
|
||||
|
||||
/** Statuses that only allow read-only history access. */
|
||||
export const TERMINAL_CASE_STATUSES: readonly RectificationCaseStatus[] = [
|
||||
"confirmed",
|
||||
"closed",
|
||||
"abandoned",
|
||||
"superseded",
|
||||
];
|
||||
|
||||
export type OpenRectificationDisposition = "created" | "resumed" | "readonly";
|
||||
|
||||
/** Wire shape. Status stays a string so cached and older payloads still parse. */
|
||||
export type OpenRectificationCaseResponseWire = Readonly<{
|
||||
disposition: OpenRectificationDisposition;
|
||||
caseId: string;
|
||||
sessionId: string;
|
||||
status: string;
|
||||
shouldStartOpening: boolean;
|
||||
skillVersion: string;
|
||||
}>;
|
||||
|
||||
export type OpenRectificationCaseResponse = Omit<OpenRectificationCaseResponseWire, "status"> & {
|
||||
readonly status: RectificationCaseStatus;
|
||||
};
|
||||
|
||||
type LatestResumableWire = Readonly<{
|
||||
caseId: string;
|
||||
status: string;
|
||||
lastActivityAt: string;
|
||||
}>;
|
||||
|
||||
type LatestTerminalWire = Readonly<{
|
||||
caseId: string;
|
||||
status: string;
|
||||
hasUsableTime: boolean;
|
||||
}>;
|
||||
|
||||
export type RectificationEntrySummaryWire = Readonly<{
|
||||
hasResumableCase: boolean;
|
||||
hasTerminalCaseWithTime: boolean;
|
||||
latestResumable: LatestResumableWire | null;
|
||||
latestTerminal: LatestTerminalWire | null;
|
||||
}>;
|
||||
|
||||
export type RectificationEntrySummary = Readonly<{
|
||||
hasResumableCase: boolean;
|
||||
hasTerminalCaseWithTime: boolean;
|
||||
latestResumable: (Omit<LatestResumableWire, "status"> & {
|
||||
readonly status: RectificationCaseStatus;
|
||||
}) | null;
|
||||
latestTerminal: (Omit<LatestTerminalWire, "status"> & {
|
||||
readonly status: RectificationCaseStatus;
|
||||
}) | null;
|
||||
}>;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { effectiveBirthDate } from "./effective-birth-date";
|
||||
import { asRecord } from "@/lib/guards";
|
||||
|
||||
export type DailyStarlanguageCard = {
|
||||
readonly trend: string;
|
||||
@@ -53,10 +54,6 @@ function normalizeLine(value: string): string {
|
||||
return value.replace(/\s+/g, " ").trim().slice(0, 90);
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
function asArray(value: unknown): readonly unknown[] {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
} from "@/lib/ephemeris-contract";
|
||||
import { isAyanamsaName, type AyanamsaName } from "@/lib/ayanamsa";
|
||||
import { normalizePanchangaName, panchangaNameZh, splitTithiName } from "@/lib/panchanga-names-zh";
|
||||
import { asRecord } from "@/lib/guards";
|
||||
|
||||
export const EPHEMERIS_COPY = {
|
||||
title: "星历",
|
||||
@@ -96,12 +97,6 @@ const FACTOR_LABEL: Record<EphemerisPanchangaFactor["key"], string> = {
|
||||
karana: "迦那",
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
function asText(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asRecord as record } from "@/lib/guards";
|
||||
type RecordValue = Record<string, unknown>;
|
||||
|
||||
export type GeneralDailyReference = Readonly<{
|
||||
@@ -34,12 +35,6 @@ type LoadGeneralDailyPanchangaInput = Readonly<{
|
||||
apiBase?: string;
|
||||
}>;
|
||||
|
||||
function record(value: unknown): RecordValue | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as RecordValue
|
||||
: null;
|
||||
}
|
||||
|
||||
function text(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Shared guards for non-array objects and finite numbers.
|
||||
* Array-accepting record variants and string-to-number parsing stay private.
|
||||
*/
|
||||
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return isRecord(value) ? value : null;
|
||||
}
|
||||
|
||||
export function recordOrEmpty(value: unknown): Record<string, unknown> {
|
||||
return isRecord(value) ? value : {};
|
||||
}
|
||||
|
||||
export function finiteNumberOrNull(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* SHA-256 hex digest.
|
||||
* Imports node:crypto. Server modules only — do not import this file from client components.
|
||||
*/
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
export function sha256Hex(text: string): string {
|
||||
return createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
@@ -15,6 +15,9 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { reportFactTablesSchema } from "./report-fact-table-schema";
|
||||
import { REPORT_DEPTHS, type ReportDepth } from "./contracts/personal-report-request";
|
||||
|
||||
export { REPORT_DEPTHS, type ReportDepth };
|
||||
|
||||
export const LEGACY_REPORT_DOCUMENT_SCHEMA_VERSION = "report_document.v1" as const;
|
||||
export const REPORT_DOCUMENT_SCHEMA_VERSION = "report_document.v2" as const;
|
||||
@@ -39,8 +42,6 @@ export type ClaimStatus = (typeof CLAIM_STATUSES)[number];
|
||||
|
||||
export const REPORT_TYPES = ["personal_full", "personal_thematic"] as const;
|
||||
export const PRESENTATION_MODES = ["default", "research"] as const;
|
||||
export const REPORT_DEPTHS = ["concise", "standard", "deep", "research"] as const;
|
||||
export type ReportDepth = (typeof REPORT_DEPTHS)[number];
|
||||
export const BIRTH_TIME_STATUSES = ["reported", "candidate", "accepted", "confirmed"] as const;
|
||||
export const TECHNIQUE_STATUSES = ["verified", "partial", "blocked"] as const;
|
||||
export const CONFLICT_STATUSES = ["unresolved", "partial", "resolved"] as const;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { assembleReportFactTables } from "./report-fact-tables";
|
||||
import { sha256Hex } from "./hash";
|
||||
import {
|
||||
computeEvidenceHash,
|
||||
safeParseServerReportDocument,
|
||||
@@ -10,13 +10,13 @@ import {
|
||||
CHART_IDS,
|
||||
REPORT_DOCUMENT_V2_ACTION_NOTES_MAX,
|
||||
REQUIRED_THEME_CHARTS,
|
||||
type ClaimStatus,
|
||||
type EvidenceAppendix,
|
||||
type ReportDepth,
|
||||
type ReportDocumentParseError,
|
||||
type ReportDocumentV1,
|
||||
type ReportDocumentV2,
|
||||
} from "./personal-report-contract.ts";
|
||||
import type { ClaimStatus } from "./personal-report-contract.ts";
|
||||
import {
|
||||
PersonalReportAgentOutputError,
|
||||
type EvidenceRefStatus,
|
||||
@@ -62,8 +62,10 @@ import {
|
||||
} from "./personal-report-plan.ts";
|
||||
import type { PersonalReportSectionRecord, PersonalReportSectionService } from "./personal-report-section-service-core.ts";
|
||||
import { withReportBirthFingerprint } from "./report-birth-binding.ts";
|
||||
import { asRecord as record, finiteNumberOrNull as finiteNumber } from "@/lib/guards";
|
||||
// Compatibility re-export: prefer importing from ./personal-report-codes.ts
|
||||
// directly (the pure, dependency-free codes module).
|
||||
export { sha256Hex };
|
||||
export { REPORT_STABLE_CODES } from "./personal-report-codes.ts";
|
||||
|
||||
/**
|
||||
@@ -105,10 +107,6 @@ export function canonicalSerialize(value: unknown): string {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
export function sha256Hex(text: string): string {
|
||||
return createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
|
||||
const sha256Pattern = /^[0-9a-f]{64}$/;
|
||||
|
||||
/**
|
||||
@@ -183,22 +181,12 @@ export function resolveSkillSnapshot(): SkillSnapshot {
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function record(value: unknown): JsonRecord | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as JsonRecord
|
||||
: null;
|
||||
}
|
||||
|
||||
function text(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
function finiteNumber(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function booleanValue(value: unknown): boolean | null {
|
||||
return typeof value === "boolean" ? value : null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { sha256Hex } from "./hash";
|
||||
|
||||
export const LONGFORM_APPENDIX_TABLE = "personal_report_longform_appendices";
|
||||
export const LONGFORM_APPENDIX_MAX_ATTEMPTS = 2;
|
||||
@@ -25,7 +25,7 @@ export type LongformAppendixWrite = Readonly<{
|
||||
}>;
|
||||
|
||||
export function hashLongformMarkdown(markdown: string): string {
|
||||
return createHash("sha256").update(markdown, "utf8").digest("hex");
|
||||
return sha256Hex(markdown);
|
||||
}
|
||||
|
||||
export function parseLongformAppendixRow(value: unknown): LongformAppendixRow | null {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { persistLongformSnapshot, readLongformSnapshot, validateLongformSnapshot
|
||||
import { acceptEnglishEdition, type EnglishOutcome } from "./personal-report-english-edition";
|
||||
import { hashLongformMarkdown } from "./personal-report-longform-appendix";
|
||||
import { annotateUnreliableChartSections } from "./personal-report-chart-caveats";
|
||||
import { asRecord as record } from "@/lib/guards";
|
||||
|
||||
import {
|
||||
LONGFORM_APPENDIX_TABLE,
|
||||
@@ -70,12 +71,6 @@ export type LongformGenerateDeps = Readonly<{
|
||||
signal?: AbortSignal;
|
||||
}>;
|
||||
|
||||
function record(value: unknown): JsonRecord | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as JsonRecord
|
||||
: null;
|
||||
}
|
||||
|
||||
function text(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import type { ReportEvidenceBundleV2 } from "./report-evidence-bundle-v2.ts";
|
||||
import { REPORT_DEPTHS, type ReportDepth } from "./contracts/personal-report-request.ts";
|
||||
|
||||
export const REPORT_DEPTHS = ["concise", "standard", "deep", "research"] as const;
|
||||
export type ReportDepth = (typeof REPORT_DEPTHS)[number];
|
||||
export { REPORT_DEPTHS, type ReportDepth };
|
||||
|
||||
export const REPORT_SECTION_KINDS = [
|
||||
"executive_summary",
|
||||
|
||||
@@ -13,8 +13,12 @@ import { parseReportCandidateRange, type ReportCandidateClockRange } from "./rep
|
||||
* deletes, and the persistence service scopes every query by userId.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { effectiveBirthDate } from "./effective-birth-date";
|
||||
import { asRecord as record, finiteNumberOrNull as finiteNumber } from "@/lib/guards";
|
||||
import {
|
||||
personalReportCreateRequestSchema,
|
||||
type PersonalReportCreateRequest,
|
||||
} from "./contracts/personal-report-request";
|
||||
import type { ConsultationInput } from "@/mastra";
|
||||
import { resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
import type {
|
||||
@@ -49,19 +53,7 @@ import type {
|
||||
PersonalReportService,
|
||||
} from "./personal-report-service-core";
|
||||
|
||||
const reportRequestThemes = z.enum(["career", "marriage", "wealth", "health", "timing", "general"]);
|
||||
|
||||
export const personalReportCreateRequestSchema = z.object({
|
||||
requestId: z.string().uuid(),
|
||||
sessionId: z.string().uuid().nullable().optional(),
|
||||
chartProfileId: z.string().uuid().nullable().optional(),
|
||||
reportType: z.enum(["personal_full", "personal_thematic"]),
|
||||
presentationMode: z.enum(["default", "research"]).default("default"),
|
||||
depth: z.enum(["concise", "standard", "deep", "research"]).default("standard"),
|
||||
themes: z.array(reportRequestThemes).min(1).max(6).default(["career", "marriage", "wealth", "timing", "health"]),
|
||||
}).strict();
|
||||
|
||||
export type PersonalReportCreateRequest = z.infer<typeof personalReportCreateRequestSchema>;
|
||||
export { personalReportCreateRequestSchema, type PersonalReportCreateRequest };
|
||||
|
||||
export type ReportRouteResponse = Readonly<{
|
||||
status: number;
|
||||
@@ -81,22 +73,12 @@ export type ReportServicePort = Pick<
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function record(value: unknown): JsonRecord | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as JsonRecord
|
||||
: null;
|
||||
}
|
||||
|
||||
function text(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
function finiteNumber(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
const CONCRETE_REPORTED_SOURCES = new Set(["hospital_record", "family_exact", "approximate"]);
|
||||
|
||||
export type ReportSubjectBirthTimeStatus = "reported" | "candidate" | "accepted" | "confirmed";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ReportCandidateClockRange } from "./report-candidate-range";
|
||||
import "server-only";
|
||||
import { effectiveBirthDate } from "./effective-birth-date";
|
||||
import { asRecord as record, finiteNumberOrNull as finiteNumber } from "@/lib/guards";
|
||||
|
||||
import { runConsultationWorkflow } from "@/mastra";
|
||||
import { createPersonalReportAgent } from "@/mastra/personal-report";
|
||||
@@ -38,7 +39,6 @@ import { resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
import { loadReportWorkerProfile, reportWorkerUsesCandidateRange } from "@/lib/personal-report-worker-subject";
|
||||
import { readReportBirthFingerprint } from "@/lib/report-birth-binding";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
type WorkerGlobal = typeof globalThis & {
|
||||
jyotishaPersonalReportWorker?: PersonalReportWorkerHandle;
|
||||
};
|
||||
@@ -49,22 +49,12 @@ export type PersonalReportWorkerHandle = Readonly<{
|
||||
stop: () => void;
|
||||
}>;
|
||||
|
||||
function record(value: unknown): JsonRecord | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as JsonRecord
|
||||
: null;
|
||||
}
|
||||
|
||||
function text(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
function finiteNumber(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function parseBirthDate(value: unknown): { year: number; month: number; day: number } | null {
|
||||
const date = text(value);
|
||||
const match = date ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(date) : null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { parseSegmentSummary, TARGET_CHARTS, type SegmentSummary, type TargetChart } from "./rectification-agentic/core/segment-summary.ts";
|
||||
import { asRecord as record } from "@/lib/guards";
|
||||
|
||||
/**
|
||||
* Which divisional charts a segment-v1 adoption could not determine (BUG-1138/1139).
|
||||
@@ -28,10 +29,6 @@ export type AdoptedChartTierWarn = (payload: Readonly<{ event: "adopted_chart_ti
|
||||
|
||||
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
/** Only the segment-v1 adoption contract carries a result whose summary describes the saved minute. */
|
||||
export function segmentProvenance(value: unknown): SegmentProvenance | null {
|
||||
const row = record(value);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { candidateSetId } from "./candidate-set-id.ts";
|
||||
import { readCandidatePosition, parseWindowIntervals, type CandidatePosition } from "./candidate-window.ts";
|
||||
import { INFERENCE_ALGORITHM_VERSION, type InferenceState } from "./types.ts";
|
||||
import { buildSegmentSummary, parseAscendantSignature, parseSegmentSummary, TARGET_CHARTS, type SegmentMinute } from "./segment-summary.ts";
|
||||
import { isRecord } from "@/lib/guards";
|
||||
|
||||
export type InferenceTransitionSnapshot = Readonly<{
|
||||
id?: string;
|
||||
@@ -39,10 +40,6 @@ const ANSWER_SOURCES = new Set(["choice", "evidence", "declined"]);
|
||||
const ROUND_KINDS = new Set(["informative", "low_information", "tie_break"]);
|
||||
const SCORE_DIRECTIONS = new Set(["support", "weak_support", "neutral", "weak_conflict", "conflict"]);
|
||||
|
||||
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isClockRange(value: unknown, dated = false): value is readonly [string, string] {
|
||||
return Array.isArray(value)
|
||||
&& value.length === 2
|
||||
|
||||
@@ -78,7 +78,8 @@ import {
|
||||
type PersistChoiceActionInput,
|
||||
type V9CaseDossier,
|
||||
} from "./tool-service";
|
||||
import { CHOICE_SKIP_QUESTION_LABEL, isPersistedFocusId, isTieBreakRoundSchema, type ChoiceKey } from "./choice-card";
|
||||
import { CHOICE_SKIP_QUESTION_LABEL, isPersistedFocusId, isTieBreakRoundSchema } from "./choice-card";
|
||||
import type { ChoiceKey } from "./choice-card";
|
||||
import { vargaStyleFollowupRenderable } from "./probe-question-contract";
|
||||
import { clusterScoreDeltas, excludedClusterRanges } from "./probe-explain.ts";
|
||||
import {
|
||||
|
||||
@@ -17,10 +17,10 @@ import {
|
||||
CHOICE_MODE,
|
||||
CHOICE_STOP_LABEL,
|
||||
CHOICE_STOP_MESSAGE,
|
||||
type ChoiceKey,
|
||||
type RectificationChoiceFrame,
|
||||
type RectificationChoiceOption,
|
||||
} from "./choice-card.ts";
|
||||
import type { ChoiceKey } from "./choice-card.ts";
|
||||
import {
|
||||
MAX_SUB_BLOCK_ROUNDS,
|
||||
SUB_BLOCK_LABELS,
|
||||
|
||||
@@ -6,39 +6,19 @@
|
||||
* ordering.
|
||||
*/
|
||||
|
||||
export const RECTIFICATION_CASE_STATUSES = [
|
||||
"draft",
|
||||
"collecting_evidence",
|
||||
"candidate_ready",
|
||||
"candidate_accepted",
|
||||
"needs_rebaseline",
|
||||
"paused",
|
||||
"confirmed",
|
||||
"closed",
|
||||
"abandoned",
|
||||
"superseded",
|
||||
] as const;
|
||||
import {
|
||||
RECTIFICATION_CASE_STATUSES,
|
||||
RESUMABLE_CASE_STATUSES,
|
||||
TERMINAL_CASE_STATUSES,
|
||||
} from "@/lib/contracts/rectification-entry";
|
||||
import type { RectificationCaseStatus } from "@/lib/contracts/rectification-entry";
|
||||
|
||||
export type RectificationCaseStatus =
|
||||
(typeof RECTIFICATION_CASE_STATUSES)[number];
|
||||
|
||||
/** Statuses that may be resumed by the user. */
|
||||
export const RESUMABLE_CASE_STATUSES: readonly RectificationCaseStatus[] = [
|
||||
"draft",
|
||||
"collecting_evidence",
|
||||
"candidate_ready",
|
||||
"candidate_accepted",
|
||||
"needs_rebaseline",
|
||||
"paused",
|
||||
];
|
||||
|
||||
/** Statuses that only allow read-only history access. */
|
||||
export const TERMINAL_CASE_STATUSES: readonly RectificationCaseStatus[] = [
|
||||
"confirmed",
|
||||
"closed",
|
||||
"abandoned",
|
||||
"superseded",
|
||||
];
|
||||
export {
|
||||
RECTIFICATION_CASE_STATUSES,
|
||||
RESUMABLE_CASE_STATUSES,
|
||||
TERMINAL_CASE_STATUSES,
|
||||
};
|
||||
export type { RectificationCaseStatus };
|
||||
|
||||
const RESUMABLE_SET = new Set<string>(RESUMABLE_CASE_STATUSES);
|
||||
const TERMINAL_SET = new Set<string>(TERMINAL_CASE_STATUSES);
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
CHOICE_SKIP_QUESTION_LABEL,
|
||||
CHOICE_SKIP_QUESTION_MESSAGE,
|
||||
HOLDOUT_MESSAGE_PREFIX,
|
||||
type ChoiceKey,
|
||||
type RectificationChoiceCard,
|
||||
} from "./choice-card";
|
||||
import type { ChoiceKey } from "./choice-card";
|
||||
import {
|
||||
explainExcludedRanges,
|
||||
explainRangeChange,
|
||||
|
||||
@@ -18,6 +18,9 @@ import {
|
||||
type ProbeAnswerImpact,
|
||||
type ProbeExplainCandidate,
|
||||
} from "./probe-explain.ts";
|
||||
import type { ChoiceKey } from "./probe-explain.ts";
|
||||
|
||||
export type { ChoiceKey };
|
||||
|
||||
export type ChoiceCardKind = EventProbeChoiceKind | "block_choice" | "widen_window";
|
||||
|
||||
@@ -53,8 +56,6 @@ export function isTieBreakRoundSchema(value: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export type ChoiceKey = "A" | "B" | "C" | "D";
|
||||
|
||||
export type RectificationChoiceOption = Readonly<{
|
||||
key: ChoiceKey;
|
||||
label: string;
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { WindowInterval } from "../core/candidate-window.ts";
|
||||
import { RECTIFICATION_POLICY } from "../../rectification-policy.ts";
|
||||
import { RECTIFICATION_TERMINATION_COPY } from "../core/rectification-decision.ts";
|
||||
import { indistinguishableWidthMinutes } from "./candidate-plateau.ts";
|
||||
import { asRecord } from "@/lib/guards";
|
||||
/**
|
||||
* The six fields of `references/rectification_sealed_holdout.v1.json` the gate
|
||||
* reads. That JSON stays the one source: `rectification-confirmation-gate.test.ts`
|
||||
@@ -182,12 +183,6 @@ export function sessionOutcomeFromGate(input: {
|
||||
return sessionOutcomeView("collect_evidence");
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Readonly<Record<string, unknown>> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Readonly<Record<string, unknown>>
|
||||
: null;
|
||||
}
|
||||
|
||||
export function readVedastroMinuteSensitiveStatus(
|
||||
decisionReceipt: Readonly<Record<string, unknown>> | null | undefined,
|
||||
): string {
|
||||
|
||||
@@ -29,6 +29,7 @@ import type { AscendantSignature } from "../core/segment-summary.ts";
|
||||
import { isDatedScoringAlgorithmVersion, requestCandidateIntervals, readCandidatePosition, type CandidatePosition, type DatedCandidateRange } from "../core/candidate-window.ts";
|
||||
import { parseBlockScanPayload, type BlockScanBlock } from "./block-scan.ts";
|
||||
import { recordEngineCallTiming, reportTurnProgress } from "./turn-instrumentation.ts";
|
||||
import { asRecord as record } from "@/lib/guards";
|
||||
|
||||
export type EngineCallFailureKind = "busy" | "http_error" | "timeout" | "bad_payload";
|
||||
|
||||
@@ -210,12 +211,6 @@ const EXECUTION_LEDGER_VERSION = "rectification-execution-ledger-v2";
|
||||
const timePattern = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
||||
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
function engineNumber(value: unknown): number | null {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
|
||||
@@ -23,13 +23,14 @@ import type { TransitionSignLookup } from "../core/sign-from-transitions.ts";
|
||||
import { datedPrecision } from "./evidence-model.ts";
|
||||
import {
|
||||
isHoldoutVerificationQuote,
|
||||
type ChoiceKey,
|
||||
} from "./choice-card.ts";
|
||||
import type { ChoiceKey } from "./choice-card.ts";
|
||||
import {
|
||||
parseNakshatraBoundary,
|
||||
type DiscriminatingEventProbe,
|
||||
type NakshatraBoundary,
|
||||
} from "./refinement-packet.ts";
|
||||
import { asRecord } from "@/lib/guards";
|
||||
|
||||
function yearFrom(value: string | null | undefined): number | null {
|
||||
if (!value || value.length < 4 || !/^\d{4}/.test(value)) return null;
|
||||
@@ -470,12 +471,6 @@ export function buildCaseInferenceState(input: {
|
||||
});
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Readonly<Record<string, unknown>> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Readonly<Record<string, unknown>>
|
||||
: null;
|
||||
}
|
||||
|
||||
function asText(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { sha256Hex } from "@/lib/hash";
|
||||
|
||||
export const RECTIFICATION_MESSAGE_ORIGINS = [
|
||||
"typed",
|
||||
@@ -24,5 +24,5 @@ export function defaultMessageOrigin(action: string): RectificationMessageOrigin
|
||||
}
|
||||
|
||||
export function messageContentHash(text: string | null | undefined): string {
|
||||
return createHash("sha256").update(text ?? "").digest("hex");
|
||||
return sha256Hex(text ?? "");
|
||||
}
|
||||
|
||||
@@ -10,8 +10,18 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
isRectificationCaseStatus,
|
||||
type RectificationCaseStatus,
|
||||
} from "./case-status";
|
||||
import type {
|
||||
OpenRectificationCaseResponse,
|
||||
OpenRectificationDisposition,
|
||||
RectificationEntrySummary,
|
||||
} from "@/lib/contracts/rectification-entry";
|
||||
|
||||
export type {
|
||||
OpenRectificationCaseResponse,
|
||||
OpenRectificationDisposition,
|
||||
RectificationEntrySummary,
|
||||
};
|
||||
|
||||
export const OPEN_RECTIFICATION_INTENTS = [
|
||||
"homepage",
|
||||
@@ -57,35 +67,7 @@ export const openRectificationDispositions = [
|
||||
"created",
|
||||
"resumed",
|
||||
"readonly",
|
||||
] as const;
|
||||
|
||||
export type OpenRectificationDisposition =
|
||||
(typeof openRectificationDispositions)[number];
|
||||
|
||||
export type OpenRectificationCaseResponse = Readonly<{
|
||||
disposition: OpenRectificationDisposition;
|
||||
caseId: string;
|
||||
sessionId: string;
|
||||
status: RectificationCaseStatus;
|
||||
shouldStartOpening: boolean;
|
||||
skillVersion: string;
|
||||
}>;
|
||||
|
||||
/** Entry-summary contract used by the homepage card. */
|
||||
export type RectificationEntrySummary = Readonly<{
|
||||
hasResumableCase: boolean;
|
||||
hasTerminalCaseWithTime: boolean;
|
||||
latestResumable: Readonly<{
|
||||
caseId: string;
|
||||
status: RectificationCaseStatus;
|
||||
lastActivityAt: string;
|
||||
}> | null;
|
||||
latestTerminal: Readonly<{
|
||||
caseId: string;
|
||||
status: RectificationCaseStatus;
|
||||
hasUsableTime: boolean;
|
||||
}> | null;
|
||||
}>;
|
||||
] as const satisfies readonly OpenRectificationDisposition[];
|
||||
|
||||
export function parseOpenRectificationCaseRequest(
|
||||
value: unknown,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { parseCandidateIntervals } from "../core/candidate-window.ts";
|
||||
import { hasDatedTransition, parseDatedTransition, validDatedTransitions, type DatedTransitionPosition } from "../core/sign-from-transitions.ts";
|
||||
import { engineMeaningToDisplayCopy } from "../user-copy.ts";
|
||||
import { questionContractVersionIsCompatible } from "./probe-question-contract.ts";
|
||||
import { asRecord } from "@/lib/guards";
|
||||
|
||||
const TIME = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
||||
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
@@ -356,12 +357,6 @@ export type CandidateContrastOpportunity = Readonly<{
|
||||
phase: "candidate_discriminator";
|
||||
}>;
|
||||
|
||||
function asRecord(value: unknown): Readonly<Record<string, unknown>> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Readonly<Record<string, unknown>>
|
||||
: null;
|
||||
}
|
||||
|
||||
function asTime(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const normalized = value.slice(0, 5);
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { parseAscendantSignature, type SegmentMinute, type AscendantSignature } from "../core/segment-summary.ts";
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
}
|
||||
import { asRecord as record } from "@/lib/guards";
|
||||
|
||||
export function segmentCandidateFields(row: Record<string, unknown> | null): { signature?: AscendantSignature; rawScore?: number } {
|
||||
const signature = parseAscendantSignature(row?.signature);
|
||||
|
||||
@@ -3,7 +3,8 @@ import { z } from "zod";
|
||||
|
||||
import type { ResolvedLanguageModel } from "@/mastra/model";
|
||||
import type { AnswerClass } from "../core/types";
|
||||
import { parseAgentChoiceCopy, type ChoiceKey } from "./choice-card";
|
||||
import { parseAgentChoiceCopy } from "./choice-card";
|
||||
import type { ChoiceKey } from "./choice-card";
|
||||
import { isCollectFocusSchema } from "./server-focus";
|
||||
import type { ConversationFocus } from "./tool-service";
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { evidenceRecapLine } from "./host-fallback.ts";
|
||||
import { displayDateLabel, eventPhraseFromSummary } from "./evidence-model.ts";
|
||||
import { stripQuestionSentences } from "./collect-prompt";
|
||||
import { parseAgentChoiceCopy, type ChoiceKey, type RectificationChoiceCard } from "./choice-card";
|
||||
import { parseAgentChoiceCopy, type RectificationChoiceCard } from "./choice-card";
|
||||
import type { ChoiceKey } from "./choice-card";
|
||||
import type { ConversationFocus } from "./tool-service";
|
||||
|
||||
export type TurnQuestionKind = "choice" | "collect_spoken" | "reverse_verify";
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
parseWindowScanTransitions,
|
||||
type WindowScanTransition,
|
||||
} from "./refinement-packet";
|
||||
import { asRecord } from "@/lib/guards";
|
||||
|
||||
export type WindowScan = Readonly<{
|
||||
scanned: boolean;
|
||||
@@ -48,12 +49,6 @@ export type InternalVargaObservation = Readonly<{
|
||||
ask_theme: "relationship_style" | "career_style" | "home_change" | "education_style" | "family_event" | "finance_change" | "health_pressure" | null;
|
||||
}>;
|
||||
|
||||
function asRecord(value: unknown): Readonly<Record<string, unknown>> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Readonly<Record<string, unknown>>
|
||||
: null;
|
||||
}
|
||||
|
||||
function asCount(value: unknown): number | null {
|
||||
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
CHOICE_MODE,
|
||||
CHOICE_STOP_LABEL,
|
||||
CHOICE_STOP_MESSAGE,
|
||||
type ChoiceKey,
|
||||
type RectificationChoiceFrame,
|
||||
} from "./choice-card.ts";
|
||||
import type { ChoiceKey } from "./choice-card.ts";
|
||||
import { WIDEN_WINDOW_KIND, WIDEN_WINDOW_INTENT } from "./block-scan.ts";
|
||||
import {
|
||||
MAX_DECLARED_RADIUS_MINUTES,
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
verificationMarkdownFromUnknown,
|
||||
type RangeDeliveryProjection,
|
||||
} from "./rectification-agentic/v9/divergence-panel.ts";
|
||||
import { finiteNumberOrNull as finiteNumber } from "@/lib/guards";
|
||||
|
||||
import { parseProductDelivery } from "./rectification-agentic/v9/product-delivery.ts";
|
||||
|
||||
@@ -134,10 +135,6 @@ function text(value: unknown): string | null {
|
||||
return typeof value === "string" ? value : null;
|
||||
}
|
||||
|
||||
function finiteNumber(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
const deniedHouseTable = /lon|latitude|longitude|degree|score|fingerprint/i;
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ import {
|
||||
shouldContinueAfterStructuredChoice,
|
||||
type ChoiceOptionId,
|
||||
} from "@/lib/rectification-agentic/v9/choice-action";
|
||||
import { isPersistedFocusId, type ChoiceKey, type RectificationChoiceCard as ChoiceCardModel } from "@/lib/rectification-agentic/v9/choice-card";
|
||||
import { isPersistedFocusId, type RectificationChoiceCard as ChoiceCardModel } from "@/lib/rectification-agentic/v9/choice-card";
|
||||
import type { ChoiceKey } from "@/lib/rectification-agentic/v9/choice-card";
|
||||
import {
|
||||
appendUnseenAssistantTurns,
|
||||
applySnapshotTurnsToMessages,
|
||||
|
||||
@@ -5,54 +5,27 @@
|
||||
* never guesses "resume vs create" from session lists or message counts.
|
||||
*/
|
||||
|
||||
export type RectificationEntrySummary = Readonly<{
|
||||
hasResumableCase: boolean;
|
||||
hasTerminalCaseWithTime: boolean;
|
||||
latestResumable: Readonly<{
|
||||
caseId: string;
|
||||
status: string;
|
||||
lastActivityAt: string;
|
||||
}> | null;
|
||||
latestTerminal: Readonly<{
|
||||
caseId: string;
|
||||
status: string;
|
||||
hasUsableTime: boolean;
|
||||
}> | null;
|
||||
}>;
|
||||
import {
|
||||
RESUMABLE_CASE_STATUSES,
|
||||
TERMINAL_CASE_STATUSES,
|
||||
type OpenRectificationCaseResponseWire,
|
||||
type OpenRectificationDisposition,
|
||||
type RectificationEntrySummaryWire,
|
||||
} from "@/lib/contracts/rectification-entry";
|
||||
|
||||
export type OpenRectificationDisposition = "created" | "resumed" | "readonly";
|
||||
export type { OpenRectificationDisposition };
|
||||
export type RectificationEntrySummary = RectificationEntrySummaryWire;
|
||||
export type OpenRectificationCaseResponse = OpenRectificationCaseResponseWire;
|
||||
|
||||
export type OpenRectificationCaseResponse = Readonly<{
|
||||
disposition: OpenRectificationDisposition;
|
||||
caseId: string;
|
||||
sessionId: string;
|
||||
status: string;
|
||||
shouldStartOpening: boolean;
|
||||
skillVersion: string;
|
||||
}>;
|
||||
|
||||
const RESUMABLE_STATUSES = new Set([
|
||||
"draft",
|
||||
"collecting_evidence",
|
||||
"candidate_ready",
|
||||
"candidate_accepted",
|
||||
"needs_rebaseline",
|
||||
"paused",
|
||||
]);
|
||||
|
||||
const TERMINAL_STATUSES = new Set([
|
||||
"confirmed",
|
||||
"closed",
|
||||
"abandoned",
|
||||
"superseded",
|
||||
]);
|
||||
const resumableStatuses = new Set<string>(RESUMABLE_CASE_STATUSES);
|
||||
const terminalStatuses = new Set<string>(TERMINAL_CASE_STATUSES);
|
||||
|
||||
export function isResumableRectificationStatus(status: string): boolean {
|
||||
return RESUMABLE_STATUSES.has(status);
|
||||
return resumableStatuses.has(status);
|
||||
}
|
||||
|
||||
export function isTerminalRectificationStatus(status: string): boolean {
|
||||
return TERMINAL_STATUSES.has(status);
|
||||
return terminalStatuses.has(status);
|
||||
}
|
||||
|
||||
/* `GET /api/rectification/cases/entry-summary` answers with the camelCase
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "server-only";
|
||||
import { asRecord as record } from "@/lib/guards";
|
||||
|
||||
import { resolveSessionLanguageModel } from "./model-catalog";
|
||||
import { buildReplyQualitySnapshot } from "./reply-quality-snapshot";
|
||||
@@ -15,10 +16,6 @@ export type ReplyQualityService = {
|
||||
rpc(name: string, args: Record<string, unknown>): PromiseLike<{ data: unknown; error: { message?: string } | null }>;
|
||||
};
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
export type ReplyQualityCaptureResult = "saved" | "skipped" | "failed";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { resolveServerOwnedChartBirth } from "./chart-birth-truth.ts";
|
||||
import { asRecord as record } from "@/lib/guards";
|
||||
|
||||
/**
|
||||
* Report ↔ birth-data binding for the report cover (TASK-birth-sky-followup-20260928,
|
||||
@@ -19,12 +20,6 @@ export const REPORT_BIRTH_FINGERPRINT_KEY = "report_birth_fingerprint";
|
||||
|
||||
const FINGERPRINT_PATTERN = /^[0-9a-f]{32}$/;
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
/** Same algorithm and same input (the stored row) as the chart page's `profileFingerprint`. */
|
||||
export function subjectBirthFingerprint(storedRow: unknown): string {
|
||||
return resolveServerOwnedChartBirth(storedRow).fingerprint;
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import { reportFactTablesSchema } from "./report-fact-table-schema";
|
||||
import type { ClaimStatus } from "./personal-report-contract";
|
||||
|
||||
export type ClaimStatus =
|
||||
| "multi_system_consensus"
|
||||
| "single_system_inference"
|
||||
| "parameter_sensitive"
|
||||
| "unclosed_divisional_chart"
|
||||
| "user_history_verification_required"
|
||||
| "blocked";
|
||||
export type { ClaimStatus };
|
||||
|
||||
export type EvidenceRefStatus = "verified" | "partial" | "blocked";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { findForbiddenContent } from "./personal-report-contract";
|
||||
import { PLANET_ZH, SIGN_ORDER, SIGN_ZH, type PlanetName, type SignName } from "./chart-view-labels";
|
||||
import { recordOrEmpty as record } from "@/lib/guards";
|
||||
|
||||
export const record = (value: unknown): Record<string, unknown> => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
export const at = (value: unknown, path: string): unknown => path.split(".").reduce((next, key) => record(next)[key], value);
|
||||
export const entries = (value: unknown) => Object.entries(record(value)).filter(([key]) => /^[A-Za-z0-9_-]+$/.test(key));
|
||||
export const list = (value: unknown): unknown[] => Array.isArray(value) ? value : [];
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { reportFactTablesSchema, type ReportFactTable, type ReportFactSubtable } from "./report-fact-table-schema";
|
||||
import { SIGN_ORDER, SIGN_ZH } from "./chart-view-labels";
|
||||
import { containsHan, type ReportLanguage } from "./report-language";
|
||||
import { at, date, decimal, entries, isBlocked, list, planet, planetEn, readable, record, sign, signEn, yesNo } from "./report-fact-table-values";
|
||||
import { recordOrEmpty as record } from "./guards";
|
||||
import { at, date, decimal, entries, isBlocked, list, planet, planetEn, readable, sign, signEn, yesNo } from "./report-fact-table-values";
|
||||
|
||||
const SOURCES = [
|
||||
["vimshottari", "Vimshottari 主运", "timing_and_predictive_systems.dasha", "dasha", ["主运", "起", "止", "年数", "当前"]],
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import type { ReportLanguage } from "./report-language";
|
||||
import { isRecord } from "@/lib/guards";
|
||||
|
||||
export const REPORT_DOCUMENT_KINDS = [
|
||||
"chat_export",
|
||||
@@ -995,6 +996,3 @@ function readRecord(value: unknown): Record<string, unknown> | null {
|
||||
return isRecord(value) ? value : null;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
win32,
|
||||
} from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { isRecord } from "@/lib/guards";
|
||||
|
||||
export type SkillPackageStatus = "active" | "deprecated" | "blocked";
|
||||
|
||||
@@ -84,10 +85,6 @@ function fail(message: string): never {
|
||||
throw new SkillPackageRegistryError(message);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function assertString(
|
||||
value: unknown,
|
||||
field: keyof SkillPackageIdentity,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
globalBirthProfileFromAccountRow,
|
||||
type AccountBirthRow,
|
||||
} from "./server-owned-birth-profile.ts";
|
||||
import { asRecord as recordOf } from "@/lib/guards";
|
||||
|
||||
export const consultationSubjectErrorCodes = [
|
||||
"subject_not_found",
|
||||
@@ -73,12 +74,6 @@ export function subjectIdFromQuery(value: unknown): string {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function recordOf(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
function text(value: unknown): string {
|
||||
return typeof value === "string" ? value.trim().slice(0, 80) : "";
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { sha256Hex } from "../hash";
|
||||
|
||||
const CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
|
||||
@@ -7,7 +8,7 @@ export function normalizeRedeemCode(code: string) {
|
||||
}
|
||||
|
||||
export function hashRedeemCode(code: string) {
|
||||
return createHash("sha256").update(code, "utf8").digest("hex");
|
||||
return sha256Hex(code);
|
||||
}
|
||||
|
||||
export function generateRedeemCode() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { consultationEvidenceCategoryValues, createConsultationPlan, type Consul
|
||||
import { consultationThemeValues, projectConsultationWorkflowRequest } from "../lib/consultation-workflow-request.ts";
|
||||
import { AYANAMSA_VALUES, DEFAULT_AYANAMSA } from "../lib/ayanamsa.ts";
|
||||
import { applyUnreliableVargasToWorkflowContext } from "../lib/consultation-birth-time-mode.ts";
|
||||
import { recordOrEmpty as record } from "@/lib/guards";
|
||||
|
||||
export const consultationInputSchema = z.object({
|
||||
year: z.number().int().min(1900).max(2100),
|
||||
@@ -58,10 +59,6 @@ export const consultationWorkflowResponseSchema = z.object({
|
||||
}).passthrough().optional(),
|
||||
}).passthrough();
|
||||
|
||||
function record(value: unknown): JsonRecord {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value as JsonRecord : {};
|
||||
}
|
||||
|
||||
function rectificationBoundaryFromGate(rectification: JsonRecord): string {
|
||||
const accuracy = String(rectification.effective_accuracy || "");
|
||||
const lagnaSensitive = record(rectification.lagna_boundary).is_sensitive === true;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { completeDynamicScoreTransition } from "../src/lib/birth-time-dynamic-transitions.ts";
|
||||
import type { CandidateResult } from "../src/lib/birth-time-evidence.ts";
|
||||
import type { LegacyCandidateResult } from "../src/lib/birth-time-evidence.ts";
|
||||
import { dynamicCase } from "./birth-time-dynamic-persistence-fixture.ts";
|
||||
|
||||
const narrowedLowCandidate: CandidateResult = {
|
||||
const narrowedLowCandidate: LegacyCandidateResult = {
|
||||
resultId: "615499c9-f4da-4da0-a8bd-da26b2b8477f",
|
||||
confidence: "low",
|
||||
canApply: false,
|
||||
|
||||
@@ -4,7 +4,7 @@ import test from "node:test";
|
||||
import {
|
||||
completeDynamicScoreTransition,
|
||||
} from "../src/lib/birth-time-dynamic-transitions.ts";
|
||||
import type { CandidateResult } from "../src/lib/birth-time-evidence.ts";
|
||||
import type { LegacyCandidateResult } from "../src/lib/birth-time-evidence.ts";
|
||||
import type { ServerChoiceEvidence } from "../src/lib/birth-time-dynamic-choice-internal.ts";
|
||||
import { createBirthTimeJourneyService } from "../src/lib/birth-time-journey-service.ts";
|
||||
import {
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { memoryStore } from "./birth-time-journey-memory-store.ts";
|
||||
import { dynamicJobStore } from "./birth-time-dynamic-job-memory-store.ts";
|
||||
|
||||
const lowCandidate: CandidateResult = {
|
||||
const lowCandidate: LegacyCandidateResult = {
|
||||
resultId: "11111111-1111-4111-8111-111111111111",
|
||||
confidence: "low",
|
||||
canApply: false,
|
||||
@@ -33,7 +33,7 @@ const lowCandidate: CandidateResult = {
|
||||
const actionId = "ab2d936b-5ce7-45d8-a0fb-33f48f960f36";
|
||||
|
||||
function freshDynamicCase(
|
||||
candidateResult: CandidateResult | null = null,
|
||||
candidateResult: LegacyCandidateResult | null = null,
|
||||
priorEvidence: readonly ServerChoiceEvidence[] = [],
|
||||
) {
|
||||
const stored = dynamicCase();
|
||||
@@ -67,8 +67,8 @@ function freshDynamicCase(
|
||||
}
|
||||
|
||||
function scoringFlow(input: {
|
||||
readonly candidate?: CandidateResult;
|
||||
readonly initialCandidate?: CandidateResult | null;
|
||||
readonly candidate?: LegacyCandidateResult;
|
||||
readonly initialCandidate?: LegacyCandidateResult | null;
|
||||
readonly priorEvidence?: readonly ServerChoiceEvidence[];
|
||||
readonly failOnce?: boolean;
|
||||
readonly emptyNarrowedRange?: boolean;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { decideDynamicStop } from "../src/lib/birth-time-dynamic-stop-policy.ts";
|
||||
import type { CandidateResult } from "../src/lib/birth-time-evidence.ts";
|
||||
import type { LegacyCandidateResult } from "../src/lib/birth-time-evidence.ts";
|
||||
|
||||
const lowCandidate: CandidateResult = {
|
||||
const lowCandidate: LegacyCandidateResult = {
|
||||
resultId: "11111111-1111-4111-8111-111111111111",
|
||||
confidence: "low",
|
||||
canApply: false,
|
||||
@@ -18,7 +18,7 @@ const lowCandidate: CandidateResult = {
|
||||
algorithmVersion: "birth-time-choice-scoring-v2",
|
||||
};
|
||||
|
||||
const mediumCandidate: CandidateResult = {
|
||||
const mediumCandidate: LegacyCandidateResult = {
|
||||
...lowCandidate,
|
||||
resultId: "22222222-2222-4222-8222-222222222222",
|
||||
confidence: "medium",
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
parseGuideQuestionOutput,
|
||||
renderQuestionVariant,
|
||||
} from "../src/lib/birth-time-guide-agent.ts";
|
||||
import { evidenceDomains, type QuestionSpec } from "../src/lib/birth-time-question-planner.ts";
|
||||
import { legacyGuidedEvidenceDomains, type QuestionSpec } from "../src/lib/birth-time-question-planner.ts";
|
||||
import { draftEvidenceStructureTool, getBirthTimeGuideAgent } from "../src/mastra/index.ts";
|
||||
import type { ResolvedLanguageModel } from "../src/mastra/model.ts";
|
||||
|
||||
@@ -174,14 +174,14 @@ test("server renders every approved variant from its QuestionSpec domain and pre
|
||||
finance: /收入|资产|负债|资源渠道/,
|
||||
health_pressure: /健康压力|生活压力/,
|
||||
} as const;
|
||||
for (const domain of evidenceDomains) {
|
||||
for (const domain of legacyGuidedEvidenceDomains) {
|
||||
for (const variant of guideQuestionVariants) {
|
||||
const copy = renderQuestionVariant(question(domain), variant);
|
||||
assert.equal((copy.match(/[??]/g) ?? []).length, 1);
|
||||
assert.ok(copy.length <= 120);
|
||||
assert.match(copy, semanticMarkers[domain]);
|
||||
assert.doesNotMatch(copy, /哪一天|几号|具体日期|候选|支持|更符合|更接近|出生分钟/);
|
||||
for (const otherDomain of evidenceDomains.filter((item) => item !== domain)) {
|
||||
for (const otherDomain of legacyGuidedEvidenceDomains.filter((item) => item !== domain)) {
|
||||
assert.doesNotMatch(copy, semanticMarkers[otherDomain]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { candidateResultSchema } from "../src/lib/birth-time-journey.ts";
|
||||
import { completeScoreTransition } from "../src/lib/birth-time-journey-score-transition.ts";
|
||||
import type { CandidateResult } from "../src/lib/birth-time-evidence.ts";
|
||||
import type { LegacyCandidateResult } from "../src/lib/birth-time-evidence.ts";
|
||||
import type { LegacyStoredRectificationCase as StoredRectificationCase } from "../src/lib/birth-time-journey-service.ts";
|
||||
import {
|
||||
existingCareerEvent,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
const scoreJobId = "c70ea014-f8b4-41f2-9305-e4ae60c0d4d1";
|
||||
|
||||
function candidate(
|
||||
confidence: CandidateResult["confidence"],
|
||||
confidence: LegacyCandidateResult["confidence"],
|
||||
resultId: string,
|
||||
) {
|
||||
const winningSegment = confidence === "high"
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
LegacyBirthTimeJourneyEngine,
|
||||
LegacyStoredRectificationCase,
|
||||
} from "../src/lib/birth-time-journey-service.ts";
|
||||
import type { EvidenceDomain } from "../src/lib/birth-time-question-planner.ts";
|
||||
import type { LegacyGuidedEvidenceDomain } from "../src/lib/birth-time-question-planner.ts";
|
||||
import {
|
||||
journeyCaseId,
|
||||
memoryStore,
|
||||
@@ -83,7 +83,7 @@ export function preserveLegacyResumeStore(store: BirthTimeJourneyStore): BirthTi
|
||||
|
||||
export function evidenceQuestion(
|
||||
phase: "baseline" | "adaptive",
|
||||
domain: EvidenceDomain,
|
||||
domain: LegacyGuidedEvidenceDomain,
|
||||
round: number,
|
||||
) {
|
||||
return {
|
||||
@@ -129,9 +129,9 @@ export const lowCandidate = candidateResultSchema.parse({
|
||||
type GuidedCaseInput = {
|
||||
readonly version?: number;
|
||||
readonly phase?: "baseline" | "adaptive";
|
||||
readonly domain?: EvidenceDomain;
|
||||
readonly domain?: LegacyGuidedEvidenceDomain;
|
||||
readonly adaptiveRound?: number;
|
||||
readonly askedDomains?: readonly EvidenceDomain[];
|
||||
readonly askedDomains?: readonly LegacyGuidedEvidenceDomain[];
|
||||
readonly lifeEvents?: readonly ReturnType<typeof lifeEventSchema.parse>[];
|
||||
readonly candidateResult?: ReturnType<typeof candidateResultSchema.parse> | null;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
deriveNextAction,
|
||||
projectJourneyTurn,
|
||||
} from "../src/lib/birth-time-journey-turn.ts";
|
||||
import type { CandidateResult } from "../src/lib/birth-time-evidence.ts";
|
||||
import type { LegacyCandidateResult } from "../src/lib/birth-time-evidence.ts";
|
||||
import type { QuestionSpec } from "../src/lib/birth-time-question-planner.ts";
|
||||
|
||||
function question(domain: QuestionSpec["domain"]): QuestionSpec {
|
||||
@@ -20,7 +20,7 @@ function question(domain: QuestionSpec["domain"]): QuestionSpec {
|
||||
};
|
||||
}
|
||||
|
||||
const lowResult: CandidateResult = {
|
||||
const lowResult: LegacyCandidateResult = {
|
||||
resultId: "1d8ee348-61a3-433d-8907-ff6d281b9992",
|
||||
confidence: "low",
|
||||
canApply: false,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user