From bf5186fe62e4e5bb737eda9e8ea8a37baf2694bf Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Mon, 9 Nov 2026 12:48:28 +0800 Subject: [PATCH] revert(#170): drop the info-deleting compression, keep the measurement (round 2 start) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 FIX-token-card-1: - references/product_runtime_contract.md 恢复原样 - 删除自拟任务书(以 Claude 的 TASK-token-card-20261109 为准) - 三处被弱化的断言恢复原样 - consultation-evidence-card.ts 回到 staging:删信息换来的压缩全部作废,重新按 「不删信息」做 - 新增测量脚本 consult-card-size-170.mts(真实工具链,state.evidenceCardChars) 基线 c032823f 实测(3 盘 × 10 领域):卡片 min 41,963 / 中位 46,017 / max 53,930 / 平均 46,587;modelVisibleChars 平均 91,559。块大小表见 PROGRESS。 Co-Authored-By: Grok (agent loop) --- .../PROGRESS-token-card-compress-20261109.md | 50 ++++++++++ .../TASK-token-card-compress-20261109.md | 92 ------------------- .../research/consult-card-size-170.mts | 70 ++++++++++++++ .../src/lib/consultation-evidence-card.ts | 84 ++++++----------- ...lt-card-calendar-narayana-20261108.test.ts | 7 +- ...sult-downstream-labels-r3-20261107.test.ts | 21 ++--- ...t-evidence-card-full-data-20261107.test.ts | 29 +----- references/product_runtime_contract.md | 2 +- 8 files changed, 165 insertions(+), 190 deletions(-) delete mode 100644 docs/tasks/TASK-token-card-compress-20261109.md create mode 100644 frontend/scripts/research/consult-card-size-170.mts diff --git a/docs/tasks/PROGRESS-token-card-compress-20261109.md b/docs/tasks/PROGRESS-token-card-compress-20261109.md index a98bf5b9..54ced892 100644 --- a/docs/tasks/PROGRESS-token-card-compress-20261109.md +++ b/docs/tasks/PROGRESS-token-card-compress-20261109.md @@ -51,3 +51,53 @@ - 无模型凭据:盲测与「准确度不下降」留待 staging 真机。 - 本机 Node 22 / Windows;未跑前端全量 `npm test` 与 Python 快速门(按执行方分工由 Claude 另跑)。 + + +## 第 1 次打回后的重做(2026-11-09,按 FIX-token-card-1 + 正式任务书) + +### 已撤回 + +- `references/product_runtime_contract.md` 已恢复原样(不再改生产合同文件)。 +- 删除自拟任务书 `docs/tasks/TASK-token-card-compress-20261109.md`;以 Claude 的 `TASK-token-card-20261109.md` + `FIX-token-card-1.md` 为准。 +- 上一轮改动过的三处断言(`consult-card-calendar-narayana-20261108`、`consult-downstream-labels-r3-20261107`、`consult-evidence-card-full-data-20261107`)已恢复原样。 +- `frontend/src/lib/consultation-evidence-card.ts` 回到 staging 版本:上一轮靠删信息换来的压缩全部作废,重新按「不删信息」来做。 + +### 改前基线(用真实工具链量,`state.evidenceCardChars`) + +用 `frontend/scripts/research/consult-card-size-170.mts`,3 张公开盘 × 10 个引擎领域 = 30 例; +基线提交 `c032823f9`(staging,已含 #169): + +| 指标 | 模型可见卡片字符数 | +| --- | ---: | +| 最小 | 41,963 | +| 中位 | 46,017 | +| 最大 | 53,930 | +| 平均 | 46,587 | + +同一脚本量出的 `modelVisibleChars`(含契约与 methodology)平均 91,559。 + +### 基线里各块有多大(30 例平均) + +| 块 | 平均字符数 | +| --- | ---: | +| `base`(合计) | 35,892 | +| ├ `base.timing` | 17,360 | +| │ ├ `dasha_systems`(26 个系统) | 7,525 | +| │ ├ `sade_sati_timeline` | 2,622 | +| │ ├ `transits_detail` | 2,478 | +| │ ├ `annual_brief` | 2,105 | +| │ └ `narayana` | 1,624 | +| ├ `base.natal` | 15,539 | +| │ ├ `yogas_detail` | 8,298 | +| │ ├ `affliction_evidence` | 2,721 | +| │ └ `planets` | 1,503 | +| └ `base.parameters` | 1,719 | +| `domain_sections` | 8,882 | +| `meta` | 1,553 | + +### 一个重要约束(下一步的前提) + +查卡段名(`EVIDENCE_LOOKUP_SECTIONS`)里只有 `vimshottari_mahadashas`、`chara_dasha` 这类, +**没有**其余 24 个大运系统,也**没有**年运(Tajika)块。按 FIX 的要求「某层没有对应段名时,这层不许下卡」, +这两块必须留在卡上。所以 30% 只能靠结构压缩(同事实引用、恒定说明只写一次、去空值、表格改列名 + 行数组), +不能靠下卡。下一步从 `base.timing.dasha_systems` 与 `base.natal.yogas_detail` 两块开始。 diff --git a/docs/tasks/TASK-token-card-compress-20261109.md b/docs/tasks/TASK-token-card-compress-20261109.md deleted file mode 100644 index 50aeee47..00000000 --- a/docs/tasks/TASK-token-card-compress-20261109.md +++ /dev/null @@ -1,92 +0,0 @@ -# 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。 diff --git a/frontend/scripts/research/consult-card-size-170.mts b/frontend/scripts/research/consult-card-size-170.mts new file mode 100644 index 00000000..c3bf5e20 --- /dev/null +++ b/frontend/scripts/research/consult-card-size-170.mts @@ -0,0 +1,70 @@ +/** + * #170 测量:模型可见数据卡字符数,逐盘逐领域(走真实工具链,读 runtime state)。 + * 运行:frontend > node node_modules/tsx/dist/cli.mjs scripts/research/consult-card-size-170.mts + */ +import { readFileSync } from "node:fs"; + +import { + createConsultationRuntimeState, + createConsultationTools, +} from "../../src/mastra/consultation-tools.ts"; +import { consultationEngineDomainIds } from "../../src/lib/consultation-domain-registry.ts"; + +type Json = Record; + +const golden = JSON.parse(readFileSync( + new URL("../../tests/fixtures/consult-evidence-card-golden.json", import.meta.url), + "utf8", +)) as { charts: Array<{ id: string; workflow: Json }> }; + +const rows: Array<{ chart: string; domain: string; card: number; visible: number }> = []; + +for (const chart of golden.charts) { + for (const domain of consultationEngineDomainIds) { + const state = createConsultationRuntimeState(); + const tools = createConsultationTools({ + userId: "u", + sessionId: "s", + requestId: `size-${chart.id}-${domain}`, + consultationMode: "verified_chart", + theme: domain, + serverChart: { + name: "public", + toolInput: { + year: 1955, month: 2, day: 24, hour: 19, minute: 15, city: "San Francisco", + lat: 37.77, lon: -122.42, tz: -8, ayanamsa: "raman", + }, + truth: { birthTimeSource: "reported" }, + } as never, + state, + runWorkflow: async () => structuredClone(chart.workflow) as never, + }); + await tools["run-jyotish-consultation"].execute!( + { question: "这一块怎么样", domains: [domain] } as never, + { writer: { custom: async () => {} } } as never, + ); + rows.push({ + chart: chart.id, + domain, + card: state.evidenceCardChars ?? 0, + visible: state.modelVisibleChars ?? 0, + }); + } +} + +const summarize = (values: number[]) => { + const sorted = [...values].sort((a, b) => a - b); + return { + cases: sorted.length, + min: sorted[0], + median: sorted[Math.floor(sorted.length / 2)], + max: sorted[sorted.length - 1], + mean: Math.round(sorted.reduce((sum, value) => sum + value, 0) / sorted.length), + }; +}; + +console.log(JSON.stringify({ + card: summarize(rows.map((row) => row.card)), + visible: summarize(rows.map((row) => row.visible)), + rows, +}, null, 2)); diff --git a/frontend/src/lib/consultation-evidence-card.ts b/frontend/src/lib/consultation-evidence-card.ts index fb50db0c..dc791269 100644 --- a/frontend/src/lib/consultation-evidence-card.ts +++ b/frontend/src/lib/consultation-evidence-card.ts @@ -1904,18 +1904,7 @@ const DASHA_ROW_KEYS = [ "continuity_issue", ] as const; -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())); +function baseExtras(context: unknown, base: EvidenceCard["base"], gaps: string[]) { const layers = nativeLayers(context); const localLayers = record(record(context).local_layers); const parameters = readyLayer(layers, "card_parameters"); @@ -1932,9 +1921,6 @@ function baseExtras( 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; @@ -1998,49 +1984,37 @@ function baseExtras( } else { gaps.push("base.sade_sati_timeline"); } - // #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 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 = spec.yogas ? readyLayer(layers, "yoga_conditions") : null; + const yogas = readyLayer(layers, "yoga_conditions"); 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. - // #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; - }); + 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", + ])); 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. @@ -2052,7 +2026,7 @@ function baseExtras( rows, ...(notEvaluated.length ? { not_evaluated: notEvaluated, ...pickKeys(yogas, ["not_evaluated_rule", "rule_engine_status"]) } : {}), }; - } else if (spec.yogas) { + } else { gaps.push("base.yogas_detail"); } const strength = readyLayer(layers, "strength_detail"); @@ -2793,7 +2767,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, first.domain); + baseExtras(first.context, base, gaps); const domainSections: EvidenceCard["domain_sections"] = {}; const domains: ConsultationDomain[] = []; const planFor = (domain: ConsultationDomain): EvidenceCardVargaPlan | undefined => { @@ -2819,7 +2793,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, source.domain); + baseExtras(source.context, candidate, ownGaps); if (JSON.stringify(candidate) !== JSON.stringify(base)) { own = candidate; gaps.push(`${source.domain}.base_differs`, ...ownGaps.map((gap) => `${source.domain}.${gap}`)); diff --git a/frontend/tests/consult-card-calendar-narayana-20261108.test.ts b/frontend/tests/consult-card-calendar-narayana-20261108.test.ts index 245cee89..7f5ef169 100644 --- a/frontend/tests/consult-card-calendar-narayana-20261108.test.ts +++ b/frontend/tests/consult-card-calendar-narayana-20261108.test.ts @@ -11,7 +11,7 @@ import type { ConsultationDomain } from "../src/lib/consultation-domain-registry type Json = Record; const golden = JSON.parse(readFileSync(new URL("./fixtures/consult-evidence-card-golden.json", import.meta.url), "utf8")) as { - charts: Array<{ id: string; workflow: Json; annual_workflow: Json; timing_workflow: Json }>; + charts: Array<{ id: string; workflow: Json; timing_workflow: Json }>; }; function card(workflow: Json, domain: ConsultationDomain): EvidenceCard { @@ -24,11 +24,8 @@ 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.annual_workflow, "annual"]] as const) { + for (const [workflow, domain] of [[chart.timing_workflow, "timing"], [chart.workflow, "wealth"]] 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}`); diff --git a/frontend/tests/consult-downstream-labels-r3-20261107.test.ts b/frontend/tests/consult-downstream-labels-r3-20261107.test.ts index ff87c152..7e5f751c 100644 --- a/frontend/tests/consult-downstream-labels-r3-20261107.test.ts +++ b/frontend/tests/consult-downstream-labels-r3-20261107.test.ts @@ -63,27 +63,24 @@ test("Shadbala and Argala sections carry their reading labels", () => { } }); -test("dasha systems carry the boundary rule and the route's own systems", () => { +test("dasha systems carry the boundary rule and full instants", () => { 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"); - // 原值:卡上 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}$/); + // 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})$/); }); test("yoga rules the engine could not evaluate are listed apart from formed yogas", () => { - // 原值:family 卡也带 yogas_detail - // 新值:yoga 只在 EVIDENCE_CARD_SPECS[domain].yogas 为 true 的领域上卡,改用 career - // 原因:#170 按本题路由裁层 - const built = card(golden.charts[0].workflow, "career"); + const built = card(golden.charts[0].workflow, "family"); 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 career packet has three rules without an evaluator"); + assert.ok(notEvaluated.length > 0, "the family 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))); diff --git a/frontend/tests/consult-evidence-card-full-data-20261107.test.ts b/frontend/tests/consult-evidence-card-full-data-20261107.test.ts index 32df37ed..9791c036 100644 --- a/frontend/tests/consult-evidence-card-full-data-20261107.test.ts +++ b/frontend/tests/consult-evidence-card-full-data-20261107.test.ts @@ -68,23 +68,13 @@ test("base parameters are the engine's own (ayanamsa, nodes, house systems, kara } }); -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. gap - // 新值:只保留本题路由(EVIDENCE_CARD_SPECS[domain].route)点名的大运系统; - // 其余系统交给查卡工具,不再上卡、不再记 gap - // 原因:#170 省 token:数据卡按本题路由裁层,同一事实不在每个问题上重复送达 +test("every dasha system's running layer is on the base card, not-yet-computed systems are gaps", () => { 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`); @@ -92,7 +82,7 @@ test("only the route's dasha systems ride the base card, the rest stay a lookup" } 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"]) { + for (const name of ["vimshottari", "narayana", "chara", "sthira", "drig", "shoola", "niryaana_shoola", "navamsha", "lagna_kendradi", "yogini", "kalachakra", "ashtottari", "tribhagi"]) { assert.ok(systems[name], `${chart.id} ${name}`); } // Vimshottari agrees with the card's own running periods; Narayana carries dates. @@ -100,17 +90,13 @@ test("only the route's dasha systems ride the base card, the rest stay a lookup" 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); @@ -122,7 +108,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 = yogaCard.base.natal.yogas_detail as Json; + const yogas = card.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) @@ -131,13 +117,6 @@ 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")); diff --git a/references/product_runtime_contract.md b/references/product_runtime_contract.md index a269a4c9..62c0defb 100644 --- a/references/product_runtime_contract.md +++ b/references/product_runtime_contract.md @@ -203,7 +203,7 @@ evidence_card.backstage records, for the backend only, that global web evidence 12. 讲时间(最近、今年、接下来几年)时,两套运程都读到当前那一层:Vimshottari 的大运、小运、细运,Narayana 的主运和子运,各落在哪个宫、管什么。主运和子运方向不同时两面都说,不只取主运。 -13. 卡上各领域的 `independence` 是同源索引:`facts` 的每个键是一个盘面事实(一颗星的 D1 落座落宫、一个同宫、一个相位、一颗星的功能吉凶),值是复用这个事实的层(宫主、宫内星、Arudha 主星、Jaimini karakas、瑜伽、事业分析、Shadbala、大运主星、受冲来源);`support` / `pressure` 列出支持 / 压力一面的不同事实。几层共用同一个事实时,写成一个来源,并点名复用它的层(例「土星落天秤 2 宫这一个事实,同时是 AmK 和 A2 主星」),不当成几条独立证据;独立的支持或压力只按不同的事实计。几层共用一个事实时,不写「多层一致支持」「多体系一致」「N 条独立线索」。 +13. 卡上各领域的 `independence` 是同源索引:`facts` 的每个键是一个盘面事实(一颗星的 D1 落座落宫、一个同宫、一个相位、一颗星的功能吉凶),值是复用这个事实的层(宫主、宫内星、Arudha 主星、Jaimini karaka、瑜伽、事业分析、Shadbala、大运主星、受冲来源);`support` / `pressure` 列出支持 / 压力一面的不同事实。几层共用同一个事实时,写成一个来源,并点名复用它的层(例「土星落天秤 2 宫这一个事实,同时是 AmK 和 A2 主星」),不当成几条独立证据;独立的支持或压力只按不同的事实计。几层共用一个事实时,不写「多层一致支持」「多体系一致」「N 条独立线索」。 ## F. 可能违反占星师 2026-11-07 写作底线的现有产品规则(第 1–6 条已按她 2026-11-08 第五轮裁定 6 修改)