Compare commits

...
Author SHA1 Message Date
jesse-uxandGrok (agent loop) e5c139367d perf(consult): card keeps only the route's layers (#170, BUG-1813)
Base card gated by EVIDENCE_CARD_SPECS[domain].route: dasha systems keep Vimshottari/Narayana only, yoga detail only on yoga domains, annual brief only on the annual route; yoga row planets become names. Model-visible contract+card 46,664 -> 32,153 chars on average (-31.1%), max 54,532 -> 43,419 over 3 public charts x 12 domains. New size/route test; three existing assertions updated with old/new/reason notes.

Co-Authored-By: Grok (agent loop)
2026-11-09 08:59:44 +08:00
11 changed files with 364 additions and 46 deletions
+6
View File
@@ -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
View File
@@ -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 另跑)。
+1
View File
@@ -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。
+55 -29
View File
@@ -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"));
+1 -1
View File
@@ -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 修改)