fix(consult): keep career type off significators and warm off the compute cap

The career checklist now says significators are only for strength and affliction. Chart-save warm no longer takes a heavy-compute slot.

Biography severe conflicts fell from 3 to 1. One career answer still names a direction from a planet nature, so this is not accepted for merge. Not pushed.
This commit is contained in:
jesse-ux
2026-10-09 10:28:44 +08:00
parent 3ad8e80077
commit fc3a2e680d
8 changed files with 195 additions and 3 deletions
+6
View File
@@ -1,5 +1,11 @@
# 印度占星 Skill 更新日志
## 2026-10-09 — 事业清单写明代表星不猜职业,预热不再占重计算名额(回测未达标,未推送,未合入,未上线)
- 事业清单加一条:判断表里的自然代表星、Jaimini 代表星、大运主星只看力量和受冲,不按星的本性推断职业类型、做事方式或台前 / 幕后。职业类型仍问用户。系统提示没有改。
- 保存后的预热仍走原来的咨询入口,但不再占用重计算名额,避免刚保存就提问时收到 429。预热失败不影响保存。
- 生平回测严重冲突从 3 降到 1,对照组误报仍是 0。齐达内的事业题还有一次按星性写方向,不合入。Skill 版本不变。
## 2026-10-09 — 对话卡按提问当天取数,模型只看见数值(未推送,未合入,未上线)
- 同一张盘隔天再问,当前大运、行运和年盘按提问当天重算,不再冻在第一次计算的那天。超过两天的缓存文件会清掉。
+32
View File
@@ -17455,3 +17455,35 @@
- 相关记录:BUG-1251、BUG-1296。
- 复发自:无。
- 修复版本:本分支提交。未部署。
## BUG-1301 | 事业判断表的代表星被读成职业类型
- 状态:investigating(分支 `codex/consult-card-full-source-20261007`,清单已加一条,回测仍有 1 次;未推送、未合入、未部署)
- 首次发现 / 最近更新:2026-10-09 / 2026-10-09
- 来源:修复单 G1。前一轮生平回测严重冲突 1 → 3,新增的两次都是同一位公开球星的事业题。
- 影响面:普通咨询的事业回答。判断表仍保留代表星、旺弱和功能吉凶,没有删行,也没有改取值。
- 现象:模型把入庙、功能吉、又是当前小运主星的水星,读成说、写、算或知识、表达、信息这一类工作,并压低外面认不认你。
- 触发条件:事业题,判断表同时点出自然代表星、Jaimini 代表星或大运主星的星性。
- 根因:既有「不猜行业、问用户」没有写明这些代表星只能看力量和受冲。模型用星的本性补上了职业类型。
- 修复:事业清单加一条,不进系统提示。本轮回测严重冲突 3 → 1,对照组误报仍是 0。齐达内事业两次里仍有一次按星性写方向,所以这条不标 resolved。
- 验证:`frontend/tests/consult-condensed-checklist-20260927.test.ts` 锁住这句,并锁住系统提示源码切片仍是 15,291 字符。生平回测 72 份、失败 0。阅读评分见 `docs/tasks/PROGRESS-consult-card-full-source-fix2-20261009.md`。
- 防复发:上述测试。合入仍要求严重冲突 ≤ 1,且齐达内事业两次都不再按星性写职业类型。
- 相关记录:BUG-1176。
- 复发自:无。产品 2026-10-03「不猜行业、问用户」没有拦住判断表上的星性。
- 修复版本:清单已写进本分支。回测未达标。未部署。
## BUG-1302 | 保存后的预热占用重计算名额
- 状态:resolved(分支 `codex/consult-card-full-source-20261007`,本地未推送、未合入、未部署;有针对性回归测试)
- 首次发现 / 最近更新:2026-10-09 / 2026-10-09
- 来源:修复单 G2。
- 影响面:星盘保存成功后的后台预热,以及紧接着的用户提问。保存响应本身仍然不等待预热。
- 现象:预热走完整的 `/api/consultation_workflow`,会先占一个重计算名额。名额默认 2,占满返回 429。用户保存后立刻提问,可能撞上自己的预热。
- 触发条件:请求体 `consult_card_warm_only` 为 true。名额在读完 JSON 之后、进入预热分支之前就申请。
- 根因:预热标志只让计算提前返回,没有跳过名额申请。
- 修复:仅当路径是 `/api/consultation_workflow` 且 `consult_card_warm_only` 为 true 时不申请名额。不新增 API 类方法。预热失败仍不影响保存。
- 验证:`tests/test_api_heavy_compute_gate.py` 把上限设为 1,预热进行中时用户请求能进入计算,同时再来的用户请求仍因名额返回 429。`tests/test_api_server_growth_contract.py` 通过。`jyotish_api_server.py` 10,970 行,粗护栏上限 11,224。
- 防复发:上述并发测试。
- 相关记录:BUG-1300。
- 复发自:无。
- 修复版本:本分支提交。未部署。
@@ -0,0 +1,53 @@
# PROGRESS · 对话卡同源第二轮修复(2026-10-09)
分支 `codex/consult-card-full-source-20261007`。已 rebase 到 `origin/staging` `8bb7ff93`。状态:执行中。G3 未通过。未推送,未合入,未上线。
## 结论
事业清单加了「代表星只看力量和受冲」,预热不再占重计算名额。生平回测严重冲突从 3 降到 1,对照组误报仍是 0。齐达内事业两次里还有一次按星性写方向,不合入,不让步。
## 修复
| 项 | 做法 | 结果 |
| --- | --- | --- |
| G1 事业清单 | 规则写在 `consultation-condensed-checklist.ts` 的事业列表,不进系统提示。不删判断表的行,不改取值。 | 测试锁住这句。系统提示源码切片仍是 15,291 字符。回测未过,见下表。BUG-1301,仍 investigating。 |
| G2 预热名额 | `consult_card_warm_only` 为 true 时不申请重计算名额。不新增 API 类方法。保存仍不等待预热。 | 上限设为 1 时,预热进行中用户请求能进入计算,另一条用户请求仍 429。BUG-1302。 |
| G3 回测 | 9 人 × 4 领域 × 2 次。改前用上一轮已挂事实卡的 72 份。改后用同一份事实、同一把临时 key,只多了这一条清单。 | 严重冲突 3 → 1。齐达内事业第 2 次仍按星性写方向。 |
其他领域清单里,按星性推断能力的只有学业:「4、9 宫受冲而 5 宫、水星有力,常见学历这条路不顺,本事不靠学历」。本轮只列出,没有改。婚恋、父母、健康里的「两个及以上」是受冲范围,不是按星性猜职业。
`scripts/jyotish_api_server.py` 10,970 行,粗护栏 11,224。类方法数没有增加。`page.tsx` 没有改。系统提示文件没有改。
## 生平回测
模型 `deepseek-flash`。key 只在当次命令的环境变量里,没有写入仓库、日志正文或本文。引擎仍是 golden 桩,参照日 2026-09-27,岁差 raman,交点 mean。判断靠阅读,脚本只写确定性检查。
| 项 | 改前(上一轮事实卡) | 改后(加了这条规则) |
| --- | --- | --- |
| 份数 / 失败 / 空答 | 72 / 0 / 0 | 72 / 0 / 0 |
| 墙钟 | 920.9 秒 | 958.5 秒 |
| 严重冲突 | 3 | 1 |
| 对照组误报 | 0 | 0 |
| 行业类型冲突 | 3 | 1 |
| 事业题请问一句 | — | 18 / 18 |
| 禁句 / 性别代词 / must_not 原文 | 0 / 0 / 4 | 0 / 0 / 3 |
| 平均秒 / 平均卡字符 / 最大卡 | 49.9 / 14,129.8 / 16,281 | 50.0 / 14,139.4 / 16,281 |
| 平均输入 / 输出 / 推理 / 缓存输入 | 28,655.8 / 9,328.0 / 8,736.1 / 21,240.9 | 29,472.2 / 9,566.8 / 9,003.8 / 21,968.0 |
改前的 3 条严重冲突是泰勒事业第 2 次,加齐达内事业两次。改后泰勒两次和齐达内第 1 次不再把行业写成另一类,也不再压低「外面认不认你」。齐达内第 2 次仍把方向写成知识、表达、信息和资源,并写名声不是一次亮相。其余 17 份事业题没有写成分析、顾问、技术,也没有写成幕后或不靠曝光。
对照组里「不常在 / 不在身边」出现在布什父母两次和齐达内父母第 2 次,都是「可能这样,也可能人在、只是话少」的两头范围,不算误报。其余领域没有把对照盘写成终身重病、多段婚姻或父母缺席的定论。
用量合计:输入 2,122,001,输出 688,813,推理 648,271,缓存输入 1,581,696。
## 本机检查
| 项 | 结果 |
| --- | --- |
| 事业清单测试 | 6 条通过。含新句子和系统提示长度。单领域含清单仍 ≤ 20,000。 |
| 预热名额、增长合同、对话卡测试 | 通过。 |
| tsc | 0 |
| lint | 0 error,125 条原有 warning,没有清理。 |
| 快速门、Python 全量、npm test 全量、next build、Linux | 本轮没跑。不把上一轮 Windows 快速门当成通过。 |
G4 未完成。G3 未过,状态保持执行中。
+2 -1
View File
@@ -130,7 +130,8 @@
| 任务书 | 进度 | 主题 | 状态 | 落点 |
| --- | --- | --- | --- | --- |
| `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` |
| `TASK-consult-card-full-source-fix-20261009.md` | `PROGRESS-consult-card-full-source-fix-20261009.md` | **对话卡同源修复**:缓存键无日期致当前大运冻结、整卡 37.5K(每格带内部路径)、星照整列 blocked(资料库其实有出处)、前端测试手写数据、无预热;合入前用临时 key 跑模型 A/B 与生平回测 | 执行中(F6 严重冲突 1→3,未让步) | `codex/consult-card-full-source-20261007` |
| `TASK-consult-card-full-source-fix-20261009.md` | `PROGRESS-consult-card-full-source-fix-20261009.md` | **对话卡同源修复**:缓存键无日期致当前大运冻结、整卡 37.5K(每格带内部路径)、星照整列 blocked(资料库其实有出处)、前端测试手写数据、无预热;合入前用临时 key 跑模型 A/B 与生平回测 | F1–F5 已验收,F6 未过(见 fix2) | `codex/consult-card-full-source-20261007` |
| `TASK-consult-card-full-source-fix2-20261009.md` | `PROGRESS-consult-card-full-source-fix2-20261009.md` | **对话卡同源第二轮**:齐达内事业被「水星=说写算」带偏致严重冲突 1→3;事业清单加「代表星只看力量受冲、不按星性猜职业」后重跑回测(≤1 才合入);预热改为不占重计算名额 | 执行中(G3:齐达内事业仍有 1 次按星性写方向,未让步) | `codex/consult-card-full-source-20261007` |
| — (产品 09-27 拍板 D1–D4,直接执行) | [PROGRESS](PROGRESS-home-landing-blank-20260927.md) | **登录后 / 裸 `/` 落空白首页**:真机登录后在「首页」提问其实问进了上一次生时校正。删登录返回存根(401 / 次级页链接写、登录后写回 `?c=` 打开),裸 `/` 不再落最近会话,一律当前人物的空白首页(复用空草稿);`?c=` / `?new=1` / 对话内刷新不变;推翻 BUG-1038 存根与 BUG-599 默认落点 | 已验收(真机欠) | `codex/home-landing-blank-20260927`(BUG-1052,本地未推) |
| — (产品 09-26 口头拍板 D1–D3,直接执行) | `PROGRESS-consult-answer-truncation-20260926.md` | **普通咨询回答写到一半被掐断仍扣点(BUG-1051,复发自 BUG-305)**:工具循环与写回答共用 110 秒 signal;Mastra 1.50 超时不抛错(`abort` 块 + `finish(tripwire)` 后正常关流),结算只认抛错与 `length`。D1 写回答自有 70 秒时钟(首用起算,续写 / 回答重试共用,最坏 180 秒,`maxDuration` 240);D2 写回答的最后一个流不是 `stop` 且有正文 → `answer_truncated`、不扣点、记 abort 步、不冲半句,`length` 续写不变;D3 观测加 `composeFinishReason` / `composeAborted` / `answerVisibleChars` | 已验收(真机欠) | `codex/consult-answer-truncation-20260926`(本地,未推送);新回归 15 条用真实 Mastra `Agent`(修复前 11 条红);全量失败名单 0 新增;Python 948/1;`/` ○、gzip 0%;真机清单 `docs/testing/consult-answer-truncation-20260926.md` |
| `TASK-consult-evidence-card-research-20260927.md` | `PROGRESS-consult-evidence-card-research-20260927.md` | **普通对话数据卡调研**:引擎输出逐项分五类计量(现约 4 万 token、父母问题相关约 3.5%);四处领域→技法来源对账并起草各领域数据卡(家庭拆父母/子女);卡体量与逐字一致性;按卡算的提速空间;反馈迭代埋点方案。只调研不改线上 | 已验收(待产品拍板 7 项) | `codex/consult-evidence-card-research-20260927` 快进 staging;报告 `docs/research/consult_evidence_card_research_2026_09_27.md`;投影缺口记 BUG-1054 investigating |
@@ -80,9 +80,20 @@ export function sharedConsultationReading(): string {
*/
export const CONSULT_BRIEF_CHECKLIST_SOURCE = "consultation checklist (TASK-consult-affliction-reading-20261001)";
/**
* Product 2026-10-09: judgment-table significators are strength and affliction
* only. A planet's nature is not a career type, a working style, or a
* public / behind-the-scenes verdict. Career type still asks the user
* (CAREER_FIELD_ASK_RULE). This line stays in the career checklist, not the
* system prompt.
*/
export const CAREER_SIGNIFICATOR_STRENGTH_RULE =
"判断表里的代表星(自然代表星、Jaimini 代表星、大运主星)只用来看力量与受冲,不按星的本性推断职业类型、做事方式或台前 / 幕后。";
export const CONSULTATION_CONDENSED_CHECKLISTS: Readonly<Partial<Record<ConsultationDomain, readonly string[]>>> = {
career: [
CAREER_FIELD_ASK_RULE,
CAREER_SIGNIFICATOR_STRENGTH_RULE,
"必看:D1 10 宫与 10 宫主、AmK;卡上 d9 段确认 10 宫主与 AmK 的旺弱、Vargottama、D1/D9 反转;D10 上升与 10 宫;AL 与 A10;10 宫 SAV 与木星、土星所在宫的 SAV。",
"时间:Vimshottari(MD/AD/PD)与 Narayana 双轨同向才谈应期,看大运主与 10 宫、10 宫主、D10 的关系;每颗星先按功能吉凶定性。",
"分三件事看(回答里用生活说法,不报层名和编号),不得合成一句「事业机会」:①机会出现(消息、邀约、初步接洽)②成形(合同、合作、长期项目)③公开落地(发布、到账、被看见、名声)。",
@@ -13,6 +13,7 @@ import test from "node:test";
import { fileURLToPath } from "node:url";
import {
CAREER_SIGNIFICATOR_STRENGTH_RULE,
CONDENSED_CHECKLIST_SOURCE,
CONSULT_BRIEF_CHECKLIST_SOURCE,
CONSULTATION_CONDENSED_CHECKLISTS,
@@ -76,6 +77,25 @@ async function writerPrompt(domain: "career" | "marriage" | "wealth") {
return toolResultText(result.prompts[writer]);
}
test("career significators stay strength and affliction, and the system prompt is unchanged", () => {
const career = CONSULTATION_CONDENSED_CHECKLISTS.career!;
assert.equal(career[0]!.startsWith("事业题只讲盘上的力量、阻力和时间"), true);
assert.equal(career.filter((line) => line === CAREER_SIGNIFICATOR_STRENGTH_RULE).length, 1);
assert.match(CAREER_SIGNIFICATOR_STRENGTH_RULE, /自然代表星、Jaimini 代表星、大运主星/);
assert.match(CAREER_SIGNIFICATOR_STRENGTH_RULE, /不按星的本性推断职业类型、做事方式或台前 \/ 幕后/);
const mastra = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
const natal = mastra.slice(
mastra.indexOf("const jyotishInstructions"),
mastra.indexOf("const generalJyotishInstructions"),
);
assert.equal(natal.includes(CAREER_SIGNIFICATOR_STRENGTH_RULE), false);
assert.equal(natal.includes("不按星的本性推断职业类型"), false);
// 原值: 无此断言
// 新值: jyotishInstructions 源码切片 15,291 字符,且不含本条
// 原因: 产品 2026-10-09 要求规则只进事业清单,系统提示长度不变(仍 ≤ 26.6K)
assert.equal(natal.length, 15_291);
});
test("each condensed list is 5–8 lines", () => {
// 原值:全部清单 5–8 行,且只有 career / marriage / wealth 三份;
// 新值:这三份仍 5–8 行;另有 TASK-consult-affliction-reading-20261001 定稿的 8 份(2–6 行,按任务书原文,不凑行数),
@@ -100,7 +120,7 @@ test("the writer's prompt carries the condensed list with the three layers and t
// 原值(2): career 必含「事业形态」
// 新值(2): 改为必含「不替用户断定他做哪一行」(CAREER_FIELD_ASK_RULE)
// 原因(2): 产品 2026-10-02「行业不猜,问用户」,删掉「先讲一生事业形态:公众型还是幕后型」(BUG-1176)
career: ["接触", "机会出现", "成形", "公开落地", "不替用户断定他做哪一行", "事业必成", "d9 段", "KP 精确宫头仍 blocked"],
career: ["接触", "机会出现", "成形", "公开落地", "不替用户断定他做哪一行", "事业必成", "d9 段", "KP 精确宫头仍 blocked", "不按星的本性推断职业类型"],
marriage: [
// 原值: "心动接触", "关系成对", "社会法律落地"
// 新值: 三件事用生活说法,并写明回答里不报层名
+8 -1
View File
@@ -3372,7 +3372,14 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
try:
self._enforce_request_security(require_json=True)
body = self._read_json_body()
compute_slot = acquire_heavy_compute_slot(path)
# Chart-save warm builds today's packet on the same route. It must
# not take a heavy-compute slot: the cap is 2, and a question asked
# right after save would otherwise come back 429.
warm_only = (
path == '/api/consultation_workflow'
and body.get('consult_card_warm_only') is True
)
compute_slot = None if warm_only else acquire_heavy_compute_slot(path)
if path == '/api/location/resolve':
city = str(body.get('city') or '').strip()
city_aliases = {'beijing': '北京', 'shanghai': '上海', 'guangzhou': '广州', 'shenzhen': '深圳'}
+62
View File
@@ -186,6 +186,68 @@ def test_saturated_request_returns_429_with_retry_after(limit_one_gate, monkeypa
assert recovered.payload()["ok"] is True
def test_warm_consultation_does_not_consume_the_user_slot(limit_one_gate, monkeypatch) -> None:
"""A warm in progress must leave the only heavy-compute slot for a real question."""
warm_entered = threading.Event()
release_warm = threading.Event()
user_entered = threading.Event()
release_user = threading.Event()
def _compute(self, body): # noqa: ANN001
if body.get("consult_card_warm_only") is True:
warm_entered.set()
assert release_warm.wait(timeout=5)
return {"success": True, "warmed": True}
user_entered.set()
assert release_user.wait(timeout=5)
return {"success": True, "endpoint": "consultation_workflow"}
monkeypatch.setattr(JyotishAPIHandler, "_compute_consultation_workflow", _compute)
warm_box: dict = {}
user_box: dict = {}
def _run_warm() -> None:
handler = _PostCaptureHandler(
"/api/consultation_workflow",
{"consult_card_warm_only": True},
)
handler.do_POST()
warm_box["code"] = handler.status_code
warm_box["payload"] = handler.payload()
def _run_user() -> None:
handler = _PostCaptureHandler("/api/consultation_workflow", {"question": "career"})
handler.do_POST()
user_box["code"] = handler.status_code
user_box["payload"] = handler.payload()
warm_thread = threading.Thread(target=_run_warm)
warm_thread.start()
assert warm_entered.wait(timeout=5)
user_thread = threading.Thread(target=_run_user)
user_thread.start()
assert user_entered.wait(timeout=5)
blocked = _PostCaptureHandler("/api/consultation_workflow", {"question": "again"})
blocked.do_POST()
assert blocked.status_code == 429
assert blocked.payload()["error_code"] == "ERR_COMPUTE_BUSY"
release_user.set()
release_warm.set()
user_thread.join(timeout=5)
warm_thread.join(timeout=5)
assert not user_thread.is_alive()
assert not warm_thread.is_alive()
assert user_box["code"] == 200
assert user_box["payload"]["endpoint"] == "consultation_workflow"
assert warm_box["code"] == 200
assert warm_box["payload"] == {"success": True, "warmed": True}
assert "packet" not in warm_box["payload"]
def test_quality_gate_runs_heavy_compute_gate() -> None:
from scripts.run_quality_gate import CORE_PYTEST_TARGETS