Clarify default birth time trials in reports (#280)
Independent Staging Quality Gate / validate-python (pull_request) Successful in 10m19s
Independent Staging Quality Gate / validate-frontend (pull_request) Successful in 18m3s
Independent Staging Quality Gate / publish (pull_request) Skipped
Independent Staging Quality Gate / validate-python (push) Successful in 16m57s
Independent Staging Quality Gate / validate-frontend (push) Failing after 28m43s
Independent Staging Quality Gate / publish (push) Skipped
Independent Staging Quality Gate / validate-python (pull_request) Successful in 10m19s
Independent Staging Quality Gate / validate-frontend (pull_request) Successful in 18m3s
Independent Staging Quality Gate / publish (pull_request) Skipped
Independent Staging Quality Gate / validate-python (push) Successful in 16m57s
Independent Staging Quality Gate / validate-frontend (push) Failing after 28m43s
Independent Staging Quality Gate / publish (push) Skipped
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-11-15 — 报告注明默认出生时间范围是试算
|
||||
|
||||
- 个人报告、年运和合盘区分默认试算、自报误差和生时纠正区间。页面、导出与追问沿用同一口径,个人报告和年运在证据等级处说明默认试算。占星 Skill 版本不变(BUG-1891)。
|
||||
|
||||
## 2026-11-14 — 合盘按两个人各自的出生时间误差显示依据
|
||||
|
||||
- 计算附录显示甲乙各自的误差来源,以及本命上升、九分盘上升、月亮星宿与四分的核对结果。会受影响的技法标明参数敏感。占星 Skill 版本不变(BUG-1889)。
|
||||
|
||||
@@ -25513,3 +25513,14 @@
|
||||
- 验证与防复发:公开双盘使用默认窗口,合成用例分别覆盖甲方 D1、乙方 D9、月亮四分边界、两人稳定、自报误差、纠正记录复用及过时记录拒用。详见 [#276 进度](tasks/PROGRESS-synastry-precision-20261114.md)。
|
||||
- 关联记录:#224、#275、#276、BUG-1654、BUG-1679、BUG-1869。
|
||||
- 修复版本:`codex/synastry-precision-20261114`,待独立验收。
|
||||
|
||||
## BUG-1891 | 三类报告把默认出生时间试算显示为稳定
|
||||
|
||||
- 状态:fixed-pending-verify(待独立验收)。
|
||||
- 首次发现 / 最近更新:2026-11-15 / 2026-11-15。
|
||||
- 触发与现象:在没有填写出生时间误差时生成个人报告、年运或合盘,页面、导出或写作材料可能将默认前后各 30 分钟的试算写成「稳定」,部分等级收据也未披露这一前提。
|
||||
- 根因:展示层把计算状态直接映射为用户文案,证据等级披露未识别默认窗口来源。
|
||||
- 修复:按窗口来源描述实际核对范围,默认窗口的来源行说明试算限制。个人报告和年运将相关依据纳入等级披露,保留原有等级上限。合盘分别保留两方来源和变化候选。
|
||||
- 验证与防复发:针对三种窗口来源、披露次数、候选保留、等级上限及旧快照回读增加回归测试。详见 [#280 进度](tasks/PROGRESS-default-window-scenario-20261115.md)。
|
||||
- 关联记录:#224、#279、#280、BUG-1887、BUG-1889。
|
||||
- 修复版本:`codex/default-window-scenario-20261115`,待独立验收。
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# #280 · 默认出生时间窗口是试算(2026-11-15)
|
||||
|
||||
- 执行:Codex。staging 基线 `405876ad2e5a6312145d9061e5c738bd5dc506ac`,任务书 `2670542f129108823cc56f5e2624c4f5f2a2f6f5`。BUG-1890 由 #279 占用,本单使用任务书分配的 BUG-1891。
|
||||
- 计算窗口、采样与候选原值不变。已确认到分钟的窗口沿用原口径。本轮只更新三个报告及其追问材料,生时纠正及报告取窗逻辑由各自工单负责。
|
||||
|
||||
## 盘点
|
||||
|
||||
| 范围 | 位置与原行为 | 来源 | 新写法 |
|
||||
| --- | --- | --- | --- |
|
||||
| 个人报告页面、计算资料导出 | `professional_report_reference`、`upstream_sync_chapters` 出生资料,原不确定范围可只显示数值,未区分试算。 | 报告请求中归一化后的窗口。 | 来源行标明前后各 30 分钟为试算,并说明不能确认稳定。填写误差与纠正区间明确标名。 |
|
||||
| 个人报告附录 | `upstream_sync_chapters` 敏感性矩阵、裁决矩阵,原用「稳定」「窗口内稳定」;`audit_chapters` 分盘样本账本,原用「稳定性」。 | 三分钟矩阵固定核对中心及前后各一分钟,分盘账本使用自身样本。两者独立于报告窗口。 | 明确「三分钟对照中没有变化」或「已核对的样本没有变化」,不把不同试算范围互相替代。 |
|
||||
| 个人报告写作、证据与追问材料 | `reading_facts`、`birth_precision_margins` 以前写「区间内不换座」,`personal-report-reading` 只区分确认状态。 | 分盘边界投影使用报告的归一化窗口。 | 「按前后 30 分钟试算,没有变化」;已填写和已纠正的分别限定于其区间。写作输入禁止把试算升格为稳定或确认。 |
|
||||
| 个人报告证据等级 | `evidence_grade` 的 `purpose_stability` 与前端披露映射。 | 默认窗口。 | 内部记录 `default_window_scenario` 并披露,C、D 上限保持。 |
|
||||
| 年运启动页、参数及导出 | `birthNotice`、「稳定性」行和 `buildAppendix` 的默认范围。 | 年运等级资料的窗口来源;旧资料按已有字段。 | 默认来源行一次说明试算;核对结果限定于已检查样本;保留变化项、候选和日期区间。 |
|
||||
| 年运 KP 与差异列表 | `kpCuspAppendix`、`conflictLine` 原以「稳定性」或「在填的范围」描述默认结果。 | 年运窗口与 KP 自己的取样精度。 | 默认按试算描述,不把样本当成已填写的范围;原候选和样点仍展示。 |
|
||||
| 年运证据等级与写作输入 | `annual_service/evidence_grade`、`annual-report-grade`、`annual-report-sections`。以前稳态依据不披露。 | 默认窗口下既有 `stable` 状态。 | 仍算可用。披露 `default_window_scenario:<依赖>`,写作、附录和导出映射为「该依据按前后 30 分钟试算」。 |
|
||||
| 合盘页面、附录与导出 | `marriage_match_service/birth_precision` 的来源标签;`synastry-full-technical` 原用「误差内稳定」。 | 甲乙各自的来源及 D1、D9、月亮样本。 | 同一附录只说明一次试算限制,结果按人和范围陈述,候选原样保留。 |
|
||||
| 合盘写作和追问 | `sectionEvidence` 与 `precisionEvidence` 原只给变化层,追问可遗漏试算边界。 | 报告已保存的两人证据。 | 原候选继续作为条件证据,确认过范围的无变化项带相应措辞;写作和追问均保留试算限制。 |
|
||||
|
||||
新报告的个人等级计算收据的规范化 SHA-256 和年运等级块的 `sha256` 随披露字段变化。计算边界、依赖可用性和 C、D 上限不变。既有快照继续使用当时保存的段落和附录,不重新分级。合盘继续使用已保存的附录和分组收据。
|
||||
|
||||
| 改动的断言 | 原值 | 新值与原因 |
|
||||
| --- | --- | --- |
|
||||
| 个人报告回退后的默认来源标签 | 没有误差信息,按前后各 30 分钟 | 没有填写误差,按前后各 30 分钟试算。标明是假设窗口。 |
|
||||
| 个人报告边缘不换座 | 区间内不换座 | 按默认、自报、纠正来源分别写无变化。已确认分钟和会变化的断言保持。 |
|
||||
| 年运启动页默认误差提醒 | 按记录时间前后各 30 分钟核对 | 按前后各 30 分钟试算。这只是试算,不能确认稳定。 |
|
||||
| 合盘旧图例无来源的无变化行 | 误差内稳定 | 在已核对的范围内没有变化。旧输入不能补造范围来源。 |
|
||||
|
||||
## 验证
|
||||
|
||||
- 三类报告分别用仓库既有公开样例走原生计算。个人报告通过全量 Markdown 接口生成并核对写作资料,年运和合盘用真实计算结果渲染附录。三个默认窗口都在来源行披露一次试算。未调用 DeepSeek,带模型正文的浏览器验收交给独立验收方。
|
||||
- 前端定向 32 项通过,覆盖三种来源、年运等级与导出、合盘追问与旧快照。Python 定向与隐私标记检查合计 80 项通过。TypeScript、ESLint(0 错误)与 Next.js 构建通过。
|
||||
- 候选与 staging 基线都用本机同一套 Node 环境运行全量前端测试,测试进程不加载 Docker CLI。候选登记 3758 项、失败 45、取消 0、跳过 37。基线登记 3754 项、失败 46、取消 0、跳过 37。候选 45 个失败项都在基线上复现,主要涉及 WSL、数据库测试和 Windows 临时目录。基线多出的失败是回答时钟用例。
|
||||
- 两次构建均把首页 `/` 生成为静态。按生成的首页 HTML 去重统计其引用的 JS/CSS 文件,gzip 总量均为 607090 字节,变化 0%。
|
||||
- 开工预检的本地适配器检查通过,目录扫描与预检定向测试超时。Git fetch 与 Gitea API 已单独核对 staging 和任务书提交。
|
||||
|
||||
- Python 快速门单独运行快速配置的 Python 部分,前端检查另跑。1496 项通过、3 项失败、2 项跳过,另有 29 个子测试通过。三个失败均来自 `test_upstream_technique_layers.py` 的 Kaala 运限检查,逐项在同一 staging 基线上复现。
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
|
||||
代号与中文释义用括号,如「AK(自我)」「D9(婚姻)」。运期层级沿用「›」,Yogini 运名后的主星用括号。Saham 公式以「白天:」或「夜晚:」开头。
|
||||
|
||||
婚恋合盘技术依据的计算参数按甲乙各占一行,写出生时间误差来源和三项扫描结果。误差影响在甲方、乙方及两盘对照各组的原技法行内显示。计算列写「参数敏感」,用途列写「只作观察。出生时间误差内会变」。
|
||||
婚恋合盘技术依据的计算参数按甲乙各占一行,写出生时间误差来源和三项扫描结果。个人报告、年运和合盘在误差来源一行说明默认前后各 30 分钟只是试算,同一页或导出只说明一次。误差影响在甲方、乙方及两盘对照各组的原技法行内显示。计算列写「参数敏感」,用途列写「只作观察。出生时间误差内会变」。
|
||||
|
||||
### Palette
|
||||
|
||||
|
||||
@@ -373,6 +373,8 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
|
||||
|
||||
## 报告里的限制说明
|
||||
|
||||
没有填写出生时间误差时,误差来源写「没有填写误差,按前后各 30 分钟试算」,同页说明一次「这只是试算,不能确认稳定」。依赖试算窗口的结果如无变化,写「按前后 30 分钟试算,没有变化」。填写过误差或有纠正区间的,说明是在哪个范围里没有变化。
|
||||
|
||||
普通报告只留下人能读的正文、结论、行动建议和必要限制。不写字段名、状态码、评分、权重或执行账本。说不到日期就写「这次说不到具体哪一天。」依据没补上就写「有些依据还没补上,相关说法不能当成确定预测。」判断没闭合就写「有些判断还没闭合,不能写成确定结论。」不出现 `technique_truth`、`workflow_route` 这类键。
|
||||
|
||||
### 采用后判断不了的分盘(2026-10-01,BUG-1138 / BUG-1139)
|
||||
|
||||
@@ -251,7 +251,7 @@ function AnnualTimeline({ snapshot, failed }: Readonly<{ snapshot: AnnualSnapsho
|
||||
export function birthNotice(subject: AnnualOverview["subject"]): { text: string } | null {
|
||||
const who = subject.isSelf ? "你" : subject.name;
|
||||
const range = birthRangeText(subject.birthTime);
|
||||
if (range === null) return { text: `${who}没有填出生时间的误差范围,按记录时间前后各 30 分钟核对。` };
|
||||
if (range === null) return { text: `${who}没有填出生时间的误差范围,按前后各 30 分钟试算。这只是试算,不能确认稳定。` };
|
||||
if (range === "准确到分钟") return null;
|
||||
return { text: `出生时间按${who}填的误差(${range})两头都算过:会跟着变的地方只列候选,日期会写明可能前后移动多少。` };
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
type AnnualSectionKind,
|
||||
type AnnualWindowRow,
|
||||
} from "./annual-report-sections.ts";
|
||||
import { DEFAULT_WINDOW_DISCLOSURE_ZH, DEFAULT_WINDOW_LABEL_ZH, DEFAULT_WINDOW_SOURCE, unchangedWithinBirthWindow } from "./report-window-copy.ts";
|
||||
|
||||
export type AnnualSectionWriter = (input: Readonly<{
|
||||
system: string;
|
||||
@@ -161,13 +162,13 @@ function structureName(key: string): string {
|
||||
return STRUCTURE_CN[key] ?? key;
|
||||
}
|
||||
|
||||
function conflictLine(row: Row): string {
|
||||
function conflictLine(row: Row, scenario: boolean): string {
|
||||
const label = CONFLICT_CN[str(row.class)] ?? str(row.class);
|
||||
// BUG-1650: the declared range replaces the confirmed / unconfirmed switch and its 14-day widening.
|
||||
if (row.code === "birth_range_unknown") return `${label}:没有填出生时间误差,上升、分盘和运限日期是否会变无法核对,相关日期标为不确定`;
|
||||
if (row.code === "birth_structures_vary") return `${label}:在填的误差范围内会变的:${(Array.isArray(row.structures) ? row.structures as string[] : []).map(structureName).join("、")};依赖它们的判断只列候选`;
|
||||
if (row.code === "dasha_dates_shift") return `${label}:运限换段的日期在误差范围内最多前后移动约 ${String(row.max_days)} 天`;
|
||||
if (row.code === "birth_range_sampled") return `${label}:误差范围内每 ${String(row.resolution_minutes)} 分钟取样、变化处逐分钟定位;没变化的只能说「已检查样本一致」,不是整段都核实过`;
|
||||
if (row.code === "birth_structures_vary") return `${label}:${scenario ? "前后 30 分钟试算中会变的" : "在填的误差范围内会变的"}:${(Array.isArray(row.structures) ? row.structures as string[] : []).map(structureName).join("、")};依赖它们的判断只列候选`;
|
||||
if (row.code === "dasha_dates_shift") return `${label}:运限换段的日期${scenario ? "在前后 30 分钟试算中" : "在误差范围内"}最多前后移动约 ${String(row.max_days)} 天`;
|
||||
if (row.code === "birth_range_sampled") return `${label}:${scenario ? "前后 30 分钟试算中" : "误差范围内"}每 ${String(row.resolution_minutes)} 分钟取样、变化处逐分钟定位,只说明已检查样本`;
|
||||
if (row.code === "annual_chart_birth_range_not_checked") return `${label}:年盘上升和 Mudda / Patyayini 的日期按所用出生时间算,没有在误差两端重算`;
|
||||
if (row.code === "phase_split") return `${label}:${str(row.phase_id)} 这一段各领域方向不同,分开写,不合成一个总评`;
|
||||
if (row.code === "tracks_differ") return `${label}:${str(row.phase_id)} Vimshottari 和 Narayana 对同一领域方向相反。${NARAYANA_REFERENCE}。`;
|
||||
@@ -190,16 +191,23 @@ export function birthTimeLines(facts: unknown, birth: AnnualBirthTime): string[]
|
||||
const varies = Array.isArray(check.varies) ? check.varies as string[] : [];
|
||||
const range = birthRangeText(birth);
|
||||
const used = record(annualGradeBlock(facts).birth_window);
|
||||
const scenario = used.source === DEFAULT_WINDOW_SOURCE;
|
||||
const source = scenario ? `${DEFAULT_WINDOW_LABEL_ZH}(${DEFAULT_WINDOW_DISCLOSURE_ZH})` : range ?? "没有填";
|
||||
const checkLabel = scenario ? "范围核对" : "稳定性";
|
||||
const sampled = check.coverage !== "every_minute";
|
||||
const scope = scenario || used.source === "rectified_range" || used.source === "confirmed_minute"
|
||||
? used.source : range ? "user_stated" : undefined;
|
||||
const unchanged = unchangedWithinBirthWindow(scope);
|
||||
return [
|
||||
"出生资料:用户核对后录入",
|
||||
`时间来源:${(birth.source && BIRTH_SOURCE_CN[birth.source]) || "未标明"};误差:${range ?? (used.source === "default_unknown_30" ? "未填写,按记录时间前后各 30 分钟核对" : "没有填")}`,
|
||||
`时间来源:${(birth.source && BIRTH_SOURCE_CN[birth.source]) || "未标明"};误差:${source}`,
|
||||
`校正:${birth.rectification === "adopted" ? "采用了校正时间(是选定的时间,不等于误差已消失)" : "未校正"}`,
|
||||
// Second round, item 3: sampled minutes agreeing is not the whole range proven stable.
|
||||
check.status === "range_unknown" ? "稳定性:没有误差范围,无法核对,和出生时间有关的日期标为不确定"
|
||||
: check.status === "no_range" ? "稳定性:按填的时间计算,没有误差范围可核对"
|
||||
: `稳定性:本命、分盘、特殊点和年盘${check.coverage === "every_minute" ? "逐分钟核对" : `每 ${String(check.resolution_minutes)} 分钟取样、变化处逐分钟定位`};${
|
||||
varies.length > 0 ? `会变的有 ${varies.map(structureName).join("、")},只列候选` : "没有发现变化"}${
|
||||
check.coverage === "every_minute" ? "" : ";没变化的只能说已检查样本一致"}`,
|
||||
check.status === "range_unknown" ? `${checkLabel}:没有误差范围,无法核对,和出生时间有关的日期标为不确定`
|
||||
: check.status === "no_range" ? `${checkLabel}:按填的时间计算,没有误差范围可核对`
|
||||
: `${checkLabel}:本命、分盘、特殊点和年盘${sampled ? `每 ${String(check.resolution_minutes)} 分钟取样、变化处逐分钟定位` : "逐分钟核对"}。${
|
||||
varies.length > 0 ? `会变的有 ${varies.map(structureName).join("、")},只列候选` : unchanged}${
|
||||
sampled ? "。只说明已检查样本,没变化的只能说已检查样本一致" : ""}`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -755,6 +763,10 @@ function kpCuspAppendix(root: Row): AnnualAppendixBlock[] {
|
||||
const kp = record(record(root.page_appendix).kp_cusps);
|
||||
if (!kp.status) return [];
|
||||
const precision = record(kp.precision);
|
||||
const source = record(annualGradeBlock(root).birth_window).source;
|
||||
const scenario = source === DEFAULT_WINDOW_SOURCE;
|
||||
const checkedSource = source && !scenario && source !== "rectified_range" && source !== "confirmed_minute"
|
||||
? "user_stated" : source;
|
||||
const domains = record(kp.domains);
|
||||
const ayanamsa = AYANAMSA_NAMES[str(kp.ayanamsa)] ?? "未标明";
|
||||
const node = kp.node_mode === "true" ? "真交点" : "平交点";
|
||||
@@ -770,15 +782,18 @@ function kpCuspAppendix(root: Row): AnnualAppendixBlock[] {
|
||||
const cusp = `${title}第 ${row.house} 宫。记录时刻的宫头在${signZh(str(row.sign))}座,星宿主星为${planetZh(str(lords.nakshatra_lord))}。`;
|
||||
if (row.stability === "boundary_sensitive") return `${cusp}出生时间范围内子主有变化。逐分钟取样,${branches.map(band).join(";")}。`;
|
||||
const lord = `记录时刻的子主为${planetZh(str(lords.sub_lord))}。`;
|
||||
if (row.stability === "stable_in_window") return `${cusp}${lord}出生时间范围内的分钟样点一致。`;
|
||||
if (row.stability === "stable_in_window") return `${cusp}${lord}${checkedSource
|
||||
? `子主${unchangedWithinBirthWindow(checkedSource)}。`
|
||||
: "出生时间范围内的分钟样点一致。"}`;
|
||||
const variations = branches.length > 1 ? `参考取样中,${branches.map(band).join(";")}。` : "";
|
||||
return `${cusp}${lord}误差未知,稳定性未判定。${variations}`;
|
||||
return `${cusp}${lord}${scenario ? "误差未知,子主只供试算参考。" : "误差未知,稳定性未判定。"}${variations}`;
|
||||
}));
|
||||
return [{
|
||||
id: "kp-cusps", title: "KP 宫头(另一种分宫方法)", collapseAfterFirst: true,
|
||||
lines: kp.status === "computed" ? [
|
||||
`${str(kp.method_note)}。${precision.status === "range_unknown"
|
||||
? `${str(precision.note)}。参考取样用记录时间的前后各 30 分钟。` : ""}`,
|
||||
? scenario ? "用前后各 30 分钟试算。" : `${str(precision.note)}。参考取样用记录时间的前后各 30 分钟。`
|
||||
: ""}`,
|
||||
`采用 Placidus 宫头、${ayanamsa} 岁差与${node}。`,
|
||||
...lines,
|
||||
] : [str(kp.reason) || "KP 宫头未算出。"],
|
||||
@@ -810,6 +825,7 @@ export function buildAppendix(facts: unknown, range: AnnualSnapshot["range"], bi
|
||||
const parameters = record(root.parameters);
|
||||
const cards = record(root.cards);
|
||||
const config = record(cards.config);
|
||||
const scenario = record(annualGradeBlock(root).birth_window).source === DEFAULT_WINDOW_SOURCE;
|
||||
return [
|
||||
{
|
||||
id: "parameters",
|
||||
@@ -845,7 +861,7 @@ export function buildAppendix(facts: unknown, range: AnnualSnapshot["range"], bi
|
||||
"行运只算木星、土星、罗睺计都的换宫、停滞和精确经过本命点,只收与本命或报告领域有关的;木星、土星按 Parashari 星座级相照(木星 5、7、9,土星 3、7、10,同座另记)关联各领域的本命星,相照只是关联,不代表好坏;不用统一容许度;天象日期不等于事情发生的日子",
|
||||
`转折窗口:${String(config.window_cluster_days ?? 21)} 天内、指向同一领域、至少 ${String(config.window_min_independent_systems ?? 2)} 个互相独立的来源;罗睺计都算一条轴,Mudda 和 Patyayini 算一层,同一颗慢星的几次经过算一个来源;这些是展示参数,不是统计验证`,
|
||||
"精确相照时刻(木星 120°/180°/240°、土星 60°/180°/270°,及同座)单独列表:只记达到角度的时刻,同一次顺逆往返算一个序列,不设影响范围,也不进转折窗口",
|
||||
"出生时间按填的误差范围取样重算(本命、分盘、特殊点、运限日期,以及太阳回归、年盘、Mudda / Patyayini),变化处逐分钟定位:不变的照用,会变的列候选;日期给出可能落的整个范围,不截成一个核心日期",
|
||||
"本命、分盘、特殊点、运限日期、太阳回归、年盘、Mudda 和 Patyayini 都按参数页所列范围取样重算,变化处逐分钟定位。会变的列候选,日期给出可能落的整个范围。",
|
||||
"Tithi Pravesha 只作独立的年度视角,不并入 Tajika 年,也不进转折窗口",
|
||||
],
|
||||
},
|
||||
@@ -859,7 +875,7 @@ export function buildAppendix(facts: unknown, range: AnnualSnapshot["range"], bi
|
||||
{
|
||||
id: "conflicts",
|
||||
title: "分歧与限制",
|
||||
lines: rows(cards.conflicts).length > 0 ? rows(cards.conflicts).map(conflictLine) : ["没有记录到需要单独说明的分歧"],
|
||||
lines: rows(cards.conflicts).length > 0 ? rows(cards.conflicts).map((row) => conflictLine(row, scenario)) : ["没有记录到需要单独说明的分歧"],
|
||||
},
|
||||
{
|
||||
id: "not_included",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { DEFAULT_WINDOW_SCENARIO } from "./report-window-copy.ts";
|
||||
import { isSignName, signZh } from "./chart-view-labels.ts";
|
||||
import { EVIDENCE_GRADES, evidenceGradeTag } from "./personal-report-reading-contract.ts";
|
||||
|
||||
@@ -75,7 +76,10 @@ export function weakestCap(caps: readonly AnnualCap[], fallback: AnnualCap): Ann
|
||||
}
|
||||
|
||||
export function dependencyLabels(ids: readonly string[]): string[] {
|
||||
return ids.map((id) => DEPENDENCY_LABELS[id] ?? "计算依据");
|
||||
const scenario = `${DEFAULT_WINDOW_SCENARIO}:`;
|
||||
return ids.map((id) => id.startsWith(scenario)
|
||||
? `${DEPENDENCY_LABELS[id.slice(scenario.length)] ?? "计算依据"}按前后 30 分钟试算`
|
||||
: DEPENDENCY_LABELS[id] ?? "计算依据");
|
||||
}
|
||||
|
||||
export function varyingAnnualReasons(cap: AnnualCap): { label: string; reason: string }[] {
|
||||
|
||||
@@ -695,6 +695,7 @@ const WRITING_RULES = [
|
||||
"- 不给时间段打顺或紧的总评,不写偏顺、偏紧、有顺有紧、先紧后松、平稳;「基调」只决定建议的方向,写成具体该推什么、该守什么。",
|
||||
"- 可以写月份,例如「5 月下旬」,不写具体哪一天。材料里的日期是天象或运限换段的日子,不等于事情发生的那天;写成「这前后」「从这时起」。",
|
||||
"- 标着「日期不确定」或「会前后移动」的,写大概的时间并说明会因出生时间误差前后移动。",
|
||||
"- 标着「按前后 30 分钟试算」的依据,如果样本没有变化,就写「按前后 30 分钟试算,没有变化」。不得写成「稳定」「已确认」或「不会变」。",
|
||||
"- 「分领域」里各领域不一样时,分开写,不合成一个总的说法;看不出的领域直接说看不出。",
|
||||
"- 每一节只写这一块独有的事;前面几节已经写过的,后面半句话带过。建议具体到一件事和一个时间。",
|
||||
"必须遵守:",
|
||||
|
||||
@@ -245,6 +245,7 @@ const DEPENDENCY_LABELS: Readonly<Record<string, string>> = {
|
||||
"p:health:drekkana22": "第 22 个三分盘", "p:health:Badhaka": "阻碍宫和宫主",
|
||||
"t:transits": "行运", "t:annual": "年盘", "t:double_transit": "双行运",
|
||||
purpose_stability_not_evaluated: "未评估出生时间对这类判断的影响",
|
||||
default_window_scenario: "按前后 30 分钟试算",
|
||||
};
|
||||
|
||||
export function basisAuditLine(row: BasisAuditRow): string {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { asRecord } from "./guards";
|
||||
import { DEFAULT_WINDOW_LABEL_ZH, DEFAULT_WINDOW_SOURCE, unchangedWithinBirthWindow } from "./report-window-copy";
|
||||
/**
|
||||
* The model-written reading at the front of the personal report (issue #113 R1,
|
||||
* product decision 2026-10-27 「R1 C1」).
|
||||
@@ -641,8 +643,15 @@ export function acceptReadingFacts(value: unknown): Record<string, unknown> | nu
|
||||
export const READING_FACTS_SCHEMA = "report-reading-facts-v3";
|
||||
|
||||
export function reportReadingUserMessage(facts: Record<string, unknown>, birthTimeConfirmed: boolean): string {
|
||||
const precision = asRecord(facts.birth_precision);
|
||||
const window = asRecord(precision?.window);
|
||||
const scenario = window?.source === DEFAULT_WINDOW_SOURCE;
|
||||
const description = scenario ? DEFAULT_WINDOW_LABEL_ZH
|
||||
: typeof window?.label_zh === "string" ? window.label_zh
|
||||
: birthTimeConfirmed ? "已确认" : "用户填报,没有确认到分钟";
|
||||
return [
|
||||
birthTimeConfirmed ? "出生时间:已确认。" : "出生时间:用户填报,没有确认到分钟。",
|
||||
`出生时间:${description}。`,
|
||||
...(scenario ? [`没有变化的层写「${unchangedWithinBirthWindow(DEFAULT_WINDOW_SOURCE)}」。不得写成「稳定」「已确认」或「不会变」。`] : []),
|
||||
"盘面事实(引擎已算好,只能用这些):",
|
||||
JSON.stringify(facts),
|
||||
].join("\n");
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export const DEFAULT_WINDOW_SOURCE = "default_unknown_30";
|
||||
export const DEFAULT_WINDOW_SCENARIO = "default_window_scenario";
|
||||
export const DEFAULT_WINDOW_LABEL_ZH = "没有填写误差,按前后各 30 分钟试算";
|
||||
export const DEFAULT_WINDOW_DISCLOSURE_ZH = "这只是试算,不能确认稳定";
|
||||
|
||||
export function unchangedWithinBirthWindow(source: unknown, language: "zh" | "en" = "zh"): string {
|
||||
if (source === DEFAULT_WINDOW_SOURCE) {
|
||||
return language === "zh" ? "按前后 30 分钟试算,没有变化" : "showed no change in the 30-minute trial on either side";
|
||||
}
|
||||
if (source === "rectified_range") {
|
||||
return language === "zh" ? "在生时纠正选定的区间内没有变化" : "showed no change in the rectified interval";
|
||||
}
|
||||
if (source === "user_stated" || source === "shichen" || source === "period") {
|
||||
return language === "zh" ? "在填写的误差范围内没有变化" : "showed no change in the declared range";
|
||||
}
|
||||
if (source === "confirmed_minute") {
|
||||
return language === "zh" ? "误差内稳定" : "was stable within the checked tolerance";
|
||||
}
|
||||
return language === "zh" ? "在已核对的范围内没有变化" : "showed no change in the checked interval";
|
||||
}
|
||||
@@ -372,6 +372,7 @@ const ZH_SYSTEM = `你在写一份专业的婚恋合盘报告的一节,按占
|
||||
reading_scope:status 是 limited 时,不写全量结论,每个判断写成条件,不得用 multi_system_consensus;blocked 时只说明为什么无法判断。
|
||||
证据等级:parameter_sensitive、user_history_verification_required、blocked 照实保留,不要升级成确定的话。
|
||||
出生时间误差内会变的层只能写成条件性证据。它们已经算出,不写成缺少或没有算出。
|
||||
只用前后 30 分钟试算的结果时,样本没有变化就写「按前后 30 分钟试算,没有变化」,不得写成「稳定」「已确认」或「不会变」。
|
||||
不要报分数。不要用「克 / 煞 / 凶」贴标签,也不用「吉」字下判语(「功能吉星」「功能凶星」作为术语可以用)。不要说「注定」「必然」「命中」「一定会」。不要把一方说成问题源。不要替当事人做决定。不要点名补充方法(西方占星、宿曜、问事盘),也不要写出生日期、时间、地点。不要写某年结婚、领证或分开。第一句不要写成「这段关系的核心不是……」,正文也不要以这一节的标题开头。两人运限日期同时落在一段时间里,只说明各自的背景,不等于共同的事件窗口。
|
||||
示例(别人的盘,只学写法):${ZH_EXAMPLE}
|
||||
每个判断给两到三条分析。盘面行必须逐字来自证据里的某个完整字段,不要改写。证据互相冲突时把 label 降为 parameter_sensitive 或 blocked,并写成条件,不要写成应期。
|
||||
@@ -383,6 +384,7 @@ Terms: Jyotish terms are allowed (house, lord, ascendant, D9, UL, DK, A7, dasha,
|
||||
reading_scope: when its status is limited, give no full conclusion, write every judgment as a condition and never use multi_system_consensus; when blocked, only say why no judgment is possible.
|
||||
Evidence labels: keep parameter_sensitive, user_history_verification_required and blocked as they are; never upgrade them into certainty.
|
||||
Layers that vary within a person's birth-time window are computed and conditional. Do not describe them as missing or certain.
|
||||
If a result only uses the default 30-minute trial on either side, describe matching samples as no change in that trial. Never call the result stable, confirmed, or certain to stay the same.
|
||||
Do not quote a score. Do not use soulmate, destined, inevitable, guaranteed. Do not call one person the problem. Do not decide for them. Do not name supplementary methods (Western astrology, Shukuyo, horary), and do not include birth dates, times, or places. Do not predict a wedding or a breakup in a named year. Do not open with "The core of this relationship is not". Do not start the body with the section title. Two people's periods running at the same time only describe each person's background; they are not a shared event window.
|
||||
Example (someone else's chart, for the voice only): ${EN_EXAMPLE}
|
||||
Give two or three supports for each judgment. The chart line must copy one complete evidence field verbatim. When supports conflict, lower the label to parameter_sensitive or blocked and write a condition, not a dated event.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DEFAULT_WINDOW_DISCLOSURE_ZH, DEFAULT_WINDOW_LABEL_ZH, DEFAULT_WINDOW_SOURCE, unchangedWithinBirthWindow } from "./report-window-copy.ts";
|
||||
import { AYANAMSA_OPTIONS } from "@/lib/ayanamsa";
|
||||
import { signZh } from "./chart-view-labels.ts";
|
||||
import { NAKSHATRA_ZH } from "./panchanga-names-zh.ts";
|
||||
@@ -181,13 +182,16 @@ const PRECISION_LAYERS_EN: Record<string, string> = {
|
||||
};
|
||||
|
||||
function precisionLines(precision: unknown): string[] {
|
||||
let scenarioDisclosed = false;
|
||||
return ["person_a", "person_b"].flatMap((side, index) => {
|
||||
const person = record(record(precision)?.[side]);
|
||||
if (!person) return [];
|
||||
const axes = record(person.axes) ?? {};
|
||||
const source = record(person.window)?.source;
|
||||
const scenario = source === DEFAULT_WINDOW_SOURCE;
|
||||
const summary = Object.entries(PRECISION_NAMES).map(([key, label]) => {
|
||||
const row = record(axes[key]);
|
||||
if (row?.status !== "varies") return `${label}${row?.status === "stable" ? "误差内稳定" : "暂无法核对"}`;
|
||||
if (row?.status !== "varies") return `${label}${row?.status === "stable" ? unchangedWithinBirthWindow(source) : "暂无法核对"}`;
|
||||
const candidates = Array.isArray(row.candidates) ? row.candidates.flatMap((value) => {
|
||||
const item = record(value);
|
||||
if (!item) return [];
|
||||
@@ -202,22 +206,41 @@ function precisionLines(precision: unknown): string[] {
|
||||
}) : [];
|
||||
return `${label}会变${candidates.length ? `,可能是${candidates.join("、")}` : ""}`;
|
||||
});
|
||||
return [`${index === 0 ? "甲方" : "乙方"}出生时间误差:${text(person.window_label)}。${summary.join(",")}。`];
|
||||
const label = scenario ? DEFAULT_WINDOW_LABEL_ZH : text(person.window_label);
|
||||
const disclosure = scenario && !scenarioDisclosed ? `(${DEFAULT_WINDOW_DISCLOSURE_ZH})` : "";
|
||||
scenarioDisclosed ||= scenario;
|
||||
return [`${index === 0 ? "甲方" : "乙方"}出生时间误差:${label}${disclosure}。${summary.join(",")}。`];
|
||||
});
|
||||
}
|
||||
|
||||
export function precisionEvidence(dossier: unknown, language: "zh" | "en"): string {
|
||||
const precision = record(record(dossier)?.birth_precision);
|
||||
const affected = ["person_a", "person_b"].flatMap((side, index) => {
|
||||
const layers = record(precision?.[side])?.affected_layers;
|
||||
const person = record(precision?.[side]);
|
||||
const source = record(person?.window)?.source;
|
||||
const scenario = source === DEFAULT_WINDOW_SOURCE;
|
||||
const layers = person?.affected_layers;
|
||||
const labels = Array.isArray(layers) ? layers.filter((key): key is string => typeof key === "string" && Boolean(PRECISION_LAYERS[key])) : [];
|
||||
if (!labels.length) return [];
|
||||
if (language === "zh") return [`${index === 0 ? "甲方" : "乙方"}的${labels.map((key) => PRECISION_LAYERS[key]).join("、")}`];
|
||||
return [`${index === 0 ? "Person A" : "Person B"}: ${labels.map((key) => PRECISION_LAYERS_EN[key]).join(", ")}`];
|
||||
const axes = record(person?.axes);
|
||||
const checked = Object.entries(axes ?? {}).filter(([, row]) => record(row)?.status === "stable");
|
||||
const label = language === "zh" ? index === 0 ? "甲方" : "乙方" : index === 0 ? "Person A" : "Person B";
|
||||
const lines = [];
|
||||
if (labels.length) {
|
||||
lines.push(language === "zh"
|
||||
? `${label}的${labels.map((key) => PRECISION_LAYERS[key]).join("、")}${scenario ? "在前后 30 分钟试算中" : "在出生时间误差内"}会变。`
|
||||
: `${label}: ${labels.map((key) => PRECISION_LAYERS_EN[key]).join(", ")} vary within the checked birth-time window.`);
|
||||
}
|
||||
if (source && checked.length) {
|
||||
const names = language === "zh"
|
||||
? checked.map(([key]) => PRECISION_NAMES[key]).filter(Boolean).join("、")
|
||||
: checked.map(([key]) => ({ D1: "rising sign", D9: "Navamsa ascendant", moon: "lunar nakshatra and pada" })[key]).filter(Boolean).join(", ");
|
||||
if (names) lines.push(language === "zh"
|
||||
? `${label}的${names}${unchangedWithinBirthWindow(source)}。`
|
||||
: `${label}: ${names} ${unchangedWithinBirthWindow(source, language)}.`);
|
||||
}
|
||||
return lines;
|
||||
});
|
||||
if (!affected.length) return "";
|
||||
return language === "zh" ? `${affected.join(",以及")}在出生时间误差内会变。`
|
||||
: `${affected.join(" / ")}. These layers vary within the birth-time window.`;
|
||||
return affected.join(" ");
|
||||
}
|
||||
|
||||
/** Seven kutas, each with its own ceiling; Varna (caste) is not scored. */
|
||||
|
||||
@@ -10,6 +10,7 @@ export const SYNASTRY_CHAT_INSTRUCTIONS = [
|
||||
"回答前必须先调用 readSynastrySection。没拿到材料就不要写盘面。",
|
||||
"只使用工具返回的内容。可以写星曜、宫位、星座、相位、大运和起止。不要重新排盘,不要补充材料里没有的盘面事实。",
|
||||
"材料没写到的,直接说「这份合盘没算到」,并建议回到各自的盘单独问。",
|
||||
"前后 30 分钟只是试算。材料说试算里没有变化,就保留试算口径,不写「稳定」「已确认」或「不会变」。",
|
||||
"口吻像一个懂行、说话温和的姐姐。说「你们」,不替两个人做决定。",
|
||||
"不说合不合、注定、必然、灵魂伴侣。不写某年结婚、领证或分开。",
|
||||
"不要索要或复述出生日期、出生地、经纬度。用户用英文问,就用英文答。",
|
||||
|
||||
@@ -327,7 +327,7 @@ test("the overlay matches the approved prototype", () => {
|
||||
// 新值:按误差范围说明;没填误差直说日期不确定;准确到分钟不提示;校正与否不影响
|
||||
// 原因:BUG-1650(占星师 2026-11-06 第 11 条;产品 2026-11-06)
|
||||
assert.match(birthNotice({ ...overview.subject, isSelf: false, name: "阿宁", birthTime: { ...BIRTH, known: false, beforeMinutes: null, afterMinutes: null } })?.text ?? "",
|
||||
/^阿宁没有填出生时间的误差范围,按记录时间前后各 30 分钟核对。$/);
|
||||
/^阿宁没有填出生时间的误差范围,按前后各 30 分钟试算。这只是试算,不能确认稳定。$/);
|
||||
assert.equal(birthNotice({ ...overview.subject, birthTime: { ...BIRTH, beforeMinutes: 0, afterMinutes: 0 } }), null);
|
||||
assert.ok(birthNotice({ ...overview.subject, birthTime: { ...BIRTH, rectification: "adopted" } }), "an adopted time still reads the declared range");
|
||||
assert.equal(priceLine(null), "计费配置不可用");
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { SynastryReportView } from "../src/components/synastry/synastry-report-view";
|
||||
import { buildAnnualExportBlocks } from "../src/lib/annual-report-export";
|
||||
import { buildAppendix, type AnnualSnapshot } from "../src/lib/annual-report-generate";
|
||||
import { dependencyLabels } from "../src/lib/annual-report-grade";
|
||||
import { reportReadingUserMessage } from "../src/lib/personal-report-reading";
|
||||
import { buildTechnicalBasis } from "../src/lib/synastry-full-technical";
|
||||
import { synastrySectionMaterial } from "../src/lib/synastry-report-material";
|
||||
import { DEFAULT_WINDOW_DISCLOSURE_ZH } from "../src/lib/report-window-copy";
|
||||
|
||||
const range = { kind: "birthday_year", key: "annual:birthday_year:2026", label: "生日年度",
|
||||
span: "2026 到 2027", crossesBirthday: false, birthdays: [] } as const;
|
||||
const sources = [
|
||||
["default_unknown_30", "按前后 30 分钟试算,没有变化", "没有填写误差,按前后各 30 分钟试算"],
|
||||
["user_stated", "在填写的误差范围内没有变化", "填写的误差前后各 30 分钟"],
|
||||
["rectified_range", "在生时纠正选定的区间内没有变化", "生时纠正选定的区间"],
|
||||
] as const;
|
||||
|
||||
for (const [source, unchanged, label] of sources) {
|
||||
test(`report scopes stay bounded: ${source}`, () => {
|
||||
const scenario = source === "default_unknown_30";
|
||||
const window = { source, before_minutes: 30, after_minutes: 30 };
|
||||
const person = { window, window_label: label, affected_layers: [], axes: {
|
||||
D1: { status: "stable" }, D9: { status: "stable" }, moon: { status: "stable" },
|
||||
} };
|
||||
const dossier = { birth_precision: { person_a: person, person_b: person } };
|
||||
const technical = buildTechnicalBasis({ dossier, names: { a: "甲", b: "乙" }, contacts: [], timing: [] });
|
||||
const html = renderToStaticMarkup(<SynastryReportView reportId="fixture" snapshot={{ names: { a: "甲", b: "乙" }, technical }} />);
|
||||
assert.ok(html.includes(unchanged));
|
||||
assert.equal(html.split(DEFAULT_WINDOW_DISCLOSURE_ZH).length - 1, Number(scenario));
|
||||
const material = synastrySectionMaterial({ payload: { dossier }, sectionId: "timeline" });
|
||||
assert.ok(JSON.stringify(material.evidence).includes(unchanged));
|
||||
|
||||
const facts = { cards: { evidence_grades: { birth_window: window } },
|
||||
natal_base: { birth_time: { check: { status: "checked", coverage: "every_minute", varies: [] } } } };
|
||||
const birth = { known: !scenario, beforeMinutes: scenario ? null : 30, afterMinutes: scenario ? null : 30,
|
||||
source: "approximate", rectification: "none" } as const;
|
||||
const snapshot: AnnualSnapshot = {
|
||||
kind: "annual_report_snapshot", version: 1, status: "ready", range,
|
||||
subject: { name: "虚构", isSelf: true }, plan: [], sections: {}, windows: [], remaining: [], factsSha256: "",
|
||||
appendix: buildAppendix(facts, range, birth),
|
||||
};
|
||||
const parameters = snapshot.appendix.find((part) => part.id === "parameters")!.lines.join("\n");
|
||||
assert.ok(parameters.includes(unchanged));
|
||||
assert.equal(parameters.split(DEFAULT_WINDOW_DISCLOSURE_ZH).length - 1, Number(scenario));
|
||||
assert.ok(JSON.stringify(buildAnnualExportBlocks(snapshot)).includes(unchanged));
|
||||
|
||||
const writer = reportReadingUserMessage({ birth_precision: { window: { ...window, label_zh: label } } }, false);
|
||||
assert.ok(writer.includes(label));
|
||||
if (scenario) {
|
||||
assert.ok(writer.includes(unchanged));
|
||||
assert.match(writer, /不得写成/);
|
||||
assert.doesNotMatch(parameters.replace(DEFAULT_WINDOW_DISCLOSURE_ZH, ""), /稳定|已确认|[_·]/);
|
||||
assert.doesNotMatch(technical.find((part) => part.id === "parameters")?.lines?.join("").replace(DEFAULT_WINDOW_DISCLOSURE_ZH, "") ?? "", /稳定|已确认|[_·]/);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("grade disclosure stays human readable and old snapshots keep saved appendix text", () => {
|
||||
assert.deepEqual(dependencyLabels(["default_window_scenario:D10", "kp"]), ["事业分盘上升按前后 30 分钟试算", "KP 宫头"]);
|
||||
const old = "旧报告当时保存的参数说明";
|
||||
const snapshot: AnnualSnapshot = {
|
||||
kind: "annual_report_snapshot", version: 1, status: "ready", range,
|
||||
subject: { name: "虚构", isSelf: true }, plan: [], sections: {}, windows: [], remaining: [], factsSha256: "",
|
||||
appendix: [{ id: "parameters", title: "计算参数", lines: [old] }],
|
||||
};
|
||||
assert.ok(JSON.stringify(buildAnnualExportBlocks(snapshot)).includes(old));
|
||||
});
|
||||
@@ -59,7 +59,7 @@ test("the appendix renders window sources, Chinese candidates and only the affec
|
||||
.split('<details class="synastry-technical">')[1];
|
||||
assert.match(appendix, /甲方出生时间误差:没有误差信息,按前后各 30 分钟/);
|
||||
assert.match(appendix, /水瓶座(档案时刻)、双鱼座(偏晚约 12 分钟)/);
|
||||
assert.match(appendix, /乙方出生时间误差[^<]*本命上升误差内稳定/);
|
||||
assert.match(appendix, /乙方出生时间误差[^<]*本命上升在已核对的范围内没有变化/);
|
||||
assert.match(appendix, /<h4>甲方<\/h4>[\s\S]*?<td>D1\/Rashi<\/td><td>参数敏感<\/td><td>只作观察。出生时间误差内会变<\/td>/);
|
||||
assert.match(appendix, /<h4>乙方<\/h4>[\s\S]*?<td>D1\/Rashi<\/td><td>已执行<\/td><td>供写作参考<\/td>/);
|
||||
assert.match(appendix, /<h4>两盘对照<\/h4>[\s\S]*?<td>宫位对照<\/td><td>参数敏感<\/td><td>只作观察。出生时间误差内会变<\/td>/);
|
||||
|
||||
@@ -2,12 +2,17 @@
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from functools import partial
|
||||
|
||||
try:
|
||||
from report_render.birth_precision_window import DEFAULT_TOLERANCE_MINUTES, WINDOW_POLICY
|
||||
from report_render.birth_precision_window import (
|
||||
DEFAULT_TOLERANCE_MINUTES, DEFAULT_WINDOW_SCENARIO, DEFAULT_WINDOW_SOURCE, WINDOW_POLICY,
|
||||
)
|
||||
from report_render.evidence_grade import GRADE_CONTRACT, GRADE_ORDER, MISSING_STATES
|
||||
except ModuleNotFoundError: # repository-root imports
|
||||
from scripts.report_render.birth_precision_window import DEFAULT_TOLERANCE_MINUTES, WINDOW_POLICY
|
||||
from scripts.report_render.birth_precision_window import (
|
||||
DEFAULT_TOLERANCE_MINUTES, DEFAULT_WINDOW_SCENARIO, DEFAULT_WINDOW_SOURCE, WINDOW_POLICY,
|
||||
)
|
||||
from scripts.report_render.evidence_grade import GRADE_CONTRACT, GRADE_ORDER, MISSING_STATES
|
||||
|
||||
|
||||
@@ -27,7 +32,7 @@ def annual_birth_window(window: dict) -> dict:
|
||||
return window
|
||||
minutes = int(DEFAULT_TOLERANCE_MINUTES)
|
||||
return {**window, "known": True, "before_minutes": minutes, "after_minutes": minutes,
|
||||
"source": "default_unknown_30"}
|
||||
"source": DEFAULT_WINDOW_SOURCE}
|
||||
|
||||
|
||||
def _state(block: dict) -> str:
|
||||
@@ -44,7 +49,7 @@ def _stability(row: dict) -> str:
|
||||
return row.get("stability") or "unknown"
|
||||
|
||||
|
||||
def _grade(required, checked, states, candidates=None, disclosures=()):
|
||||
def _grade(required, checked, states, candidates=None, disclosures=(), *, default_window=False):
|
||||
needed = [{"id": key, "state": states.get(key, "missing")} for key in required]
|
||||
for row in needed:
|
||||
if candidates and row["id"] in candidates and row["state"] == "varies":
|
||||
@@ -54,6 +59,8 @@ def _grade(required, checked, states, candidates=None, disclosures=()):
|
||||
disclose.extend(key for key in checked if states.get(key, "missing") in (
|
||||
MISSING_STATES | {"partial", "parameter_sensitive", "varies", "unknown", "samples_consistent", "branch_unresolved"}
|
||||
))
|
||||
if default_window:
|
||||
disclose.extend(f"{DEFAULT_WINDOW_SCENARIO}:{key}" for key in (*required, *checked) if states.get(key) == "stable")
|
||||
disclose.extend(disclosures)
|
||||
return {"cap": GRADE_ORDER[3 if missing else 2], "required": needed, "missing_required": missing,
|
||||
"must_disclose": list(dict.fromkeys(disclose))}
|
||||
@@ -72,6 +79,7 @@ def _window_precision(window: dict) -> str:
|
||||
|
||||
|
||||
def evidence_grades(package: dict, natal: dict, cards: dict, window: dict) -> dict:
|
||||
grade = partial(_grade, default_window=window.get("source") == DEFAULT_WINDOW_SOURCE)
|
||||
periods = package.get("natal_periods") or {}
|
||||
vim = periods.get("vimshottari") or {}
|
||||
narayana = periods.get("narayana") or {}
|
||||
@@ -105,8 +113,8 @@ def evidence_grades(package: dict, natal: dict, cards: dict, window: dict) -> di
|
||||
"contract": GRADE_CONTRACT,
|
||||
"precision_policy": {"id": WINDOW_POLICY, "default_minutes_each_side": DEFAULT_TOLERANCE_MINUTES},
|
||||
"birth_window": {key: window.get(key) for key in ("before_minutes", "after_minutes", "source")},
|
||||
"annual_overview": _grade(("annual_chart", *TIMING), ANNUAL_CHECKS, states),
|
||||
"annual_person": _grade(("functional", "D1"), ("D9", "shadbala", "avastha"),
|
||||
"annual_overview": grade(("annual_chart", *TIMING), ANNUAL_CHECKS, states),
|
||||
"annual_person": grade(("functional", "D1"), ("D9", "shadbala", "avastha"),
|
||||
{**states, "D1": _stability(lagna), "D9": _stability(d9),
|
||||
"shadbala": _state(natal.get("shadbala") or {}),
|
||||
"avastha": _state(natal.get("avastha") or {})},
|
||||
@@ -116,16 +124,16 @@ def evidence_grades(package: dict, natal: dict, cards: dict, window: dict) -> di
|
||||
vargas = {row["division"]: row for row in (domains.get(domain) or {}).get("vargas", [])}
|
||||
required = ("functional", "annual_chart", "vimshottari") + ((primary,) if primary else ())
|
||||
checked = DOMAIN_CHECKS + {"wealth": ("D11",), "relationship": ("bhrigu_pada",)}.get(domain, ())
|
||||
result[f"annual_domain_{domain}"] = _grade(
|
||||
result[f"annual_domain_{domain}"] = grade(
|
||||
required, checked,
|
||||
{**states, **{key: _stability(row) if _state(row) == "computed" else _state(row)
|
||||
for key, row in vargas.items()}},
|
||||
{key: row.get("candidates", []) for key, row in vargas.items()},
|
||||
)
|
||||
|
||||
result["annual_timing_window"] = _grade(TIMING, WINDOW_CHECKS, states)
|
||||
result["annual_timing_window"] = grade(TIMING, WINDOW_CHECKS, states)
|
||||
result["windows"] = {
|
||||
row["id"]: _grade((*TIMING, "window_precision"), WINDOW_CHECKS,
|
||||
row["id"]: grade((*TIMING, "window_precision"), WINDOW_CHECKS,
|
||||
{**states, "window_precision": _window_precision(row)},
|
||||
disclosures=("window_date_uncertain",) if row.get("dates_uncertain") else ())
|
||||
for row in cards.get("windows", [])
|
||||
|
||||
@@ -246,6 +246,7 @@ def _report_cache_identity(
|
||||
include_fact_tables: bool,
|
||||
sha: str,
|
||||
reading: dict[str, Any] | None = None,
|
||||
birth_window: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"birth": birth,
|
||||
@@ -259,6 +260,7 @@ def _report_cache_identity(
|
||||
# 占星师 2026-11-08 第六轮 R1:缓存键含 Narayana 全套 profile 与算法版本(不只靠 commit)。
|
||||
"narayana_profile_config": _narayana_profile_config(),
|
||||
"include_reading_facts": reading is not None,
|
||||
**({"birth_window": birth_window} if birth_window is not None else {}),
|
||||
**({"reading": reading} if reading is not None else {}),
|
||||
}
|
||||
|
||||
@@ -273,6 +275,7 @@ def _assemble_professional_report_reference(
|
||||
include_fact_tables: bool,
|
||||
engine,
|
||||
reading: dict[str, Any] | None = None,
|
||||
birth_window: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
args = _export_args(birth)
|
||||
engine_context = {key: value for key, value in vars(args).items()
|
||||
@@ -296,6 +299,8 @@ def _assemble_professional_report_reference(
|
||||
packet["report_version"] = REPORT_VERSION_FULL
|
||||
if output_format == "markdown":
|
||||
if edition == FULL_DATA_EDITION:
|
||||
if birth_window is not None:
|
||||
packet["report_birth_window"] = birth_window
|
||||
if "gender" in birth:
|
||||
# Prime the report's shared yoga projection before either edition renders.
|
||||
report_yoga_conditions(packet, gender=birth["gender"])
|
||||
@@ -325,15 +330,11 @@ def _assemble_professional_report_reference(
|
||||
}
|
||||
|
||||
|
||||
def _reading_request(body: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""`include_reading_facts: true` asks for the reading facts; off by default."""
|
||||
if body.get("include_reading_facts") is not True:
|
||||
return None
|
||||
def _report_window_options(body: dict[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
from scripts.consult_card_domain_facts import minute_confirmed_from_body
|
||||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution path
|
||||
from consult_card_domain_facts import minute_confirmed_from_body
|
||||
today = body.get("today")
|
||||
minute_confirmed = minute_confirmed_from_body(body) and body.get("reading_minute_confirmed") is not False
|
||||
window = body.get("birth_time_window")
|
||||
if window is None and body.get("birth_time_uncertainty_known") is not False:
|
||||
@@ -342,6 +343,16 @@ def _reading_request(body: dict[str, Any]) -> dict[str, Any] | None:
|
||||
return {
|
||||
"minute_confirmed": minute_confirmed,
|
||||
"birth_time_window": report_birth_window(window, minute_confirmed=minute_confirmed),
|
||||
}
|
||||
|
||||
|
||||
def _reading_request(body: dict[str, Any], window_options: dict[str, Any] | None = None) -> dict[str, Any] | None:
|
||||
"""`include_reading_facts: true` asks for the reading facts; off by default."""
|
||||
if body.get("include_reading_facts") is not True:
|
||||
return None
|
||||
today = body.get("today")
|
||||
return {
|
||||
**(window_options if window_options is not None else _report_window_options(body)),
|
||||
"today": today if isinstance(today, str) else None,
|
||||
**{key: body[key] for key in ("report_mode", "subject_life_status") if key in body},
|
||||
}
|
||||
@@ -362,7 +373,9 @@ def build_professional_report_reference(handler, body: dict[str, Any], *, engine
|
||||
if body.get("gender") in ("female", "male"):
|
||||
birth["gender"] = body["gender"]
|
||||
include_fact_tables = body.get("include_fact_tables") is True
|
||||
reading = _reading_request(body) if edition == FULL_DATA_EDITION and output_format == "markdown" else None
|
||||
window_options = _report_window_options(body) if edition == FULL_DATA_EDITION and output_format == "markdown" else None
|
||||
reading = _reading_request(body, window_options) if window_options is not None else None
|
||||
birth_window = window_options["birth_time_window"] if window_options is not None else None
|
||||
sha = _report_cache_sha()
|
||||
use_cache = (
|
||||
report_reference_cache_enabled()
|
||||
@@ -372,16 +385,16 @@ def build_professional_report_reference(handler, body: dict[str, Any], *, engine
|
||||
)
|
||||
if not use_cache:
|
||||
return _assemble_professional_report_reference(
|
||||
handler, birth, output_format, edition, packs, languages, include_fact_tables, engine, reading,
|
||||
handler, birth, output_format, edition, packs, languages, include_fact_tables, engine, reading, birth_window,
|
||||
)
|
||||
identity = _report_cache_identity(
|
||||
birth, output_format, edition, packs, languages, include_fact_tables, sha, reading,
|
||||
birth, output_format, edition, packs, languages, include_fact_tables, sha, reading, birth_window,
|
||||
)
|
||||
started = time.perf_counter()
|
||||
|
||||
def builder(_identity: dict[str, Any]) -> dict[str, Any]:
|
||||
return _assemble_professional_report_reference(
|
||||
handler, birth, output_format, edition, packs, languages, include_fact_tables, engine, reading,
|
||||
handler, birth, output_format, edition, packs, languages, include_fact_tables, engine, reading, birth_window,
|
||||
)
|
||||
|
||||
cached = _load_report_cache().get_or_build(
|
||||
|
||||
@@ -532,8 +532,8 @@ def render_d1_d60_ledger(packet: dict, language: str) -> list[str]:
|
||||
if not rows:
|
||||
return []
|
||||
summary = _dict(ledger.get("summary"))
|
||||
header = (("Division", *BODIES, "Tier", "Reading", "Stability") if en
|
||||
else ("分盘", *(PLANET_ZH[body] for body in BODIES), "技法等级", "解释许可", "稳定性"))
|
||||
header = (("Division", *BODIES, "Tier", "Reading", "Sample comparison") if en
|
||||
else ("分盘", *(PLANET_ZH[body] for body in BODIES), "技法等级", "解释许可", "样本核对"))
|
||||
body_rows = []
|
||||
for division in (f"D{number}" for number in range(1, 61)):
|
||||
row = _dict(rows.get(division))
|
||||
@@ -541,7 +541,7 @@ def render_d1_d60_ledger(packet: dict, language: str) -> list[str]:
|
||||
formal = row.get("capability_tier") == "formal_traditional_division"
|
||||
tier = ("formal division" if formal else "research D-N") if en else ("传统正式" if formal else "研究型 D-N")
|
||||
allowed = ("bounded cross-reading" if en else "允许受限交叉阅读") if row.get("interpretation_allowed") else ("raw research only" if en else "仅原始研究")
|
||||
stability = {"not_tested": ("not tested", "未测"), "stable": ("stable", "稳定"), "unstable": ("unstable", "不稳定")}.get(
|
||||
stability = {"not_tested": ("not tested", "未测"), "stable": ("no change in checked samples", "已核对的样本没有变化"), "unstable": ("varies across samples", "样本有变化")}.get(
|
||||
str(row.get("stability") or "not_tested"), (_clean(row.get("stability"), language),) * 2)[0 if en else 1]
|
||||
body_rows.append((division, *(_position(raw.get(body), language) for body in BODIES), tier, allowed, stability))
|
||||
intro = (
|
||||
|
||||
@@ -8,7 +8,7 @@ from domain_calculation_service import swiss_ephemeris_lock
|
||||
from jyotish_engine import _birth_hour_decimal, _sidereal_calculation_profile, swe
|
||||
from varga import calc_varga
|
||||
|
||||
from .birth_precision_window import crosses_birth_window
|
||||
from .birth_precision_window import DEFAULT_WINDOW_SOURCE, crosses_birth_window
|
||||
|
||||
|
||||
_CAP_SECONDS = 240 * 60
|
||||
@@ -56,7 +56,17 @@ def _margin_summary_zh(before: float | None, after: float | None, cap: float, wi
|
||||
later = (f"推后 {after:g} 分钟才换座" if after is not None
|
||||
else f"推后 {cap:g} 分钟内未换座")
|
||||
crosses = crosses_birth_window(before, after, window)
|
||||
conclusion = "区间内会换座" if crosses else "区间内不换座"
|
||||
source = window.get("source")
|
||||
if crosses:
|
||||
conclusion = "区间内会换座"
|
||||
elif source == DEFAULT_WINDOW_SOURCE:
|
||||
conclusion = "按前后 30 分钟试算,没有变化"
|
||||
elif source == "rectified_range":
|
||||
conclusion = "在生时纠正选定的区间内没有变化"
|
||||
elif source in ("user_stated", "shichen", "period"):
|
||||
conclusion = "在填写的误差范围内没有变化"
|
||||
else:
|
||||
conclusion = "区间内不换座"
|
||||
return f"{earlier};{later}。{conclusion}。"
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ CONFIRMED_TOLERANCE_MINUTES = 1.0
|
||||
DEFAULT_TOLERANCE_MINUTES = 30.0
|
||||
_WINDOW_EDGE_TOLERANCE_MINUTES = 0.01
|
||||
WINDOW_POLICY = "birth_record_window_v1"
|
||||
DEFAULT_WINDOW_SOURCE = "default_unknown_30"
|
||||
DEFAULT_WINDOW_SCENARIO = "default_window_scenario"
|
||||
DEFAULT_WINDOW_DISCLOSURE_ZH = "没有填写误差,这只是试算,不能确认稳定"
|
||||
DEFAULT_WINDOW_DISCLOSURE_EN = "No margin was supplied. This trial does not establish stability."
|
||||
_DECLARED_SOURCES = {"user_stated", "shichen", "period", "rectified_range"}
|
||||
_MAX_SIDE_MINUTES = 720
|
||||
_LOCAL_AT = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?\Z")
|
||||
@@ -81,10 +85,23 @@ def birth_window_label_zh(window: dict) -> str:
|
||||
if source == "confirmed_minute":
|
||||
return f"已确认到分钟,按前后各 {before:g} 分钟核对"
|
||||
if source == "default_unknown_30":
|
||||
return f"没有误差信息,按前后各 {before:g} 分钟"
|
||||
return f"没有填写误差,按前后各 {before:g} 分钟试算"
|
||||
label = "按填写时给的范围"
|
||||
if source == "rectified_range":
|
||||
label = "按生时纠正选定的区间"
|
||||
if "start_at" in window and "end_at" in window:
|
||||
label += f" {window['start_at'][11:16]}–{window['end_at'][11:16]}"
|
||||
return f"{label}(前 {round(before, 3):g} 分钟、后 {round(after, 3):g} 分钟)"
|
||||
|
||||
|
||||
def birth_window_disclosure(window: dict, language: str = "zh") -> str:
|
||||
if language == "en":
|
||||
before, after = window["before_minutes"], window["after_minutes"]
|
||||
if window["source"] == DEFAULT_WINDOW_SOURCE:
|
||||
return f"Trial calculation from {before:g} minutes earlier to {after:g} minutes later. {DEFAULT_WINDOW_DISCLOSURE_EN}"
|
||||
source = {"rectified_range": "Rectified interval", "confirmed_minute": "Confirmed minute"}.get(
|
||||
window["source"], "Declared birth-time range"
|
||||
)
|
||||
return f"{source}: {before:g} minutes earlier to {after:g} minutes later."
|
||||
label = birth_window_label_zh(window)
|
||||
return f"{label}。{DEFAULT_WINDOW_DISCLOSURE_ZH}。" if window["source"] == DEFAULT_WINDOW_SOURCE else label
|
||||
|
||||
@@ -30,7 +30,10 @@ from __future__ import annotations
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from .birth_precision_window import CONFIRMED_TOLERANCE_MINUTES, WINDOW_POLICY, crosses_birth_window, report_birth_window
|
||||
from .birth_precision_window import (
|
||||
CONFIRMED_TOLERANCE_MINUTES, DEFAULT_WINDOW_SCENARIO, DEFAULT_WINDOW_SOURCE,
|
||||
WINDOW_POLICY, crosses_birth_window, report_birth_window,
|
||||
)
|
||||
|
||||
try:
|
||||
from scripts.consult_card_domain_facts import PRECISION_VARGA
|
||||
@@ -237,10 +240,14 @@ def claim_grade(facts: dict, claim_type: str) -> dict[str, Any]:
|
||||
boundary_rows.append({"id": row["id"], "state": state, **details})
|
||||
precision = facts.get("birth_precision") or {}
|
||||
purpose = precision.get("purpose_stability") or "not_evaluated"
|
||||
if claim_type == "timing_window" and purpose == "stable":
|
||||
purpose = "not_evaluated"
|
||||
if purpose != "stable":
|
||||
must_disclose.append("purpose_stability_not_evaluated")
|
||||
if (precision.get("window") or {}).get("source") == DEFAULT_WINDOW_SOURCE:
|
||||
purpose = DEFAULT_WINDOW_SCENARIO
|
||||
must_disclose.append(DEFAULT_WINDOW_SCENARIO)
|
||||
else:
|
||||
if claim_type == "timing_window" and purpose == "stable":
|
||||
purpose = "not_evaluated"
|
||||
if purpose != "stable":
|
||||
must_disclose.append("purpose_stability_not_evaluated")
|
||||
cap = "D" if missing_required else "C"
|
||||
reasons = (
|
||||
[f"required dependency unavailable: {item}" for item in missing_required]
|
||||
|
||||
@@ -22,7 +22,9 @@ from datetime import date
|
||||
from typing import Any
|
||||
|
||||
from .birth_precision_margins import ascendant_margins
|
||||
from .birth_precision_window import birth_window_label_zh, report_birth_window
|
||||
from .birth_precision_window import (
|
||||
DEFAULT_WINDOW_SCENARIO, DEFAULT_WINDOW_SOURCE, birth_window_label_zh, report_birth_window,
|
||||
)
|
||||
from .evidence_grade import evidence_grades
|
||||
from .parity_chapters import _VIMSOPAKA_DIGNITY
|
||||
from .technique_status_axes import technique_audit_axes
|
||||
@@ -839,7 +841,9 @@ def _birth_precision(full_reading: dict, view: dict, domains: dict, minute_confi
|
||||
tolerance = report_birth_window(window, minute_confirmed=minute_confirmed)
|
||||
return {
|
||||
"minute_confirmed": minute_confirmed,
|
||||
"purpose_stability": "not_evaluated",
|
||||
"purpose_stability": (
|
||||
DEFAULT_WINDOW_SCENARIO if tolerance["source"] == DEFAULT_WINDOW_SOURCE else "not_evaluated"
|
||||
),
|
||||
"basis": "The purpose-specific interval contract (2026-11-09 interval_rectification handoff) is not integrated.",
|
||||
"window": {**tolerance, "label_zh": birth_window_label_zh(tolerance)},
|
||||
"ascendant_margins": ascendant_margins(birth, divisions, window=tolerance),
|
||||
|
||||
@@ -25,6 +25,8 @@ from __future__ import annotations
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from .birth_precision_window import birth_window_disclosure
|
||||
|
||||
try:
|
||||
from scripts.report_render.appendix_language import planet_name as _planet_name
|
||||
from scripts.report_render.appendix_language import sign_name
|
||||
@@ -118,9 +120,7 @@ WORDS = {
|
||||
"parameter_sensitivity_open": ("参数敏感未闭合", "parameter sensitivity open"),
|
||||
"strength_filter_missing": ("缺力量过滤", "no strength filter"),
|
||||
"multi_system_consensus_ready": ("多系统一致可用", "multi-system consensus ready"),
|
||||
"stable": ("稳定", "stable"),
|
||||
# sensitivity matrix
|
||||
"stable_across_window": ("窗口内稳定", "stable across the window"),
|
||||
"minute_sensitive": ("随分钟变化", "changes by minute"),
|
||||
"review_not_materialized": ("尚无校时复核记录", "no rectification review on record"),
|
||||
"record_rectification_review": ("记录校时复核", "record a rectification review"),
|
||||
@@ -593,6 +593,9 @@ def _rect_section(packet: dict, language: str) -> list[str]:
|
||||
replay = _dict(packet.get("event_replay"))
|
||||
contract = _dict(packet.get("rectification_evidence_contract"))
|
||||
sensitivity = three_minute_sensitivity(packet)
|
||||
reading_window = _dict(packet.get("report_birth_window"))
|
||||
scope_label = birth_window_disclosure(reading_window, language) if reading_window else None
|
||||
scope_line = f"- {'Range checked' if en else '本次核对范围'}{_colon(language)}{scope_label}"
|
||||
lines = [
|
||||
"### Birth data source and uncertainty" if en else "### 出生资料来源与不确定范围",
|
||||
"",
|
||||
@@ -600,7 +603,9 @@ def _rect_section(packet: dict, language: str) -> list[str]:
|
||||
f"- {'Coordinate precision' if en else '坐标精度'}{_colon(language)}{_word(provenance.get('coordinate_precision', 'unverified_user_coordinates'), language)}",
|
||||
f"- {'Coordinate source' if en else '坐标来源'}{_colon(language)}{_word(provenance.get('coordinate_source', 'not_recorded'), language)}",
|
||||
f"- {'Birth time source' if en else '出生时间来源'}{_colon(language)}{_word(provenance.get('time_source', 'not_recorded'), language)}",
|
||||
f"- {'Time uncertainty (minutes)' if en else '时间不确定范围(分钟)'}{_colon(language)}{_word(provenance.get('uncertainty_minutes', 'not_recorded'), language)}",
|
||||
*([scope_line] if scope_label else [
|
||||
f"- {'Time uncertainty (minutes)' if en else '时间不确定范围(分钟)'}{_colon(language)}{_word(provenance.get('uncertainty_minutes', 'not_recorded'), language)}",
|
||||
]),
|
||||
"",
|
||||
"### Timing precision contract" if en else "### 时间精度合同",
|
||||
"",
|
||||
@@ -661,6 +666,8 @@ def _verdict_section(packet: dict, sensitivity: dict, language: str) -> list[str
|
||||
if _dict(packet.get("birth_time_sensitivity")).get("status") != "candidate_window_only":
|
||||
view["birth_time_sensitivity"] = {"status": sensitivity.get("status")}
|
||||
matrix = build_prediction_verdict_matrix(view)
|
||||
parameter_status = _dict(matrix.get("parameter_sensitivity")).get("status")
|
||||
parameter_label = ("no change in checked samples" if en else "在已核对的样本里没有变化") if parameter_status == "stable" else _status(parameter_status or "blocked", language)
|
||||
rows = []
|
||||
for row in _list(matrix.get("rows")):
|
||||
row = _dict(row)
|
||||
@@ -677,7 +684,7 @@ def _verdict_section(packet: dict, sensitivity: dict, language: str) -> list[str
|
||||
"",
|
||||
f"- {'Status' if en else '状态'}{_colon(language)}{_status(matrix.get('status') or 'blocked', language)}",
|
||||
f"- {'Strength filter' if en else '力量过滤'}{_colon(language)}{_status(_dict(matrix.get('strength_filter_gate')).get('status') or 'blocked', language)}",
|
||||
f"- {'Parameter sensitivity' if en else '参数敏感'}{_colon(language)}{_word(_dict(matrix.get('parameter_sensitivity')).get('status'), language) if _dict(matrix.get('parameter_sensitivity')).get('status') == 'stable' else _status(_dict(matrix.get('parameter_sensitivity')).get('status') or 'blocked', language)}",
|
||||
f"- {'Parameter sensitivity' if en else '参数敏感'}{_colon(language)}{parameter_label}",
|
||||
("- The matrix only adjudicates evidence already in this report; without event replay it cannot claim exact dates or a confirmed result."
|
||||
if en else "- 本矩阵只裁决报告里已有的证据;没有事件回放时,不能据此给出确定日期或已证实的结果。"),
|
||||
"",
|
||||
@@ -746,8 +753,14 @@ def _sensitivity_section(sensitivity: dict, events: list[dict], language: str) -
|
||||
else:
|
||||
boundary = ("Raw structure for comparing candidates; cannot approve a birth minute alone." if en
|
||||
else "可作为候选比较的原始结构,不可单独批准出生分钟。")
|
||||
if kind == "stable_across_window":
|
||||
finding = "unchanged in the three-minute comparison" if en else "三分钟对照中没有变化"
|
||||
elif kind == "blocked":
|
||||
finding = _status(kind, language)
|
||||
else:
|
||||
finding = _word(kind, language)
|
||||
rows.append((_layer_label(key, language), _sensitivity_value(value, language) if value is not None else _status("blocked", language),
|
||||
_word(kind, language) if kind != "blocked" else _status("blocked", language), boundary))
|
||||
finding, boundary))
|
||||
minute_status = "blocked"
|
||||
lines = [
|
||||
"",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Report-local wording and grade caps for the three birth-time window sources."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.annual_service.evidence_grade import evidence_grades as annual_grades
|
||||
from scripts.report_render.birth_precision_margins import _margin_summary_zh
|
||||
from scripts.report_render.evidence_grade import evidence_grades as personal_grades
|
||||
from scripts.report_render.upstream_sync_chapters import render_upstream_sync_chapters
|
||||
from tests.test_annual_grade_20261114 import _inputs
|
||||
from tests.test_report_birth_window_20261113 import _unconfirmed_facts
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("source", "unchanged"), [
|
||||
("default_unknown_30", "按前后 30 分钟试算,没有变化"),
|
||||
("user_stated", "在填写的误差范围内没有变化"),
|
||||
("rectified_range", "在生时纠正选定的区间内没有变化"),
|
||||
])
|
||||
def test_report_scope_is_visible_without_changing_boundaries(source, unchanged):
|
||||
window = {"source": source, "before_minutes": 30, "after_minutes": 30}
|
||||
assert _margin_summary_zh(60, 64, 240, window).endswith(unchanged + "。")
|
||||
assert _margin_summary_zh(12, 64, 240, window).endswith("区间内会换座。")
|
||||
markdown = render_upstream_sync_chapters({"report_birth_window": window}, "zh")
|
||||
if source == "default_unknown_30":
|
||||
assert markdown.count("这只是试算,不能确认稳定") == 1
|
||||
assert all(term not in markdown.replace("不能确认稳定", "") for term in (
|
||||
"窗口内稳定", "误差内稳定", "已确认",
|
||||
))
|
||||
elif source == "rectified_range":
|
||||
assert "按生时纠正选定的区间" in markdown
|
||||
else:
|
||||
assert "按填写时给的范围" in markdown
|
||||
|
||||
|
||||
def test_personal_and_annual_trial_add_disclosure_without_changing_cap():
|
||||
declared = {"source": "user_stated", "before_minutes": 30, "after_minutes": 30}
|
||||
trial = {**declared, "source": "default_unknown_30"}
|
||||
before = _unconfirmed_facts(declared)
|
||||
after = _unconfirmed_facts(trial)
|
||||
for facts in (before, after):
|
||||
facts["birth_precision"]["purpose_stability"] = "stable"
|
||||
baseline, scenario = personal_grades(before), personal_grades(after)
|
||||
def digest(grades):
|
||||
return hashlib.sha256(json.dumps(grades, sort_keys=True, ensure_ascii=False).encode()).hexdigest()
|
||||
|
||||
assert digest(baseline) != digest(scenario)
|
||||
for name, row in scenario["claims"].items():
|
||||
assert row["cap"] == baseline["claims"][name]["cap"]
|
||||
assert row["ledger"]["purpose_stability"] == "default_window_scenario"
|
||||
assert "default_window_scenario" in row["must_disclose"]
|
||||
baseline, scenario = annual_grades(*_inputs(), declared), annual_grades(*_inputs(), trial)
|
||||
assert baseline["sha256"] != scenario["sha256"]
|
||||
for name, row in scenario.items():
|
||||
if name.startswith("annual_"):
|
||||
assert row["cap"] == baseline[name]["cap"]
|
||||
assert row["required"] == baseline[name]["required"]
|
||||
assert row["missing_required"] == baseline[name]["missing_required"]
|
||||
assert "default_window_scenario:D1" in scenario["annual_person"]["must_disclose"]
|
||||
assert "default_window_scenario:D10" in scenario["annual_domain_career"]["must_disclose"]
|
||||
assert "default_window_scenario:window_precision" in scenario["windows"]["a"]["must_disclose"]
|
||||
assert "default_window_scenario:D1" not in baseline["annual_person"]["must_disclose"]
|
||||
@@ -106,7 +106,7 @@ def test_rectified_metadata_survives_request_and_facts_normalization():
|
||||
def test_rejected_rectified_range_stays_marked_for_intake_and_default():
|
||||
for source, before, after, label in [
|
||||
("user_stated", 14, 1, "按填写时给的范围(前 14 分钟、后 1 分钟)"),
|
||||
("default_unknown_30", 30, 30, "没有误差信息,按前后各 30 分钟"),
|
||||
("default_unknown_30", 30, 30, "没有填写误差,按前后各 30 分钟试算"),
|
||||
]:
|
||||
window = {"source": source, "before_minutes": before, "after_minutes": after,
|
||||
"rectified_range_rejected": "clock_outside_chosen_side"}
|
||||
@@ -145,7 +145,7 @@ def test_rectified_window_only_caps_claims_for_crossings_inside_the_range():
|
||||
assert _varga_boundary(facts, code)[0] == ("boundary_sensitive" if crosses else "clear_at_tolerance")
|
||||
margin = margins[code]
|
||||
summary = _margin_summary_zh(margin["before_minutes"], margin["after_minutes"], 240, window)
|
||||
assert summary.endswith("区间内会换座。" if crosses else "区间内不换座。")
|
||||
assert summary.endswith("区间内会换座。" if crosses else "在生时纠正选定的区间内没有变化。")
|
||||
|
||||
assert {name: claims[name]["cap"] for name in (
|
||||
"wealth_earning", "wealth_retention", "health_pressure", "career_promise", "relationship_structure",
|
||||
@@ -166,4 +166,10 @@ def test_window_edge_tolerance_applies_on_each_side_and_preserves_confirmed_minu
|
||||
facts["birth_precision"]["ascendant_margins"]["D2"] = {"before_minutes": before, "after_minutes": after}
|
||||
assert _varga_boundary(facts, "D2")[0] == ("boundary_sensitive" if crosses else "clear_at_tolerance")
|
||||
summary = _margin_summary_zh(before, after, 240, window)
|
||||
assert summary.endswith("区间内会换座。" if crosses else "区间内不换座。")
|
||||
unchanged = {
|
||||
"user_stated": "在填写的误差范围内没有变化。",
|
||||
"rectified_range": "在生时纠正选定的区间内没有变化。",
|
||||
"default_unknown_30": "按前后 30 分钟试算,没有变化。",
|
||||
"confirmed_minute": "区间内不换座。",
|
||||
}
|
||||
assert summary.endswith("区间内会换座。" if crosses else unchanged[source])
|
||||
|
||||
Reference in New Issue
Block a user