把独立年度方法参考算进结果指纹,标准版不再白算这一章。
跑了修复单点名的测试、两次真算 PyJHora,以及 200 张英文扫描。 Co-Authored-By: Grok (agent loop)
This commit is contained in:
co-authored by
Grok (agent loop)
parent
683bd2fbc3
commit
c899e36509
+1
-1
@@ -39,7 +39,7 @@ The volume is not a backup domain. Chart cache is a TTL'd compute cache (default
|
||||
|
||||
Heavy rectification scans and high-rigor workflows share a process-wide concurrency gate (`JYOTISH_HEAVY_COMPUTE_CONCURRENCY`, default `2` to match the 2 vCPU host). A saturated request waits up to 8 seconds if fewer than 4 requests are already waiting, then returns HTTP 429 with `Retry-After`. Health checks and other light routes do not take a slot.
|
||||
|
||||
The API image installs PyJHora for one chapter only: the full-data report's last section, 「独立年度方法参考」. That chapter runs PyJHora in a child process. The API process does not import it. Chat cards, rectification scoring, and the rest of the report keep the previous no-PyJHora results. AGPL for this chapter is the product decision of 2026-10-21 (issue #93). Do not set `JYOTISH_YEAR_LORD_PYJHORA_REFERENCE`.
|
||||
The API image installs PyJHora for one chapter only: the full-data report's last section, 「独立年度方法参考」. That chapter runs PyJHora in a child process. The API process does not import it. Chat cards, rectification scoring, and the rest of the report keep the previous no-PyJHora results. AGPL for this chapter is the product decision of 2026-10-21 (issue #93). Do not turn on the year-lord PyJHora comparison switch.
|
||||
|
||||
## DNS and Supabase Auth
|
||||
|
||||
|
||||
@@ -19383,3 +19383,19 @@
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/full-data-parity-3-20261022`。
|
||||
|
||||
## BUG-1429 | 独立年度方法参考不进结果指纹,重算对不上
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-10-23
|
||||
- 最近更新:2026-10-23
|
||||
- 影响面:完整数据版资料包的结果指纹。阅读页正文不直接显示这串指纹。
|
||||
- 用户现象:报告里已经有「独立年度方法参考」,按同一份资料包重算指纹却对不上。防篡改核对会把这份报告看成被改过。
|
||||
- 触发条件:完整数据版组装资料包时挂上这一章,绑定指纹之前先把这一章摘掉,绑定后再放回去。
|
||||
- 根因:指纹故意不算这一章。允许排除的只有三类字段,这一章不在其中。重算时本章还在,所以两边不一致。
|
||||
- 修复:删掉摘除再放回的做法,恢复直接绑定。这一章算进指纹。标准版在已经知道版本之后才决定要不要算这一章,避免标准版白起一次计算;完整数据版仍在绑定之前挂上,指纹才算得到它。
|
||||
- 验证:`tests/test_pyjhora_annual_method_reference.py` 中两个不同标记的指纹不再相同。`tests/test_calculation_profile_contract.py` 仍要求绑定写在净化之后。`tests/test_report_reference_perf.py` 重算指纹与包上的指纹一致。
|
||||
- 防复发:上面三条。再把本章摘出指纹,标记不同却指纹相同的测试会失败,重算也对不上。
|
||||
- 相关记录:BUG-693、BUG-694。issue #93 第 1 次打回。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/full-data-parity-3-20261022`。
|
||||
|
||||
|
||||
@@ -85,3 +85,42 @@
|
||||
- 子进程超时是 120 秒。45 秒会在这台机器上把已经算完的三年掐掉。
|
||||
- 没做容器内存曲线。
|
||||
- 开工前的基线对照没有形成可用数字。
|
||||
|
||||
## 第 1 次打回(2026-10-23)
|
||||
|
||||
合并了当时的 `origin/staging`(含修复单 `4d0e398ab`)。没有重做上一轮已经通过的隔离和三年表。
|
||||
|
||||
这一章现在算进结果指纹。不再先摘掉、绑定后再放回去。两份只差这一章内容的资料包,指纹不再相同。报告里有这一章时,按同一份资料包重算,能对上。
|
||||
|
||||
比较「去掉这一章之后,装与不装是否逐字相同」时,先把下面两行换成同一个占位符 `FINGERPRINT`,再比:
|
||||
|
||||
- `- 报告 ID: \`...\``
|
||||
- `- 计算结果摘要: \`...\``
|
||||
|
||||
这两行来自结果指纹。指纹把这一章算进去之后,它们本来就会变。其余行不抹。
|
||||
|
||||
标准版在已经知道「不是完整数据版」之后,不再去算这一章。完整数据版仍在写入指纹之前挂上这一章,所以指纹里有它。直接生成完整数据资料包的旧入口默认仍会算,阅读页和扫描不变。
|
||||
|
||||
部署说明不再写出年主对照开关的变量名,只说不要打开这个开关。
|
||||
|
||||
子进程超时仍是 120 秒。这是上限,不是目标。前端整份报告最多等 180 秒;120 秒加上其余排盘和排版,在 Linux 上实测整份约 20 秒,没有顶到 180 秒。这台 Windows 电脑上中英一起最高约 59 秒,也没有顶到。
|
||||
|
||||
### 改过的断言
|
||||
|
||||
| 测试 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| 结果指纹是否包含这一章 | 两个标记的指纹相同 | 两个标记的指纹不同,本章仍留在资料包里 | 交付包里有这一章时,重算必须对得上(BUG-1429) |
|
||||
|
||||
编号:BUG-1429。
|
||||
|
||||
## 这一轮跑过的检查
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 修复单点名的测试,去掉两次真算 PyJHora | 指纹、合同、阅读页、英文版、年主、Tajika、增长、卫生等 | 退出码 0,约 460 秒 |
|
||||
| 三张虚构盘真算 PyJHora,以及父进程月亮位置不变 | 两个隔离测试 | 退出码 0,129 秒 |
|
||||
| 200 张英文扫描 | 扫描里不启动 PyJHora 子进程 | 退出码 0,1362 秒 |
|
||||
| 全量 pytest 和快速门 | 未跑 | 留给验收 |
|
||||
| 前端 | 本轮没改页面 | 没跑 |
|
||||
|
||||
依赖的 staging 提交是合并进来的 `4d0e398ab`。
|
||||
|
||||
@@ -528,7 +528,7 @@
|
||||
| `TASK-goel-jaimini-strength-20261022.md` | `PROGRESS-goel-jaimini-strength-20261022.md` | #94 搬上游公开的 Goel Jaimini 强度与 Brahma / Maheshwara / Rudra(4 个上游原样文件,按 #81 决策 A 先例根目录预算 448→452;来源串改书目引用);只做计算、不改报告;冻结身份不变 | 已合入 staging | `codex/goel-jaimini-strength-20261022` |
|
||||
| `TASK-chart-subject-table-fix-20261022.md` | `PROGRESS-chart-subject-table-fix-20261022.md` | **#86 第 1 次打回**:报告所有表被强制单行(打印跑出纸面);保存他人复制了档案页代码且已分叉;点编辑框内边距会丢修改 | 已合入 staging | `codex/chart-subject-table-20261021` |
|
||||
| `TASK-full-data-parity-3-20261022.md` | `PROGRESS-full-data-parity-3-20261022.md` | #93 完整数据版补齐(三):「独立年度方法参考」正式环境调 PyJHora(子进程隔离;装上后其余输出逐字节不变,7 个换路径开关钉死);输入默认用本地时间与正确年份(D6 默认,未收到「照上游原样」) | 第 1 次打回,见修复单 | `codex/full-data-parity-3-20261022` |
|
||||
| `TASK-full-data-parity-3-fix-20261023.md` | `PROGRESS-full-data-parity-3-20261022.md` | **#93 第 1 次打回**:功能、隔离、速度都过(Linux 约 20 秒,worker 约 60 MB);结果指纹排除本章致重算对不上(违反 D2)、源码合同测试没跟改、自己的新测试与 deploy README 打架 | 执行中 | `codex/full-data-parity-3-20261022` |
|
||||
| `TASK-full-data-parity-3-fix-20261023.md` | `PROGRESS-full-data-parity-3-20261022.md` | **#93 第 1 次打回**:结果指纹算进独立年度方法参考;标准版不再白算这一章;部署说明不点名年主开关(BUG-1429) | 待验收 | `codex/full-data-parity-3-20261022` |
|
||||
| `TASK-synastry-staging-fix-fix-20261022.md` | `PROGRESS-synastry-staging-fix-fix-20261022.md` | **#85 第 1 次打回**(T7~T11 主体通过):新地址没登记进路由清单致快速门红;CHANGELOG 与 T11 矛盾;侧栏点亮测试是空断言 | 已合入 staging | `codex/synastry-staging-fix-20261021` |
|
||||
| `TASK-report-english-han-leak-fix-20261022.md` | `PROGRESS-report-english-han-leak-fix-20261022.md` | **#89 第 1 次打回**:改了冻结的校正打分文件 `jaimini.py`;测试的 PyJHora 屏蔽没传到子进程(测的不是生产那份报告,也是慢的原因);缺英文时留空藏住漏洞 | 已合入 staging | `codex/report-english-han-leak-20261021` |
|
||||
| `TASK-full-data-parity-1-fix-20261022.md` | `PROGRESS-full-data-parity-1-fix-20261022.md` | **#91 A 段第 1 次打回**:新六章 69 节里 16 节在三张虚构盘上整节「未判定」,其中 12 节数据在资料包里、是读错了字段;测试抓不到整节未判定;新六章绕过了报告口径替换;中文附录悄悄丢列和瑜伽条目要量出来 | 已合入 staging | `codex/full-data-parity-1-20261021` |
|
||||
|
||||
@@ -2341,20 +2341,6 @@ def _attach_pyjhora_annual_method_reference(packet: dict, args):
|
||||
return attach_pyjhora_annual_method_reference(packet, args)
|
||||
|
||||
|
||||
def _bind_result_without_annual_method_reference(packet: dict, profile: dict) -> dict:
|
||||
"""Keep the new chapter out of result_hash. The chart result itself is unchanged."""
|
||||
worksheets = packet.get("worksheets") if isinstance(packet, dict) else None
|
||||
timing = worksheets.get("timing_and_predictive_systems") if isinstance(worksheets, dict) else None
|
||||
detached = None
|
||||
if isinstance(timing, dict) and "pyjhora_annual_method_reference" in timing:
|
||||
detached = timing.pop("pyjhora_annual_method_reference")
|
||||
try:
|
||||
return bind_result_to_profile(packet, profile)
|
||||
finally:
|
||||
if isinstance(timing, dict) and detached is not None:
|
||||
timing["pyjhora_annual_method_reference"] = detached
|
||||
|
||||
|
||||
def _uncertainty_minutes_from_args(args):
|
||||
explicit = getattr(args, 'uncertainty_minutes', None)
|
||||
if explicit not in (None, ''):
|
||||
@@ -18101,12 +18087,17 @@ def build_professional_report_reference_packet(
|
||||
full_reading: dict,
|
||||
args,
|
||||
selected_pack_ids: list[str] | None = None,
|
||||
*,
|
||||
include_annual_method_reference: bool = True,
|
||||
) -> dict:
|
||||
"""Build the public, non-writer professional reference packet for CLI and API."""
|
||||
packet = build_pl9_style_export_packet(full_reading, include_raw=True)
|
||||
packet = attach_calculation_profile(packet, args)
|
||||
packet = _attach_annual_tajika_pack(packet, args)
|
||||
packet = _attach_pyjhora_annual_method_reference(packet, args)
|
||||
# The flag is decided by the API after it knows the edition. Attach before
|
||||
# bind_result_to_profile so this chapter stays inside result_hash.
|
||||
if include_annual_method_reference:
|
||||
packet = _attach_pyjhora_annual_method_reference(packet, args)
|
||||
packet = _attach_report_governance_contracts(packet, args)
|
||||
packet = _attach_base_charts_pack(packet)
|
||||
packet = _attach_visual_chart_pack(packet, args)
|
||||
@@ -18124,7 +18115,7 @@ def build_professional_report_reference_packet(
|
||||
and isinstance(profile.get('input_hash'), str)
|
||||
and len(profile['input_hash']) == 64
|
||||
):
|
||||
delivered = _bind_result_without_annual_method_reference(delivered, profile)
|
||||
delivered = bind_result_to_profile(delivered, profile)
|
||||
if isinstance(delivered, dict) and delivered.get('selected_report_scope') == 'full':
|
||||
evaluate_full_report = _try_attr_import('full_report_quality_gate', 'evaluate_full_report')
|
||||
if evaluate_full_report is None:
|
||||
|
||||
@@ -245,6 +245,7 @@ def _assemble_professional_report_reference(
|
||||
full_reading,
|
||||
_export_args(birth),
|
||||
packs,
|
||||
include_annual_method_reference=edition == FULL_DATA_EDITION,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise ProfessionalReportReferenceInputError(str(exc)) from exc
|
||||
|
||||
@@ -85,6 +85,26 @@ def reset_reference_cache() -> None:
|
||||
_CACHE.clear()
|
||||
|
||||
|
||||
def mask_report_fingerprint_lines(markdown: str) -> str:
|
||||
"""Blank the report-id and result-fingerprint lines before a byte compare.
|
||||
|
||||
Including this chapter changes ``result_hash``, so those two identity lines
|
||||
move. Everything else, after the chapter itself is stripped, must not.
|
||||
"""
|
||||
output: list[str] = []
|
||||
for line in (markdown or "").splitlines(keepends=True):
|
||||
body = line.rstrip("\r\n")
|
||||
ending = line[len(body):]
|
||||
stripped = body.strip()
|
||||
if stripped.startswith("- 报告 ID: `") and stripped.endswith("`"):
|
||||
output.append(f"- 报告 ID: `FINGERPRINT`{ending}")
|
||||
elif stripped.startswith("- 计算结果摘要: `") and stripped.endswith("`"):
|
||||
output.append(f"- 计算结果摘要: `FINGERPRINT`{ending}")
|
||||
else:
|
||||
output.append(line)
|
||||
return "".join(output)
|
||||
|
||||
|
||||
def strip_annual_method_chapter(markdown: str) -> str:
|
||||
"""Drop this chapter so older full-text checks still see the previous report."""
|
||||
lines = (markdown or "").splitlines(keepends=True)
|
||||
|
||||
@@ -56,7 +56,7 @@ class _ReferenceEngine:
|
||||
self.invalid_pack = invalid_pack
|
||||
self.calls = []
|
||||
|
||||
def build_professional_report_reference_packet(self, full_reading, args, packs): # noqa: ANN001
|
||||
def build_professional_report_reference_packet(self, full_reading, args, packs, **_kwargs): # noqa: ANN001
|
||||
assert isinstance(args.hour, int)
|
||||
assert isinstance(args.minute, int)
|
||||
assert isinstance(args.second, int)
|
||||
|
||||
@@ -17,6 +17,7 @@ from scripts.report_render.annual_method_reference import (
|
||||
CHAPTER_ZH,
|
||||
build_pyjhora_annual_method_reference,
|
||||
normalize_worker_payload,
|
||||
mask_report_fingerprint_lines,
|
||||
render_annual_method_reference,
|
||||
reset_reference_cache,
|
||||
set_reference_runner,
|
||||
@@ -32,8 +33,11 @@ def setup_function() -> None:
|
||||
set_reference_runner(None)
|
||||
|
||||
|
||||
def test_result_hash_ignores_the_annual_method_chapter() -> None:
|
||||
from scripts.jyotish_engine import _bind_result_without_annual_method_reference
|
||||
def test_result_hash_includes_the_annual_method_chapter() -> None:
|
||||
# 原值: 两个标记的 result_hash 相同(本章被摘出指纹)。
|
||||
# 新值: 两个标记的 result_hash 不同,且本章仍留在资料包里。
|
||||
# 原因: 交付包里有这一章时,重算指纹必须对得上(BUG-1429)。
|
||||
from scripts.calculation_profile_contract import bind_result_to_profile
|
||||
|
||||
profile = {"input_hash": "ab" * 32}
|
||||
|
||||
@@ -48,13 +52,76 @@ def test_result_hash_ignores_the_annual_method_chapter() -> None:
|
||||
},
|
||||
}
|
||||
|
||||
installed = _bind_result_without_annual_method_reference(packet("installed"), profile)
|
||||
blocked = _bind_result_without_annual_method_reference(packet("blocked"), profile)
|
||||
assert installed["result_hash"] == blocked["result_hash"]
|
||||
installed = bind_result_to_profile(packet("installed"), profile)
|
||||
blocked = bind_result_to_profile(packet("blocked"), profile)
|
||||
assert installed["result_hash"] != blocked["result_hash"]
|
||||
assert installed["worksheets"]["timing_and_predictive_systems"]["pyjhora_annual_method_reference"]["marker"] == "installed"
|
||||
assert installed["worksheets"]["timing_and_predictive_systems"]["kept"] == "same"
|
||||
|
||||
|
||||
def test_fingerprint_lines_match_after_masking_for_the_chapter_compare() -> None:
|
||||
left = "\n".join([
|
||||
"## 报告身份与来源",
|
||||
"- 报告 ID: `full-report://one/aaaaaaaaaaaaaaaaaaaaaaaa`",
|
||||
"- 计算结果摘要: `0123456789abcdef...`",
|
||||
"## 前面的章",
|
||||
"same",
|
||||
f"## {CHAPTER_ZH}",
|
||||
"only-in-left",
|
||||
]) + "\n"
|
||||
right = "\n".join([
|
||||
"## 报告身份与来源",
|
||||
"- 报告 ID: `full-report://two/bbbbbbbbbbbbbbbbbbbbbbbb`",
|
||||
"- 计算结果摘要: `fedcba9876543210...`",
|
||||
"## 前面的章",
|
||||
"same",
|
||||
f"## {CHAPTER_ZH}",
|
||||
"only-in-right",
|
||||
]) + "\n"
|
||||
assert mask_report_fingerprint_lines(strip_annual_method_chapter(left)) == mask_report_fingerprint_lines(
|
||||
strip_annual_method_chapter(right)
|
||||
)
|
||||
assert "aaaaaaaaaaaaaaaaaaaaaaaa" not in mask_report_fingerprint_lines(left)
|
||||
assert "0123456789abcdef" not in mask_report_fingerprint_lines(left)
|
||||
|
||||
|
||||
def test_reference_edition_does_not_request_the_annual_method_worker(monkeypatch) -> None:
|
||||
from types import SimpleNamespace
|
||||
|
||||
from scripts.professional_report_reference import (
|
||||
FULL_DATA_EDITION,
|
||||
_assemble_professional_report_reference,
|
||||
)
|
||||
|
||||
seen: list[bool] = []
|
||||
|
||||
def build(_reading, _args, _packs, include_annual_method_reference=True):
|
||||
seen.append(include_annual_method_reference)
|
||||
return {"report_version": "v", "worksheets": {}}
|
||||
|
||||
engine = SimpleNamespace(
|
||||
build_professional_report_reference_packet=build,
|
||||
render_pl9_markdown=lambda _packet: "standard",
|
||||
)
|
||||
handler = SimpleNamespace(_compute_full_reading_for_thematic=lambda _birth: {})
|
||||
birth = fictional_case(0)
|
||||
reference = _assemble_professional_report_reference(
|
||||
handler, birth, "markdown", "reference", [], [], False, engine,
|
||||
)
|
||||
assert reference["markdown"] == "standard"
|
||||
assert seen == [False]
|
||||
|
||||
monkeypatch.setattr(
|
||||
"scripts.professional_report_reference.render_full_data_markdown",
|
||||
lambda _packet: "full-data",
|
||||
)
|
||||
full = _assemble_professional_report_reference(
|
||||
handler, birth, "markdown", FULL_DATA_EDITION, [], [], False, engine,
|
||||
)
|
||||
assert full["markdown"] == "full-data"
|
||||
assert seen == [False, True]
|
||||
|
||||
|
||||
def test_product_report_does_not_probe(monkeypatch) -> None:
|
||||
def _boom(*_args, **_kwargs):
|
||||
raise AssertionError("product diagnostics must not probe jhora")
|
||||
|
||||
@@ -152,7 +152,7 @@ def _api_response(packet: dict, body: dict) -> dict:
|
||||
return {"stub": True}
|
||||
|
||||
class Engine:
|
||||
def build_professional_report_reference_packet(self, full_reading, args, packs):
|
||||
def build_professional_report_reference_packet(self, full_reading, args, packs, **_kwargs):
|
||||
return copy.deepcopy(packet)
|
||||
|
||||
return build_professional_report_reference(Handler(), body, engine=Engine())
|
||||
|
||||
@@ -109,7 +109,7 @@ def test_default_edition_stays_reference_and_full_data_requests_v4() -> None:
|
||||
return {"modules": {"chart": {}}}
|
||||
|
||||
class Engine:
|
||||
def build_professional_report_reference_packet(self, full_reading, args, packs):
|
||||
def build_professional_report_reference_packet(self, full_reading, args, packs, **_kwargs):
|
||||
return {
|
||||
"schema": "pl9_style_professional_export_v1",
|
||||
"selected_report_pack_ids": packs,
|
||||
|
||||
@@ -265,7 +265,7 @@ def cached_report(monkeypatch, tmp_path, day_views):
|
||||
handler = _CountingHandler(birth, packet)
|
||||
|
||||
class _Engine:
|
||||
def build_professional_report_reference_packet(self, _reading, _args, _packs):
|
||||
def build_professional_report_reference_packet(self, _reading, _args, _packs, **_kwargs):
|
||||
return copy.deepcopy(packet)
|
||||
|
||||
def render_pl9_markdown(self, _packet):
|
||||
|
||||
Reference in New Issue
Block a user