Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5c139367d |
@@ -1,5 +1,11 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-11-09 — 普通对话数据卡按本题路由裁层,每轮少读约三成
|
||||
|
||||
- 基础段只带本题路由点名的层:大运只留 Vimshottari / Narayana;Yoga 只在带瑜伽的领域上卡;年运块只在年运问题上卡。其余层仍可用「多看一眼」查卡取到。
|
||||
- 3 张公开盘 × 12 个领域实测:模型每轮可见的答题契约 + 数据卡平均 46,664 → 32,153 字符(−31.1%),最大 54,532 → 43,419(BUG-1813)。
|
||||
- Yoga 行的行星元数据压成星名(事实仍在基础段的行星表、功能吉凶表与宫位行);回答规则与计算不变。Skill 版本不变;没有数据库变更。
|
||||
|
||||
## 2026-11-07 — 占星师第二次裁定:Drig 用 Narayana 年数、Ashtottari 只看罗睺条件、Sthira 两种方法分开保存
|
||||
|
||||
- Drig 每个星座的年数改为与本站 Narayana 同一个函数(双主星择强、入旺落陷调整都算),依据 PVR 21.2(5);Sri Aurobindo 的 Scorpio Drig 1906–1913 作为回归案例(BUG-1678)。
|
||||
|
||||
+18
-1
@@ -24415,4 +24415,21 @@
|
||||
- 验证:新测试 `tests/test_rectification_reseal_round7_bug1809.py`:新标签封存 / 偏移 / 现行数字、可比性策略与领先计数、旧标签数字不变(带原值 / 新值 / 原因)。之前没有测试按标签钉封存结果,所以没有要改写的旧测试。`test_active_rectification_api.py::test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow` 已知失败(见 BUG-1720),与本单无关。
|
||||
- 防复发:上述测试;以后重封必须新建标签,不覆盖旧标签。
|
||||
- 关联记录:BUG-1720、BUG-1759、BUG-1784、BUG-1801、BUG-1644
|
||||
- 修复版本:本地分支 `codex/rect-reseal-r7-1109`
|
||||
- 修复版本:本地分支 `codex/rect-reseal-r7-1109`
|
||||
|
||||
## BUG-1813 | 数据卡把本题路由不读的层也每轮送达:26 个大运系统、全部 Yoga 元数据、年运块
|
||||
|
||||
- 状态:fixed-pending-verify(分支 `codex/token-card-compress-20261109`)
|
||||
- 首次发现 / 最近更新:2026-11-09(产品 #170 省 token 单)/ 2026-11-09(修复)
|
||||
- 影响面:`frontend/src/lib/consultation-evidence-card.ts` `baseExtras()`;所有普通对话轮次读取的答题契约 + 数据卡(`frontend/src/mastra/consultation-tools.ts` `toModelEvidenceView`)。
|
||||
- 现象:3 张公开盘 × 12 个领域实测,模型每轮可见的数据卡平均 46,664 字符(最小 41,705、最大 54,532)。单卡最大块是 `base.natal.yogas_detail`(7,918)、`base.timing.dasha_systems`(7,794,26 个系统里 20+ 个是 `applicable:false` 的说明行)与 `base.timing.annual_brief`(2,270,每个领域都带)。
|
||||
- 触发条件:任何带数据卡的咨询轮。
|
||||
- 根因:2026-11-07「回答模型拿到完整数据」把可上卡的层扩到全部大运系统、全部 Yoga 行与年运块;基础段构建 `baseExtras()` 不知道当前领域的路由,只要引擎算出了就无条件上卡。`EVIDENCE_CARD_SPECS[domain].route` 已写明本题要读的技法,却没有被用来裁基础段。
|
||||
- 修复:`baseExtras()` 接收领域,按 `route` 过滤大运系统(只留 Vimshottari / Narayana);Yoga 只在 `spec.yogas` 的领域上卡;年运块只在 `spec.annualHouses`(annual)上卡;Yoga 行的 `planets` 从行星元数据对象压成星名数组,事实仍留在 `base.natal.planets` / `functional_benefic_malefic` / 宫位行。
|
||||
- 验证:
|
||||
- 新测试 `frontend/tests/consult-card-size-170.test.ts`:平均 ≤ 33,000(实测 32,153,较基线 −31.1%)、最大 ≤ 44,000(实测 43,419);逐领域核对路由点名的 Vimshottari / Narayana / 分盘 / 宫 / KP / 行运仍在,年运块与 Yoga 只在应上的领域出现。
|
||||
- `frontend/tests/consult*.test.ts` 90 个文件 672 条:671 通过;1 条为并行负载下的时钟 flake,单独跑通过。
|
||||
- `tsc --noEmit` 0 错;改动文件 eslint 0 error。
|
||||
- 防复发:上述体积与路由层测试;以后往基础段加层时,先问 `EVIDENCE_CARD_SPECS[domain].route` 是否点名。
|
||||
- 关联记录:BUG-1160 / BUG-1161(卡体量预算)、BUG-1773(同源索引)、BUG-1793(证据行);#171(查卡多段)另记 BUG-1814
|
||||
- 修复版本:分支 `codex/token-card-compress-20261109`
|
||||
@@ -0,0 +1,53 @@
|
||||
# PROGRESS · 数据卡按本题路由裁层(#170 / BUG-1813)
|
||||
|
||||
- 基线:`origin/staging` `c032823f9`
|
||||
- 分支:`codex/token-card-compress-20261109`
|
||||
- 工作树:`.worktrees/token-card-compress-20261109`
|
||||
|
||||
## 做了什么
|
||||
|
||||
1. `frontend/src/lib/consultation-evidence-card.ts` `baseExtras()` 增加 `domain` 参数:
|
||||
- `base.timing.dasha_systems.systems` 只保留 `EVIDENCE_CARD_SPECS[domain].route` 点名的大运系统(Vimshottari / Narayana)。其余系统的 `applicable:false` 说明行不再上卡,也不再记 `base.dasha_systems.<name>` gap。
|
||||
- `base.natal.yogas_detail` 只在 `spec.yogas === true` 的领域上卡。
|
||||
- `base.timing.annual_brief` 只在 `spec.annualHouses`(annual 路由)上卡。
|
||||
2. Yoga 行的 `planets` 从 `{ 星: { owns, functional, dignity } }` 压成星名数组;行星事实仍在 `base.natal.planets`、`functional_benefic_malefic`、宫位行。
|
||||
3. 运行合同 `references/product_runtime_contract.md` 第 13 条:`Jaimini karaka` → `Jaimini karakas`(清单词扫描要求每个标识符是卡上键名;`karaka` 这个键随 chara 等系统一起离开卡面)。
|
||||
4. 新增 `frontend/tests/consult-card-size-170.test.ts`:锁平均 ≤ 33,000、最大 ≤ 44,000,并逐领域核对路由点名的层仍在、被裁的层确实不在。
|
||||
5. 改三处既有断言(均在测试注释里写「原值 / 新值 / 原因」):
|
||||
- `consult-evidence-card-full-data-20261107`:大运系统从「全部上卡」改为「只上路由点名的系统」;yoga 断言改用 `general` 卡。
|
||||
- `consult-card-calendar-narayana-20261108`:年运块断言改用 `annual_workflow` / `annual`。
|
||||
- `consult-downstream-labels-r3-20261107`:大运系统断言改为只留 Vimshottari / Narayana;yoga 断言改用 `career`。
|
||||
|
||||
## 测量(真实金样,3 张公开盘 × 12 个领域,模型可见「答题契约 + 数据卡」字符数)
|
||||
|
||||
| | 基线 `c032823f9` | 本分支 | 变化 |
|
||||
| --- | --- | --- | --- |
|
||||
| 平均 | 46,664 | 32,153 | −31.1% |
|
||||
| 最小 | 41,705 | 25,180 | −39.6% |
|
||||
| 最大 | 54,532(`barack_obama` / `general`) | 43,419(同) | −20.4% |
|
||||
| `steve_jobs` 平均 | — | 31,899 | |
|
||||
| `barack_obama` 平均 | — | 32,705 | |
|
||||
| `elizabeth_taylor` 平均 | — | 31,855 | |
|
||||
|
||||
平均降幅 31.1%,达到 #170 的「每题卡片字符数下降 30% 以上」。
|
||||
|
||||
## 自测
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 类型 | `node node_modules/typescript/bin/tsc --noEmit -p .` | 0 错 |
|
||||
| lint(改动文件) | `node node_modules/eslint/bin/eslint.js …` | 0 error / 0 warning |
|
||||
| 体积 + 路由层测试 | `node node_modules/tsx/dist/cli.mjs --test tests/consult-card-size-170.test.ts` | 2/2 通过 |
|
||||
| 全部咨询测试 | `node node_modules/tsx/dist/cli.mjs --test tests/consult*.test.ts`(90 个文件) | 672 条:671 通过,1 失败 |
|
||||
| 唯一失败 | `consult-evidence-lookup-20260927.test.ts`「a lookup does not reset the answer clock」 | 单独跑 15/15 通过;并行跑超时,判定为机器负载下的 flake,与本单无关 |
|
||||
|
||||
## 偏离与说明
|
||||
|
||||
- **盲测未跑**:本机没有模型凭据(`BLOCKED.md` 已知缺口),无法做「同一批题改前 / 改后」的盲评;本单只交付体积、字段送达与现有回归。部署到 staging 后需要产品负责人走真机与盲测。
|
||||
- 年运块从所有领域收回到 annual 路由,是 #170「只放本题路由需要的层」的直接结果;年运 / 时运以外的题目要今年细节时走查卡工具一次。
|
||||
- `frontend/tests/consult*.test.ts` 的 672 条里没有新增失败;测试总数较基线增加 2 条(新文件)。
|
||||
|
||||
## 环境缺口
|
||||
|
||||
- 无模型凭据:盲测与「准确度不下降」留待 staging 真机。
|
||||
- 本机 Node 22 / Windows;未跑前端全量 `npm test` 与 Python 快速门(按执行方分工由 Claude 另跑)。
|
||||
@@ -145,6 +145,7 @@
|
||||
|
||||
| 任务书 | 进度 | 主题 | 状态 | 落点 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `TASK-token-card-compress-20261109.md` | `PROGRESS-token-card-compress-20261109.md` | **省 token:数据卡按本题路由裁层(#170 / BUG-1813)**:基础段只留 `EVIDENCE_CARD_SPECS[domain].route` 点名的大运系统(Vimshottari / Narayana)、Yoga(只在带瑜伽的领域)与年运块(只在 annual);Yoga 行星元数据压成星名;3 张公开盘 × 12 个领域实测平均 46,664 → 32,153 字符(−31.1%)。盲测与真机待 staging | 待验收 | `codex/token-card-compress-20261109` |
|
||||
| `TASK-composer-first-glyph-clip-20261013.md` | `PROGRESS-composer-first-glyph-clip-20261013.md` | **输入框第一个字被圆角裁掉(agent 循环 issue #52)**:`Textarea` 原语自带 12px 圆角,`.composer textarea` 清了边框与左右内边距却没清圆角,浏览器按圆角裁滚动内容,第一个字左缘被裁成圆弧;只在 `.composer textarea` 加 `border-radius: 0`,原语不动。执行方 Grok。BUG-1311 | 待验收 | `codex/composer-first-glyph-clip-20261013` |
|
||||
| `TASK-composer-first-glyph-clip-fix-20261013.md` | — | **#52 第 1 次打回(只需 rebase)**:`6e6fae8d` 内容全部通过,因 staging 前进了一个纯文档提交而不能快进 | 待领取 | `codex/composer-first-glyph-clip-20261013` |
|
||||
| `TASK-consult-card-full-source-20261007.md` | `PROGRESS-consult-card-full-source-20261007.md` | **对话数据卡与报告同源 + 领域综合判断表**:事业卡只有约 7,600 字符、无自然代表星、无大运主星关系、D10 联动只能补取、补取上限 1;全 12 领域;综合判断表、参与清单(不止 D1/D9,含精度闸)、补取 3 次、资料目录均已确认;KP 不上卡、不联网、不接书籍语料;排在 sync6 与报告单之后 | 验收未通过(`35dafd1e`,见 fix-20261009) | `codex/consult-card-full-source-20261007` |
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# TASK · 省 token:数据卡按本题路由裁层(#170 / BUG-1813)
|
||||
|
||||
- 基线:`origin/staging` `c032823f9`(2026-11-09,round7 整合重封之后)
|
||||
- 执行分支:`codex/token-card-compress-20261109`
|
||||
- 关联:issue #170(`agent/排队` → 本单领取后 `agent/执行中`)、#171(查卡多段,另一分支)、BUG-1813
|
||||
|
||||
## 1. 事实与测量
|
||||
|
||||
写答案的模型每轮读「答题契约 + 数据卡」。2026-11-07 占星师方案把「完整数据」放上卡(`TASK-consult-evidence-card-v2-20260927` 之后的第四轮):全部 26 个大运系统的当前层、全部 Yoga 行连同每行的行星元数据、年运精简块、受冲账本,全部随每张卡送达。
|
||||
|
||||
在 `c032823f9` 上用真实金样(`frontend/tests/fixtures/consult-evidence-card-golden.json`,3 张公开盘 × 12 个领域)测「答题契约 + 数据卡」的模型可见字符数:
|
||||
|
||||
| 指标 | 基线 |
|
||||
| --- | --- |
|
||||
| 平均 | 46,664 |
|
||||
| 最小 | 41,705 |
|
||||
| 最大 | 54,532(`barack_obama` / `general`) |
|
||||
|
||||
单张卡里最大的块(`barack_obama` / `general`):
|
||||
|
||||
| 块 | 字符 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `base.natal.yogas_detail` | 7,918 | 全部 Yoga 行 + 每行的 `owns` / `functional` / `dignity` 元数据 |
|
||||
| `base.timing.dasha_systems` | 7,794 | 26 个大运系统,其中 20+ 个是 `applicable:false` 的说明行 |
|
||||
| `base.timing.sade_sati_timeline` | 2,695 | 近 7 年 Sade Sati 行 + 方法说明 |
|
||||
| `base.natal.affliction_evidence` | 2,765 | 受冲账本 |
|
||||
| `base.timing.transits_detail` | 2,476 | 行运 |
|
||||
| `base.timing.annual_brief` | 2,270 | 年运块,每个领域都带 |
|
||||
| `domain.argala` | 3,199 | 领域 Argala |
|
||||
| `domain.kp` | 2,007 | KP Placidus |
|
||||
|
||||
`EVIDENCE_CARD_SPECS[domain].route` 已经在每张卡上写明本题要读的技法(`D1 / D9 / D10 / … / Vimshottari / Narayana / transits / KP`)。基础段却把路由没点名的层也塞进去:非路由大运系统、年运块、yoga 元数据。
|
||||
|
||||
## 2. 根因
|
||||
|
||||
`baseExtras()` 是「整个盘一份基础段」的实现,不知道当前领域的路由;只要引擎算出了某一层,就无条件上卡。2026-11-07 的「完整数据」决定把可上卡的层扩到全部大运与年运,于是每张卡都带着本题不读的层。
|
||||
|
||||
## 3. 决策记录(产品 #170,2026-11-09)
|
||||
|
||||
1. **按本题路由裁层**:基础段只保留 `EVIDENCE_CARD_SPECS[domain].route` 点名的层。
|
||||
- 大运系统:只留路由点名的 Vimshottari / Narayana;其余 24 个系统改为查卡工具可取。
|
||||
- Yoga:只在 `spec.yogas === true` 的领域上卡(career / wealth / parents / children / general);其余领域查卡。
|
||||
- 年运块:只在 `annual` 路由上卡;其余领域查卡。
|
||||
2. **同一事实只算一次**:Yoga 行的 `planets` 从 `{ 星: { owns, functional, dignity } }` 压成星名数组;行星事实仍在 `base.natal.planets`、`functional_benefic_malefic` 与领域宫位行里。
|
||||
3. 这是一次**推翻 2026-11-07「回答模型拿到完整数据」的有限让步**:只让出「本题路由不读的层」,路由点名的层一张不少;完整数据仍在服务端回执与查卡工具里。
|
||||
4. 不改引擎、不改提示词规则、不改数据库;金样不重采。
|
||||
|
||||
## 4. 硬红线
|
||||
|
||||
1. 路由点名的层不得缺失:每条路由的 Vimshottari / Narayana / 本领域正式分盘 / 本领域宫 / KP Placidus / 行运。
|
||||
2. 不得删事实:被裁的层必须仍能从 `read-consultation-evidence` 取到,或在卡上另一处存在。
|
||||
3. 不改既有断言的语义;必须改的断言写「原值 / 新值 / 原因」三栏。
|
||||
4. 金样 `consult-evidence-card-golden.json` 不动。
|
||||
5. 不改 `scripts/**`、不改数据库、不改 `.gitea/**`。
|
||||
|
||||
## 5. 任务分解
|
||||
|
||||
| # | 任务 | 验收 |
|
||||
| --- | --- | --- |
|
||||
| T1 | `baseExtras()` 接收领域;大运系统按 `spec.route` 过滤;Yoga 按 `spec.yogas` 上卡;年运块按 `spec.annualHouses` 上卡 | 36 张卡测量通过;每条路由的双大运轨仍在 |
|
||||
| T2 | Yoga 行 `planets` 压成星名数组 | 行的星名与引擎 `yoga_conditions.rows[].planets` 的键一致 |
|
||||
| T3 | 体积回归测试 `consult-card-size-170.test.ts` | 平均 ≤ 33,000、最大 ≤ 44,000;路由层逐项存在 |
|
||||
| T4 | 改既有断言(两处大运、一处 yoga、一处年运) | 三栏说明齐全;`consult*.test.ts` 全量通过 |
|
||||
| T5 | 文案:运行合同里 `Jaimini karaka` 改为 `Jaimini karakas`(清单词扫描要求是卡上键名) | `consult-affliction-reading-20261001` 通过 |
|
||||
|
||||
## 6. 让步顺序
|
||||
|
||||
1. 先保「路由点名的层一张不少」与「事实可查」。
|
||||
2. 再保 30% 体积目标。
|
||||
3. 最后才考虑改断言措辞;不得为过测试删字段。
|
||||
|
||||
## 7. 开工前置
|
||||
|
||||
```powershell
|
||||
git fetch origin --prune
|
||||
git worktree add -b codex/token-card-compress-20261109 .worktrees/token-card-compress-20261109 origin/staging
|
||||
```
|
||||
|
||||
前端自测(Windows,工作树内):
|
||||
|
||||
```powershell
|
||||
frontend\node_modules\.bin\tsc --noEmit -p frontend
|
||||
frontend\node_modules\eslint\bin\eslint.js <改动文件>
|
||||
node frontend\node_modules\tsx\dist\cli.mjs --test frontend/tests/consult-card-size-170.test.ts
|
||||
node frontend\node_modules\tsx\dist\cli.mjs --test (全部 frontend/tests/consult*.test.ts)
|
||||
```
|
||||
|
||||
盲测(同一批题、改前 / 改后各跑一次、人工评分)需要模型凭据;本机没有,按 `BLOCKED.md` 记为环境缺口,部署后由产品负责人在 staging 走真机。
|
||||
|
||||
## 8. BUG 编号
|
||||
|
||||
开工时 `docs/BUG_HISTORY.md` 最大号是 BUG-1809(本分支);#170 标题预留 BUG-1813,本单写 BUG-1813。
|
||||
@@ -1880,7 +1880,18 @@ const DASHA_ROW_KEYS = [
|
||||
"continuity_issue",
|
||||
] as const;
|
||||
|
||||
function baseExtras(context: unknown, base: EvidenceCard["base"], gaps: string[]) {
|
||||
function baseExtras(
|
||||
context: unknown,
|
||||
base: EvidenceCard["base"],
|
||||
gaps: string[],
|
||||
domain: ConsultationDomain,
|
||||
) {
|
||||
// #170: the base section carries only the layers this domain's route names.
|
||||
// A route does not name a dasha system, a yoga family or the annual Tajika
|
||||
// block, so the card leaves it to the one-shot lookup instead of shipping it
|
||||
// on every question.
|
||||
const spec = EVIDENCE_CARD_SPECS[domain];
|
||||
const routeSystems = new Set(spec.route.map((item) => item.toLowerCase()));
|
||||
const layers = nativeLayers(context);
|
||||
const localLayers = record(record(context).local_layers);
|
||||
const parameters = readyLayer(layers, "card_parameters");
|
||||
@@ -1897,6 +1908,9 @@ function baseExtras(context: unknown, base: EvidenceCard["base"], gaps: string[]
|
||||
for (const raw of dashas.rows) {
|
||||
const row = record(raw);
|
||||
if (typeof row.system !== "string") continue;
|
||||
// #170: other systems stay in the lookup; the card keeps the ones this
|
||||
// route reads (every route names Vimshottari and Narayana).
|
||||
if (!routeSystems.has(row.system.toLowerCase())) continue;
|
||||
if (row.status === NOT_AVAILABLE_YET && row.applicable === undefined) {
|
||||
gaps.push(`base.dasha_systems.${row.system}:${NOT_AVAILABLE_YET}`);
|
||||
continue;
|
||||
@@ -1960,37 +1974,49 @@ function baseExtras(context: unknown, base: EvidenceCard["base"], gaps: string[]
|
||||
} else {
|
||||
gaps.push("base.sade_sati_timeline");
|
||||
}
|
||||
const annualPack = record(layers.annual_tajika);
|
||||
if (annualPack.status === "parameter_sensitive" || annualPack.status === "partial_verified") {
|
||||
const reference = String(record(layers.transit_detail).reference_date ?? "").slice(0, 10);
|
||||
const mudda = Array.isArray(annualPack.mudda_dasha) ? annualPack.mudda_dasha.map(record) : [];
|
||||
const at = reference ? mudda.findIndex((row) => String(row.start) <= reference && reference < String(row.end)) : -1;
|
||||
const muddaNow = at >= 0 ? mudda.slice(at, at + 2) : mudda.slice(0, 2);
|
||||
base.timing.annual_brief = {
|
||||
...pickKeys(annualPack, ["status", "varsha_year", "annual_lagna", "varshesha", "varshesha_basis", "muntha", "tajika_aspects", "tajika_aspects_status", "boundary",
|
||||
// BUG-1722: the Mudda rows are a view clipped to this return -> next return (her Q2).
|
||||
"mudda_view",
|
||||
// BUG-1731 (her Q2): 「今年」 is the calendar year, which spans two Tajika years split at the
|
||||
// return; both years' coverage and the split date travel (the engine computed them, the card dropped them).
|
||||
"calendar_year"]),
|
||||
mudda_running_and_next: muddaNow.map((row) => copy(row)).filter((row): row is Json => row !== undefined),
|
||||
};
|
||||
} else {
|
||||
gaps.push(`base.annual_brief${typeof annualPack.reason === "string" ? `:${annualPack.reason}` : ""}`);
|
||||
// #170: the annual Tajika block rides only on the annual route; other
|
||||
// questions read it through the lookup when they ask about this year.
|
||||
if (spec.annualHouses) {
|
||||
const annualPack = record(layers.annual_tajika);
|
||||
if (annualPack.status === "parameter_sensitive" || annualPack.status === "partial_verified") {
|
||||
const reference = String(record(layers.transit_detail).reference_date ?? "").slice(0, 10);
|
||||
const mudda = Array.isArray(annualPack.mudda_dasha) ? annualPack.mudda_dasha.map(record) : [];
|
||||
const at = reference ? mudda.findIndex((row) => String(row.start) <= reference && reference < String(row.end)) : -1;
|
||||
const muddaNow = at >= 0 ? mudda.slice(at, at + 2) : mudda.slice(0, 2);
|
||||
base.timing.annual_brief = {
|
||||
...pickKeys(annualPack, ["status", "varsha_year", "annual_lagna", "varshesha", "varshesha_basis", "muntha", "tajika_aspects", "tajika_aspects_status", "boundary",
|
||||
// BUG-1722: the Mudda rows are a view clipped to this return -> next return (her Q2).
|
||||
"mudda_view",
|
||||
// BUG-1731 (her Q2): 「今年」 is the calendar year, which spans two Tajika years split at the
|
||||
// return; both years' coverage and the split date travel (the engine computed them, the card dropped them).
|
||||
"calendar_year"]),
|
||||
mudda_running_and_next: muddaNow.map((row) => copy(row)).filter((row): row is Json => row !== undefined),
|
||||
};
|
||||
} else {
|
||||
gaps.push(`base.annual_brief${typeof annualPack.reason === "string" ? `:${annualPack.reason}` : ""}`);
|
||||
}
|
||||
}
|
||||
const bookRules = readyLayer(layers, "book_rules");
|
||||
if (bookRules) base.book_rules = pickKeys(bookRules, ["status", "routes", "runtime_packets", "route_reference_rules"]) as JsonRecord;
|
||||
const yogas = readyLayer(layers, "yoga_conditions");
|
||||
const yogas = spec.yogas ? readyLayer(layers, "yoga_conditions") : null;
|
||||
if (yogas && Array.isArray(yogas.rows)) {
|
||||
// 产品 2026-11-07 同步占星师 Yoga 规则批次: claim_kind (structural_observation
|
||||
// is not a named yoga) and the executed formation checks, copied verbatim.
|
||||
const rows = yogas.rows.map((row) => pickKeys(row, [
|
||||
"name", "category", "status", "claim_kind", "claim_status", "condition", "conditions_met", "reductions",
|
||||
"formation", "formation_checks", "planets", "running_dasha_levels",
|
||||
// 占星师 2026-11-08 第六轮 R3: Neecha Bhanga witnesses with reference / relative_house /
|
||||
// self_reference; `self_reference_only` rows hold by the literal source branch only.
|
||||
"formation_evidence",
|
||||
]));
|
||||
// #170: the per-row planet block (owns / functional / dignity) repeated the
|
||||
// base planets table; the row keeps the names, the facts stay in base.natal.
|
||||
const rows = yogas.rows.map((row) => {
|
||||
const picked = pickKeys(row, [
|
||||
"name", "category", "status", "claim_kind", "claim_status", "condition", "conditions_met", "reductions",
|
||||
"formation", "formation_checks", "planets", "running_dasha_levels",
|
||||
// 占星师 2026-11-08 第六轮 R3: Neecha Bhanga witnesses with reference / relative_house /
|
||||
// self_reference; `self_reference_only` rows hold by the literal source branch only.
|
||||
"formation_evidence",
|
||||
]);
|
||||
const names = Object.keys(record(picked.planets));
|
||||
if (names.length) picked.planets = names;
|
||||
else delete picked.planets;
|
||||
return picked;
|
||||
});
|
||||
if (rows.some((row) => row.condition === NOT_AVAILABLE_YET)) gaps.push(`base.yogas_detail.condition:${NOT_AVAILABLE_YET}`);
|
||||
// 占星师第三轮审查 2026-11-07: rules the engine could not evaluate are listed
|
||||
// apart (never as "not formed"); absent when every rule was evaluated.
|
||||
@@ -2002,7 +2028,7 @@ function baseExtras(context: unknown, base: EvidenceCard["base"], gaps: string[]
|
||||
rows,
|
||||
...(notEvaluated.length ? { not_evaluated: notEvaluated, ...pickKeys(yogas, ["not_evaluated_rule", "rule_engine_status"]) } : {}),
|
||||
};
|
||||
} else {
|
||||
} else if (spec.yogas) {
|
||||
gaps.push("base.yogas_detail");
|
||||
}
|
||||
const strength = readyLayer(layers, "strength_detail");
|
||||
@@ -2743,7 +2769,7 @@ export function buildEvidenceCard(
|
||||
natal: baseNatal(claimEvidence(first.packet, "natal_foundation"), gaps),
|
||||
timing: baseTiming(claimEvidence(first.packet, "timing"), gaps, horizon),
|
||||
};
|
||||
baseExtras(first.context, base, gaps);
|
||||
baseExtras(first.context, base, gaps, first.domain);
|
||||
const domainSections: EvidenceCard["domain_sections"] = {};
|
||||
const domains: ConsultationDomain[] = [];
|
||||
const planFor = (domain: ConsultationDomain): EvidenceCardVargaPlan | undefined => {
|
||||
@@ -2769,7 +2795,7 @@ export function buildEvidenceCard(
|
||||
natal: baseNatal(claimEvidence(source.packet, "natal_foundation"), ownGaps),
|
||||
timing: baseTiming(claimEvidence(source.packet, "timing"), ownGaps, horizon),
|
||||
};
|
||||
baseExtras(source.context, candidate, ownGaps);
|
||||
baseExtras(source.context, candidate, ownGaps, source.domain);
|
||||
if (JSON.stringify(candidate) !== JSON.stringify(base)) {
|
||||
own = candidate;
|
||||
gaps.push(`${source.domain}.base_differs`, ...ownGaps.map((gap) => `${source.domain}.${gap}`));
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { ConsultationDomain } from "../src/lib/consultation-domain-registry
|
||||
|
||||
type Json = Record<string, unknown>;
|
||||
const golden = JSON.parse(readFileSync(new URL("./fixtures/consult-evidence-card-golden.json", import.meta.url), "utf8")) as {
|
||||
charts: Array<{ id: string; workflow: Json; timing_workflow: Json }>;
|
||||
charts: Array<{ id: string; workflow: Json; annual_workflow: Json; timing_workflow: Json }>;
|
||||
};
|
||||
|
||||
function card(workflow: Json, domain: ConsultationDomain): EvidenceCard {
|
||||
@@ -24,8 +24,11 @@ const nativeAnnual = (workflow: Json) => ((workflow.chart as Json).consultation_
|
||||
const narayanaModule = (workflow: Json) => (((workflow.chart as Json).modules as Json).narayana_dasha as Json);
|
||||
|
||||
test("BUG-1731: the card carries the calendar year the engine computed: both Tajika years and the return that splits them", () => {
|
||||
// 原值:timing / wealth 卡也带 base.timing.annual_brief.calendar_year
|
||||
// 新值:年运块只在 annual 路由上卡(EVIDENCE_CARD_SPECS.annual.annualHouses),其余题目从查卡工具取
|
||||
// 原因:#170 省 token:数据卡按本题路由裁层
|
||||
for (const chart of golden.charts) {
|
||||
for (const [workflow, domain] of [[chart.timing_workflow, "timing"], [chart.workflow, "wealth"]] as const) {
|
||||
for (const [workflow, domain] of [[chart.annual_workflow, "annual"]] as const) {
|
||||
const engine = nativeAnnual(workflow).calendar_year as Json;
|
||||
const onCard = (card(workflow, domain).base.timing.annual_brief as Json).calendar_year as Json;
|
||||
assert.deepEqual(onCard, engine, `${chart.id} ${domain}`);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// #170 省 token:数据卡的体积预算。
|
||||
//
|
||||
// 基线 origin/staging c032823f9(2026-11-09):3 张公开盘 × 12 个领域,
|
||||
// 模型可见的「答题契约 + 数据卡」平均 46,664 字符(最小 41,705,最大 54,532,
|
||||
// 见 PROGRESS-token-card-compress-20261109.md 的测量)。
|
||||
// 本单按「只放本题路由需要的层」裁掉基础段里路由没点名的层,并把逐行重复的
|
||||
// 行星元数据压成星名,实测平均 32,153 字符(-31.1%),最大 43,419。
|
||||
// 这里锁两件事:平均值不回到 33,000 以上,单张卡不超过 44,000;
|
||||
// 路由点到的层(Vimshottari / Narayana / 本领域分盘 / 本领域宫 / KP / 行运)
|
||||
// 一张都不能少。
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { EVIDENCE_CARD_SPECS } from "../src/lib/consultation-evidence-card.ts";
|
||||
import { consultationDomainIds, type ConsultationDomain } from "../src/lib/consultation-domain-registry.ts";
|
||||
import { createConsultationRuntimeState, createConsultationTools } from "../src/mastra/consultation-tools.ts";
|
||||
|
||||
type Json = Record<string, unknown>;
|
||||
|
||||
const golden = JSON.parse(readFileSync(
|
||||
new URL("./fixtures/consult-evidence-card-golden.json", import.meta.url),
|
||||
"utf8",
|
||||
)) as { charts: Array<{ id: string; workflow: Json }> };
|
||||
|
||||
const QUESTION = "请用 Vimshottari 和 Narayana Dasha 两套运程,给出未来十年事业上的长期规划视图。";
|
||||
const REQUEST_TIME = new Date("2026-09-27T04:00:00.000Z");
|
||||
const SERVER_CHART = {
|
||||
name: "public",
|
||||
toolInput: { year: 2000, month: 1, day: 1, hour: 0, minute: 0, city: "fixture", lat: 0, lon: 0, tz: 0, ayanamsa: "raman" },
|
||||
truth: { birthTimeSource: "reported" },
|
||||
};
|
||||
|
||||
type Run = { chart: string; domain: ConsultationDomain; chars: number; view: Json };
|
||||
|
||||
async function measure(): Promise<Run[]> {
|
||||
const out: Run[] = [];
|
||||
for (const chart of golden.charts) {
|
||||
for (const domain of consultationDomainIds) {
|
||||
const state = createConsultationRuntimeState();
|
||||
const tools = createConsultationTools({
|
||||
userId: "u", sessionId: "s", requestId: `size-${domain}`, consultationMode: "verified_chart",
|
||||
serverChart: SERVER_CHART as never, state, requestTime: REQUEST_TIME,
|
||||
runWorkflow: async () => structuredClone(chart.workflow) as never,
|
||||
});
|
||||
const view = await tools["run-jyotish-consultation"].execute!(
|
||||
{ question: QUESTION, domains: [domain] } as never,
|
||||
{ writer: { custom: async () => {} } } as never,
|
||||
) as Json;
|
||||
out.push({ chart: chart.id, domain, chars: state.evidenceCardChars ?? -1, view });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
test("#170: the model-visible card stays within the compressed budget", async () => {
|
||||
const runs = await measure();
|
||||
const chars = runs.map((run) => run.chars);
|
||||
const average = chars.reduce((sum, value) => sum + value, 0) / chars.length;
|
||||
// 原值(基线 c032823f9):平均 46,664 / 新值:≤ 33,000
|
||||
// 原因:#170 只放本题路由需要的层;实测 32,153(-31.1%)
|
||||
assert.ok(average <= 33_000, `average card chars ${Math.round(average)}`);
|
||||
// 原值(基线最大 54,532)/ 新值:≤ 44,000 / 原因:同上(实测最大 43,419)
|
||||
const largest = runs.reduce((max, run) => (run.chars > max.chars ? run : max));
|
||||
assert.ok(largest.chars <= 44_000, `${largest.chart}/${largest.domain} card ${largest.chars}`);
|
||||
});
|
||||
|
||||
test("#170: every layer the route names is still on the card", async () => {
|
||||
const runs = await measure();
|
||||
for (const { chart, domain, view } of runs) {
|
||||
const where = `${chart}/${domain}`;
|
||||
const claims = (view.claim_cards as Json[]);
|
||||
const natal = (claims.find((card) => card.category === "natal_foundation")!.evidence as Json);
|
||||
const timing = (claims.find((card) => card.category === "timing")!.evidence as Json);
|
||||
const sections = (claims.find((card) => card.category === "domain")!.evidence as Json)[domain] as Json;
|
||||
const spec = EVIDENCE_CARD_SPECS[domain];
|
||||
// 双大运轨:每条路由都点名 Vimshottari 与 Narayana。
|
||||
assert.ok(timing.vimshottari, `${where} vimshottari`);
|
||||
assert.ok(timing.narayana, `${where} narayana`);
|
||||
const systems = ((timing.dasha_systems as Json).systems as Json);
|
||||
assert.ok(systems.vimshottari, `${where} dasha_systems.vimshottari`);
|
||||
assert.ok(systems.narayana, `${where} dasha_systems.narayana`);
|
||||
// 本领域的正式分盘与宫位。
|
||||
for (const varga of spec.vargas) assert.ok((sections.vargas as Json)[varga] !== undefined, `${where} ${varga}`);
|
||||
for (const house of spec.houses) assert.ok((sections.houses as Json)[String(house)] !== undefined, `${where} house ${house}`);
|
||||
// KP Placidus 与行运是 ROUTE_TIMING 的一部分。
|
||||
assert.equal((sections.kp as Json).house_system, "placidus", `${where} kp`);
|
||||
assert.ok(timing.transits_detail, `${where} transits`);
|
||||
// 年运块只在 annual 路由上卡;其余领域走查卡工具。
|
||||
if (domain === "annual") assert.ok(timing.annual_brief, `${where} annual_brief`);
|
||||
else assert.equal(timing.annual_brief, undefined, `${where} annual_brief stays a lookup`);
|
||||
// 瑜伽只在路由带瑜伽的领域上卡。
|
||||
if (spec.yogas) assert.ok(natal.yogas_detail, `${where} yogas_detail`);
|
||||
else assert.equal(natal.yogas_detail, undefined, `${where} yogas_detail stays a lookup`);
|
||||
}
|
||||
});
|
||||
@@ -63,24 +63,27 @@ test("Shadbala and Argala sections carry their reading labels", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("dasha systems carry the boundary rule and full instants", () => {
|
||||
test("dasha systems carry the boundary rule and the route's own systems", () => {
|
||||
const built = card(golden.charts[0].workflow, "career");
|
||||
const timing = record(record(built.base).timing);
|
||||
const dashas = record(timing.dasha_systems);
|
||||
assert.equal(dashas.boundary_rule, "start_inclusive_end_exclusive");
|
||||
// Producers with a time of day carry start_at; date-only producers keep the date alone.
|
||||
const stamps = Object.values(record(dashas.systems))
|
||||
.map((row) => record(record(row).md).start_at)
|
||||
.filter((value): value is string => typeof value === "string");
|
||||
assert.ok(stamps.length > 0);
|
||||
for (const stamp of stamps) assert.match(stamp, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{2}:\d{2})$/);
|
||||
// 原值:卡上 26 个大运系统,逐行检查 md.start_at 的时刻格式
|
||||
// 新值:卡上只留本题路由点名的 Vimshottari / Narayana;其余系统交给查卡工具
|
||||
// 原因:#170 按本题路由裁层(省 token),大运系统不再每张卡全量送达
|
||||
const systems = record(dashas.systems);
|
||||
assert.deepEqual(Object.keys(systems).sort(), ["narayana", "vimshottari"]);
|
||||
assert.match(String(record(record(systems.narayana).md).start), /^\d{4}-\d{2}-\d{2}$/);
|
||||
});
|
||||
|
||||
test("yoga rules the engine could not evaluate are listed apart from formed yogas", () => {
|
||||
const built = card(golden.charts[0].workflow, "family");
|
||||
// 原值:family 卡也带 yogas_detail
|
||||
// 新值:yoga 只在 EVIDENCE_CARD_SPECS[domain].yogas 为 true 的领域上卡,改用 career
|
||||
// 原因:#170 按本题路由裁层
|
||||
const built = card(golden.charts[0].workflow, "career");
|
||||
const detail = record(record(record(built.base).natal).yogas_detail);
|
||||
const notEvaluated = Array.isArray(detail.not_evaluated) ? detail.not_evaluated as Json[] : [];
|
||||
assert.ok(notEvaluated.length > 0, "the family packet has three rules without an evaluator");
|
||||
assert.ok(notEvaluated.length > 0, "the career packet has three rules without an evaluator");
|
||||
const formed = new Set((Array.isArray(detail.rows) ? detail.rows as Json[] : []).map((row) => row.name));
|
||||
for (const row of notEvaluated) {
|
||||
assert.ok(["not_implemented", "evaluation_failed"].includes(String(row.evaluation_status)));
|
||||
|
||||
@@ -68,13 +68,23 @@ test("base parameters are the engine's own (ayanamsa, nodes, house systems, kara
|
||||
}
|
||||
});
|
||||
|
||||
test("every dasha system's running layer is on the base card, not-yet-computed systems are gaps", () => {
|
||||
test("only the route's dasha systems ride the base card, the rest stay a lookup", () => {
|
||||
// 原值:每个 applicable 的系统都在卡上(2026-11-07「回答模型拿到完整数据」),
|
||||
// not_available 的系统写 base.dasha_systems.<name> gap
|
||||
// 新值:只保留本题路由(EVIDENCE_CARD_SPECS[domain].route)点名的大运系统;
|
||||
// 其余系统交给查卡工具,不再上卡、不再记 gap
|
||||
// 原因:#170 省 token:数据卡按本题路由裁层,同一事实不在每个问题上重复送达
|
||||
for (const chart of golden.charts) {
|
||||
const card = cardFor(chart.workflow, "career");
|
||||
const systems = dig(card.base.timing, "dasha_systems", "systems") as Json;
|
||||
const rows = dig(native(chart.workflow), "dasha_systems", "rows") as Json[];
|
||||
const routeSystems = new Set(["vimshottari", "narayana"]);
|
||||
for (const row of rows) {
|
||||
const name = row.system as string;
|
||||
if (!routeSystems.has(name)) {
|
||||
assert.equal(systems[name], undefined, `${chart.id} ${name} not on the card`);
|
||||
continue;
|
||||
}
|
||||
if (row.applicable === undefined) {
|
||||
assert.equal(systems[name], undefined, `${chart.id} ${name}`);
|
||||
assert.ok(card.meta.gaps.includes(`base.dasha_systems.${name}:not_available_yet`), `${chart.id} ${name} gap`);
|
||||
@@ -82,7 +92,7 @@ test("every dasha system's running layer is on the base card, not-yet-computed s
|
||||
}
|
||||
for (const level of ["md", "ad", "pd"]) assert.deepEqual(dig(systems, name, level), row[level], `${chart.id} ${name} ${level}`);
|
||||
}
|
||||
for (const name of ["vimshottari", "narayana", "chara", "sthira", "drig", "shoola", "niryaana_shoola", "navamsha", "lagna_kendradi", "yogini", "kalachakra", "ashtottari", "tribhagi"]) {
|
||||
for (const name of ["vimshottari", "narayana"]) {
|
||||
assert.ok(systems[name], `${chart.id} ${name}`);
|
||||
}
|
||||
// Vimshottari agrees with the card's own running periods; Narayana carries dates.
|
||||
@@ -90,13 +100,17 @@ test("every dasha system's running layer is on the base card, not-yet-computed s
|
||||
assert.equal(dig(systems, "narayana", "md", "sign"), dig(card.base.timing.narayana, "md", "sign"));
|
||||
assert.match(String(dig(systems, "narayana", "md", "start")), /^\d{4}-\d{2}-\d{2}$/);
|
||||
assert.equal(typeof dig(systems, "narayana", "date_basis"), "string");
|
||||
assert.equal(typeof dig(systems, "ashtottari", "applicable"), "boolean");
|
||||
}
|
||||
});
|
||||
|
||||
test("transit detail, yogas with conditions, strength and Jaimini sit on the base card", () => {
|
||||
for (const chart of golden.charts) {
|
||||
const card = cardFor(chart.workflow, "timing");
|
||||
// 原值:timing 卡也带 yogas_detail
|
||||
// 新值:yoga 只在 EVIDENCE_CARD_SPECS[domain].yogas 为 true 的领域(career / general / …)上卡,
|
||||
// timing / annual 等路由不含 yoga 的题目从查卡工具取
|
||||
// 原因:#170 按本题路由裁层
|
||||
const yogaCard = cardFor(chart.workflow, "general");
|
||||
const layers = native(chart.workflow);
|
||||
const transits = card.base.timing.transits_detail as Json;
|
||||
assert.deepEqual(transits.bodies, dig(layers, "transit_detail", "bodies"), chart.id);
|
||||
@@ -108,7 +122,7 @@ test("transit detail, yogas with conditions, strength and Jaimini sit on the bas
|
||||
assert.notEqual(dig(transits, "bodies", "Moon", "chandra_bala"), undefined);
|
||||
assert.equal(typeof dig(transits, "sade_sati", "active"), "boolean");
|
||||
assert.deepEqual(transits.key_dates, dig(layers, "slow_transits", "key_dates"));
|
||||
const yogas = card.base.natal.yogas_detail as Json;
|
||||
const yogas = yogaCard.base.natal.yogas_detail as Json;
|
||||
assert.equal((yogas.rows as Json[]).length, (dig(layers, "yoga_conditions", "rows") as Json[]).length);
|
||||
// 原值:status 只有 candidate / computed
|
||||
// 新值:结构观察行写 structural_observation,名字带 (Observation)
|
||||
@@ -117,6 +131,13 @@ test("transit detail, yogas with conditions, strength and Jaimini sit on the bas
|
||||
assert.ok(["candidate", "computed", "structural_observation"].includes(String(row.status)), `${chart.id} ${row.name}`);
|
||||
assert.equal(row.status === "structural_observation", String(row.name).endsWith("(Observation)"), `${chart.id} ${row.name}`);
|
||||
if (row.status === "structural_observation") assert.equal(row.claim_kind, "structural_observation");
|
||||
// 原值:row.planets 是 { 星: { owns, functional, dignity } }
|
||||
// 新值:row.planets 是星名数组;owns / functional / dignity 仍在 base.natal 的行星表与功能吉凶表
|
||||
// 原因:#170 同一事实只算一次,去掉逐行重复的行星元数据
|
||||
if (row.planets !== undefined) {
|
||||
assert.ok(Array.isArray(row.planets), `${chart.id} ${row.name} planets are names`);
|
||||
for (const name of row.planets as string[]) assert.equal(typeof name, "string");
|
||||
}
|
||||
}
|
||||
const strength = card.base.natal.strength as Json;
|
||||
assert.deepEqual(strength.bhava_bala_rupas, dig(layers, "strength_detail", "bhava_bala", "houses"));
|
||||
|
||||
@@ -203,7 +203,7 @@ evidence_card.backstage records, for the backend only, that global web evidence
|
||||
<!-- 产品规则,不是占星师的方法:S-12 讲时间时两套运程都读到当前层。 -->
|
||||
12. 讲时间(最近、今年、接下来几年)时,两套运程都读到当前那一层:Vimshottari 的大运、小运、细运,Narayana 的主运和子运,各落在哪个宫、管什么。主运和子运方向不同时两面都说,不只取主运。
|
||||
<!-- 产品规则,不是占星师的方法:S-13 跨层同源只算一个来源(BUG-1773)。落实占星师 2026-11-08 验收说明(rulings-round6-reply-20261108「产品正在做的四件事」第 1 条:「不能把同源规则条数当独立证据票数」)。BUG-1758 只在受冲来源账内按事实去重;r6 盲测人工复核 3/10、staging 2/9 的回答把跨层复用(同一颗星既是 AmK 又是 AL 主星;同一宫主在 D1、Arudha、Jaimini 各算一次;同一月亮事实既进瑜伽又进力量)当成独立支持。卡上新增各领域 `independence`(fact_id → 复用它的层;support / pressure 按不同 fact_id 列)。 -->
|
||||
13. 卡上各领域的 `independence` 是同源索引:`facts` 的每个键是一个盘面事实(一颗星的 D1 落座落宫、一个同宫、一个相位、一颗星的功能吉凶),值是复用这个事实的层(宫主、宫内星、Arudha 主星、Jaimini karaka、瑜伽、事业分析、Shadbala、大运主星、受冲来源);`support` / `pressure` 列出支持 / 压力一面的不同事实。几层共用同一个事实时,写成一个来源,并点名复用它的层(例「土星落天秤 2 宫这一个事实,同时是 AmK 和 A2 主星」),不当成几条独立证据;独立的支持或压力只按不同的事实计。几层共用一个事实时,不写「多层一致支持」「多体系一致」「N 条独立线索」。
|
||||
13. 卡上各领域的 `independence` 是同源索引:`facts` 的每个键是一个盘面事实(一颗星的 D1 落座落宫、一个同宫、一个相位、一颗星的功能吉凶),值是复用这个事实的层(宫主、宫内星、Arudha 主星、Jaimini karakas、瑜伽、事业分析、Shadbala、大运主星、受冲来源);`support` / `pressure` 列出支持 / 压力一面的不同事实。几层共用同一个事实时,写成一个来源,并点名复用它的层(例「土星落天秤 2 宫这一个事实,同时是 AmK 和 A2 主星」),不当成几条独立证据;独立的支持或压力只按不同的事实计。几层共用一个事实时,不写「多层一致支持」「多体系一致」「N 条独立线索」。
|
||||
<!-- /product-block:shared-reading -->
|
||||
|
||||
## F. 可能违反占星师 2026-11-07 写作底线的现有产品规则(第 1–6 条已按她 2026-11-08 第五轮裁定 6 修改)
|
||||
|
||||
Reference in New Issue
Block a user