fix(synastry): keep the picker order in the report title
The pair key stays sorted for dedup, and generation now loads the two people in the order they were chosen. The report back link uses Link so the new route passes the page-link lint. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-21 — 合盘两人顺序跟选人页一致
|
||||
|
||||
- 先选的人写在前面。等待页、报告标题、报告中心卡片和追问对话标题用同一个顺序。同一对已经有报告时,仍打开原来那份。Skill 版本不变(BUG-1407)。
|
||||
|
||||
## 2026-10-21 — 合盘正文不再重复节标题
|
||||
|
||||
- 某一节的正文如果以这一节的标题开头,质检会要求重写这一节。保存前也会把开头重复的标题去掉。中文和英文都是这样。Skill 版本不变(BUG-1406)。
|
||||
|
||||
@@ -37063,3 +37063,19 @@
|
||||
- 相关记录:issue #85。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/synastry-staging-fix-20261021`。
|
||||
|
||||
## BUG-1407 | 合盘两人顺序和选人页相反
|
||||
|
||||
- 状态:resolved(待 staging 真机复测)
|
||||
- 首次发现:2026-10-21(产品负责人真机)
|
||||
- 最近更新:2026-10-21
|
||||
- 影响面:合盘等待页、报告标题、报告中心卡片、追问对话标题
|
||||
- 用户现象:选人页先选甲、再选乙。等待页是甲 × 乙。写完之后,报告标题、报告中心卡片和追问对话标题变成乙 × 甲。
|
||||
- 触发条件:先选的那个人,编号排在后选的那个人后面。同一对已经有一份报告时,仍打开原来那份,顺序是当时保存的顺序。
|
||||
- 根因:去重用的一对编号按编号从小到大存放。生成时把较小的编号当成甲、较大的编号当成乙。等待页读的是选人页当场记下的顺序,所以只有等待页是对的。
|
||||
- 修复:创建时另存选人顺序(先选的人、后选的人)。生成按这个顺序加载两个人,报告标题、卡片摘要和追问标题都用这个顺序。去重用的编号排序不变。没有这份顺序的旧报告仍按原来的编号顺序。等待页本来就是选人顺序,没有改。
|
||||
- 验证:`frontend/tests/synastry-display-order.test.ts`:先选的人编号更大时,标题和追问标题仍是甲 × 乙;等待页句子是甲在前。`frontend/tests/database-synastry-full-create.test.ts`:请求先是本人、后是另一人时,存下的顺序是本人在前,而去重编号仍是另一人在前。
|
||||
- 防复发:上述两条。生成不要再把较小的编号直接当成甲。
|
||||
- 相关记录:issue #85。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/synastry-staging-fix-20261021`。
|
||||
|
||||
@@ -59,8 +59,30 @@
|
||||
- 合盘揭幕后的打印样式加上 `media="print"`,平时侧栏还在。
|
||||
- 失败态带上 `reportType`。个人报告失败断言的原值是没有这个字段,新值带上 `personal_full`,因为合盘地址要能认出个人报告。
|
||||
- 真机(执行方未做,不得写成已通过):点「开始合盘」后,侧栏当前项仍是「婚恋合盘」。
|
||||
- 左上角回到选人页仍是「‹ 婚恋合盘」。新路由让页面链接规则不接受普通 `<a href="/synastry">`,所以改成 `Link`,目的地不变。
|
||||
|
||||
## T9 重复标题(BUG-1406)
|
||||
|
||||
- 正文以该节标题开头时,质检返回 `repeated_heading`,只重写这一节。通过后保存前再剥掉开头的标题。中文和英文同一条规则。「沟通与理解力」这种紧挨着的字不剥。
|
||||
- 真机(执行方未做,不得写成已通过):打开一份新写的合盘,各节标题下面的第一句不要再重复标题。
|
||||
|
||||
## T10 两人顺序(BUG-1407)
|
||||
|
||||
- 根因:去重把两个人按编号从小到大存放,生成又把较小的编号当成甲。等待页用的是选人页当场记下的顺序,所以只有等待页没反。
|
||||
- 创建时把选人顺序另存一节。生成按这个顺序加载两个人,标题、卡片和追问标题跟着走。去重用的编号排序不动。同一对已经有报告时仍打开原来那份,顺序是当时保存的。
|
||||
- 真机(执行方未做,不得写成已通过):先选甲、再选乙。等待页、揭幕后的标题、报告中心卡片、从报告进的对话标题都是甲 × 乙。
|
||||
|
||||
## 这一轮检查
|
||||
|
||||
| 检查 | 结果 |
|
||||
| --- | --- |
|
||||
| `tsc --noEmit` | 0 错 |
|
||||
| 定向:滚动、重复标题、地址、顺序、报告渲染 | 通过,0 cancelled。失败态断言原值没有 `reportType`,新值带上 `personal_full` |
|
||||
| 质检 `tests/test_synastry_full_reader_gate.py` | 通过。正文以节标题开头判失败 |
|
||||
| 合盘创建真库(单独) | 通过。请求先是本人、后是另一人;顺序节是本人在前,去重编号是另一人在前 |
|
||||
| `npm run build` | `/` 与 `/synastry` 为 Static。`/synastry/[reportId]` 为 Dynamic。首屏引用的 js/css gzip -9:JS 577,319,CSS 70,518,合计 647,837。对照 643,725,差 +0.64%,在 ±2% 内 |
|
||||
| 全量 `eslint` | 0 error,122 warning,与基线相同。中途有过 1 个 error:新路由不接受 `<a href="/synastry">`。返回链接改成 `Link` 后消失 |
|
||||
|
||||
全量 Python 快速门没有跑:这个工作树没有项目虚拟环境。质检定向测试用系统 Python 跑过。
|
||||
|
||||
未推 staging,未改 `.gitea/`。真机未做,上面的真机步骤都不得写成已通过。
|
||||
|
||||
@@ -509,5 +509,5 @@
|
||||
| `TASK-niryaana-shoola-pvr-20261018.md` | `PROGRESS-niryaana-shoola-pvr-20261018.md` | #84 Niryaana Shoola 改 PVR 口径(我们的表与 Shoola 逐行相同;推翻 BUG-1283 所选 profile;占星师 7a773d13) | 待验收 | `codex/niryaana-shoola-pvr-20261018` |
|
||||
| `TASK-synastry-full-report-20261018.md` | #82 | 婚恋合盘:选人页 + 完整合盘报告(关系画像九节:结论+折叠的分析逻辑+关系时间线+技术依据摘要;英文按需;计费键 synastry.full) | 排队(等 #78、#79、#81) | `codex/synastry-full-report-20261018`;动表 |
|
||||
| `TASK-synastry-report-chat-20261018.md` | `PROGRESS-synastry-report-chat-20261018.md` | 从合盘报告某一节进入对话问依据(绑定报告与节、对话里可用术语、只依据已存计算结果并逐条核对、计费照普通对话) | 已验收 | `549e37b99`;动表 |
|
||||
| `TASK-synastry-staging-fix-20261021.md` | `PROGRESS-synastry-staging-fix-20261021.md` | #85 婚恋合盘 staging:P0 `.gte` 已在 staging(BUG-1399)。本分支补时间线起止、英文分析逻辑、删模型后旧对话改用默认模型、选人页开关文案,以及合盘创建真库测试(BUG-1400~1403) | 待验收 | `codex/synastry-staging-fix-20261021` |
|
||||
| `TASK-synastry-staging-fix-20261021.md` | `PROGRESS-synastry-staging-fix-20261021.md` | #85 婚恋合盘 staging:T3~T6 已在 staging `ff6b228dd`(BUG-1400~1403)。本分支继续报告滚动、独立地址、重复标题、两人顺序(BUG-1404~1407) | 待验收 | `codex/synastry-staging-fix-20261021` |
|
||||
| `TASK-synastry-report-chat-fix-20261020.md` | `PROGRESS-synastry-report-chat-20261018.md` | 大运核对要认报告里的「小运」和英文 mahadasha / antardasha;预填文案只留一句 | 已验收 | `549e37b99` |
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { SynastryWait } from "@/components/synastry/synastry-wait";
|
||||
import { beginSynastrySectionChat } from "@/lib/synastry-report-chat-client";
|
||||
@@ -131,7 +132,7 @@ export function SynastryReportView(props: {
|
||||
|
||||
return (
|
||||
<article className="synastry-report">
|
||||
<a className="synastry-report-back" href="/synastry">‹ 婚恋合盘</a>
|
||||
<Link className="synastry-report-back" href="/synastry">‹ 婚恋合盘</Link>
|
||||
<header className="synastry-report-heading">
|
||||
<h1>{title}</h1>
|
||||
<div className="synastry-report-meta">
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/** Selection order for a synastry report. The pair key stays sorted for dedup. */
|
||||
|
||||
export const SYNASTRY_ORDER_SECTION = "synastry_order";
|
||||
|
||||
export function synastryDisplaySubjectIds(low: string, high: string, order: unknown): [string, string] {
|
||||
const row = order && typeof order === "object" && !Array.isArray(order)
|
||||
? order as Record<string, unknown>
|
||||
: null;
|
||||
const first = typeof row?.first === "string" ? row.first : "";
|
||||
const second = typeof row?.second === "string" ? row.second : "";
|
||||
if (first !== second && (first === low || first === high) && (second === low || second === high)) {
|
||||
return [first, second];
|
||||
}
|
||||
return [low, high];
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type { DataQueryBuilder } from "@/lib/db/data-client";
|
||||
import { consultationSubjectFailureResponse } from "@/lib/consultation-subject-resolver";
|
||||
import { FeaturePricingError, resolveFeaturePricing } from "@/lib/feature-pricing";
|
||||
import { MARRIAGE_MATCH_RULES_VERSION, marriageMatchFingerprint } from "@/lib/marriage-match-service";
|
||||
import { SYNASTRY_ORDER_SECTION } from "@/lib/synastry-display-order";
|
||||
import { loadLanguageModelCatalog } from "@/lib/model-catalog";
|
||||
import { REPORT_STABLE_CODES } from "@/lib/personal-report-codes";
|
||||
import { readPersonalReportDailyLimit } from "@/lib/personal-report-entitlement";
|
||||
@@ -215,6 +216,24 @@ export async function createSynastryFullReport(input: Readonly<{
|
||||
await release(REPORT_STABLE_CODES.generationInProgress);
|
||||
return { status: 409, body: { error: "已有报告正在生成中", code: REPORT_STABLE_CODES.generationInProgress } };
|
||||
}
|
||||
const orderSaved = await (input.admin as {
|
||||
from(table: string): {
|
||||
insert(row: Record<string, unknown>): Promise<{ error: { message?: string } | null }>;
|
||||
};
|
||||
}).from("personal_report_sections").insert({
|
||||
user_id: input.userId,
|
||||
request_id: requestId,
|
||||
section_id: SYNASTRY_ORDER_SECTION,
|
||||
status: "ready",
|
||||
payload: { first: subjectA, second: subjectB },
|
||||
attempt_count: 1,
|
||||
max_attempts: 1,
|
||||
});
|
||||
if (orderSaved.error) {
|
||||
await persistence.markFailed(input.userId, created.record.id, REPORT_STABLE_CODES.generationFailed);
|
||||
await release(REPORT_STABLE_CODES.generationFailed);
|
||||
return { status: 503, body: { error: "报告任务暂时无法入队", code: REPORT_STABLE_CODES.generationFailed } };
|
||||
}
|
||||
try {
|
||||
const enqueued = await createSupabasePersonalReportJobService(input.admin as never).enqueue({
|
||||
userId: input.userId,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { PersonalReportWorkerError } from "@/lib/personal-report-worker-core";
|
||||
import type { PersonalReportRecord } from "@/lib/personal-report-service-core";
|
||||
import { loadSubjectBirth } from "@/lib/subject-birth";
|
||||
import { marriageMatchBirthBody } from "@/lib/marriage-match-service";
|
||||
import { SYNASTRY_ORDER_SECTION, synastryDisplaySubjectIds } from "@/lib/synastry-display-order";
|
||||
import { timelineForPair, writeSynastryLanguage, type SynastrySnapshot } from "@/lib/synastry-full-generate";
|
||||
import type { TimingPeriod } from "@/lib/synastry-full-timeline";
|
||||
|
||||
@@ -160,9 +161,21 @@ export async function runSynastryFullReport(input: Readonly<{
|
||||
}
|
||||
await input.onProgress?.({ phase: "computing_charts", completed: 0, total: 9 });
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const orderRow = await input.admin.from("personal_report_sections")
|
||||
.select("payload")
|
||||
.eq("user_id", input.report.userId)
|
||||
.eq("request_id", input.report.requestId)
|
||||
.eq("section_id", SYNASTRY_ORDER_SECTION)
|
||||
.maybeSingle();
|
||||
const orderPayload = orderRow.error ? null : asRecord(orderRow.data)?.payload ?? null;
|
||||
const [firstId, secondId] = synastryDisplaySubjectIds(
|
||||
input.report.subjectLow,
|
||||
input.report.subjectHigh,
|
||||
orderPayload,
|
||||
);
|
||||
const [personA, personB] = await Promise.all([
|
||||
loadSubjectBirth(input.admin, input.report.userId, input.report.subjectLow),
|
||||
loadSubjectBirth(input.admin, input.report.userId, input.report.subjectHigh),
|
||||
loadSubjectBirth(input.admin, input.report.userId, firstId),
|
||||
loadSubjectBirth(input.admin, input.report.userId, secondId),
|
||||
]);
|
||||
const packet = asRecord(await postJson("/api/synastry_full_dossier", {
|
||||
person_a: marriageMatchBirthBody(personA.row, today),
|
||||
|
||||
@@ -123,6 +123,10 @@ test("createSynastryFullReport reserves synastry.full and enqueues a job on the
|
||||
assert.equal(fixture.psql(`select status from public.personal_report_jobs where user_id = '${userId}'`), "queued");
|
||||
assert.equal(fixture.psql(`select status from public.usage_reservations where user_id = '${userId}'`), "reserved");
|
||||
assert.equal(fixture.psql(`select credits from public.profiles where id = '${userId}'`), "81");
|
||||
assert.equal(fixture.psql(`select payload->>'first' from public.personal_report_sections where user_id = '${userId}' and section_id = 'synastry_order'`), "self");
|
||||
assert.equal(fixture.psql(`select payload->>'second' from public.personal_report_sections where user_id = '${userId}' and section_id = 'synastry_order'`), personId);
|
||||
assert.equal(fixture.psql(`select subject_low from public.personal_reports where id = '${report?.id}'`), personId);
|
||||
assert.equal(fixture.psql(`select subject_high from public.personal_reports where id = '${report?.id}'`), "self");
|
||||
catalog.setModelCatalogIoForTests(null);
|
||||
catalog.invalidateLanguageModelCatalog();
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
import { synastryDisplaySubjectIds } from "../src/lib/synastry-display-order.ts";
|
||||
import { synastryFollowupTitle } from "../src/lib/synastry-report-session.ts";
|
||||
|
||||
test("picker order outranks the sorted pair key for the title and the chat", () => {
|
||||
const ids = synastryDisplaySubjectIds("person-yi", "self", { first: "self", second: "person-yi" });
|
||||
assert.deepEqual(ids, ["self", "person-yi"]);
|
||||
const labels: Record<string, string> = { self: "甲", "person-yi": "乙" };
|
||||
const names = { a: labels[ids[0]] ?? "", b: labels[ids[1]] ?? "" };
|
||||
assert.equal(`${names.a} × ${names.b} · 婚恋合盘`, "甲 × 乙 · 婚恋合盘");
|
||||
assert.equal(synastryFollowupTitle(names), "甲 × 乙 · 合盘追问");
|
||||
assert.deepEqual(synastryDisplaySubjectIds("person-yi", "self", null), ["person-yi", "self"]);
|
||||
const wait = readFileSync(new URL("../src/components/synastry/synastry-wait.tsx", import.meta.url), "utf8");
|
||||
const run = readFileSync(new URL("../src/lib/synastry-full-run.ts", import.meta.url), "utf8");
|
||||
assert.match(wait, /\$\{a\} × \$\{b\}/);
|
||||
assert.match(run, /synastryDisplaySubjectIds\(/);
|
||||
assert.match(run, /cardSummary: `\$\{names\.a\} × \$\{names\.b\} · 婚恋合盘`/);
|
||||
});
|
||||
Reference in New Issue
Block a user