From a7fe7e9dd5f8a768ad25d2080f45db1e429f0d24 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Fri, 16 Oct 2026 11:46:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8B=86=E7=B1=BB=E9=87=91?= =?UTF-8?q?=E6=A0=B7=E6=8D=A2=E6=9C=BA=E5=99=A8=E5=B0=B1=E5=AF=B9=E4=B8=8D?= =?UTF-8?q?=E9=BD=90=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=88=A0=E6=8E=89=E5=85=B1=E7=94=A8=E6=8E=92?= =?UTF-8?q?=E7=9B=98=E7=BC=93=E5=AD=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 指纹不再包含仓库绝对路径;采集时关闭缓存而不是删除目录。基线与本分支 8 个哈希一致,定向 pytest 11 通过。 Co-Authored-By: Grok (agent loop) --- docs/BUG_HISTORY.md | 14 ++ ...api-server-service-extract-fix-20261016.md | 53 +++++++ .../research/api_service_extract_golden.py | 142 ++++++++++++------ .../golden/api_server_service_extract_v1.json | 28 ++-- tests/test_api_server_growth_contract.py | 5 +- 5 files changed, 182 insertions(+), 60 deletions(-) create mode 100644 docs/tasks/PROGRESS-api-server-service-extract-fix-20261016.md diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 20fb7b42..1710809c 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -36367,4 +36367,18 @@ - 复发自:无 - 修复版本:分支 `codex/dasha-unify-20261013`。开工时本分支最大已用编号是 BUG-1357,1358 未被占用。 +## BUG-1359 | 拆类金样把本机路径算进指纹,测试还会删掉共用排盘缓存 +- 状态:resolved +- 首次发现:2026-10-16 +- 最近更新:2026-10-16 +- 影响面:`tests/test_api_service_extract_golden.py` 与采集脚本 `scripts/research/api_service_extract_golden.py`。计算和网页入口的拆分本身没有改。 +- 用户现象:没有用户能看见的变化。验收时这条金样在另一台机器上对不齐;全量并行时另一条排盘上下文测试变红,单独跑是绿的。 +- 触发条件:在采集金样的那台机器以外运行这条金样测试;或全量 pytest 并行时,别的测试正在用仓库里的 `scratch/local/api_chart_cache`。 +- 根因:咨询结果里的清单路径是仓库根目录的绝对路径,规范化只去掉了两个墙钟字段。采集脚本又把这个共用缓存目录整个删掉。缓存有效期为 0 时,现有读函数直接返回空、写函数不落盘。BUG-1060 已经要求采集时把有效期设为 0,因为命中缓存会改变列表顺序;这次新脚本没有沿用,改成了删目录。 +- 修复:所有字符串值里的仓库根目录绝对路径(反斜杠、正斜杠,以及 Windows 扩展前缀)换成 ``,后面的分隔符统一成 `/`。不按字段名挑选。采集期间设置 `JYOTISH_API_CHART_CACHE_TTL_SECONDS=0`,结束恢复原值,不再删除缓存目录。在基线 `fbf04f0b5f31cd8421bc4546840b1c833aaed810` 与本分支各采集一次,8 个哈希相同,用这份结果覆盖金样。方法数常量旁被盖住的 BUG-1341 三栏已恢复,本单说明另起一行。 +- 验证:有效期为 0 时读取结果为空,缓存目录文件数采集前后都是 2。基线与本分支 8 个哈希逐个相同。金样、增长合同、以及上次并行变红的那一条一起跑,11 通过、0 失败。没有改那条在改代码之前就已经失败的校正测试的断言。 +- 防复发:金样测试仍要求 sha256 与键路径类型全等,没有改成只比键或字节数。采集脚本里不再删除仓库内缓存目录。 +- 相关记录:BUG-1060 +- 复发自:无 +- 修复版本:分支 `codex/api-server-service-extract-20261013`。修复单写明无 BUG 编号;本条是验收打回的测试失败。开工时本分支最大已用编号是 BUG-1358,1351、1352、1353 已有条目,本条用 BUG-1359。 diff --git a/docs/tasks/PROGRESS-api-server-service-extract-fix-20261016.md b/docs/tasks/PROGRESS-api-server-service-extract-fix-20261016.md new file mode 100644 index 00000000..c85e1865 --- /dev/null +++ b/docs/tasks/PROGRESS-api-server-service-extract-fix-20261016.md @@ -0,0 +1,53 @@ +# PROGRESS · API 拆类金样修复(2026-10-16) + +- 分支:`codex/api-server-service-extract-20261013` +- 修复单:`docs/tasks/TASK-api-server-service-extract-fix-20261016.md`(来自 `origin/staging` `187fa375`) +- 被打回的提交:`c7350bcafbff64d3c526c2dbf3995c9845b3b66e` +- 基线(金样 `fixtureProvenance.baselineSha`):`fbf04f0b5f31cd8421bc4546840b1c833aaed810` +- 先把只含修复单的 `origin/staging` 合并进本分支(合并提交 `14ad076f`)。没有带进别的代码。 + +## 做了什么 + +- 拆类没有重做。没有改 `scripts/jyotish_api_server.py`,也没有改 `tests/test_api_server_security.py`。 +- 金样规范化对所有字符串值去掉仓库根目录绝对路径(反斜杠、正斜杠,以及 Windows 的 `\\?\` 前缀),换成 ``,其后的分隔符统一成 `/`。不按字段名挑选。 +- 采集不再删除 `scratch/local/api_chart_cache`。采集期间把 `JYOTISH_API_CHART_CACHE_TTL_SECONDS` 设为 `0`,结束恢复原值。有效期为 0 时,现有读函数直接返回空,写函数不落盘。 +- 在基线提交上用同一套规范化重新采集,再在本分支采集。8 个哈希与键路径类型逐个相同。金样文件按这份结果覆盖。 +- `JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE` 上方恢复原来的三行: + - 原值:218(删占位回退之前) + - 新值:217 + - 原因:BUG-1341 删除 `JyotishAPIHandler._fallback_dasha_periods`,类方法少 1 个,没有新增方法 + - 本单「数值未变」另起一行写在这三行后面,没有盖住历史。 +- 没有改断言。`check_golden()` 仍要求 sha256 和键路径类型全等,没有改成只比键或字节数。上一轮在改代码之前就失败的校正测试没有放宽。 +- 记了 BUG-1359。修复单写的是无编号;这次是验收打回的测试失败,按 `AGENTS.md` §5 记。开工时本分支最大已用编号是 BUG-1358。BUG-1351、1352、1353 已有条目,没有重用。 + +## 两次采集的 8 个哈希 + +基线与本分支打印的是同一组数。六条咨询路径都比上一份金样短 122 字节:上一份还含本机绝对路径,并且排盘缓存有效期字段是默认的 900 秒。这次两边都按「缓存不生效」采集,有效期字段是 0。`repairs` 与 `evidence` 的哈希和字节数与上一份相同。没有再跑「删除缓存目录」那条旧采集。 + +| 案例 | 基线 sha256 | 基线字节 | 本分支 sha256 | 本分支字节 | +| --- | --- | ---: | --- | ---: | +| evidence | `27975ba40a9ee89fc19543a18cb2ee60fb2477b8a6e8f14004ad411adfc408a2` | 2092 | `27975ba40a9ee89fc19543a18cb2ee60fb2477b8a6e8f14004ad411adfc408a2` | 2092 | +| http.career | `81a0c8429fdec0c7d5ad2ac768c0d2a0d1361c46a299fb79cdfbfa46459492a8` | 420433 | `81a0c8429fdec0c7d5ad2ac768c0d2a0d1361c46a299fb79cdfbfa46459492a8` | 420433 | +| http.relationship | `db4e04f76c016cd9e44263329b4a763fc11a1573ed1910414c369f5e3f696525` | 414350 | `db4e04f76c016cd9e44263329b4a763fc11a1573ed1910414c369f5e3f696525` | 414350 | +| http.wealth | `d75e8036c05ff36b10e7c53dad4203d1c819f60abfe8d6473ab347810fe0a1ab` | 419194 | `d75e8036c05ff36b10e7c53dad4203d1c819f60abfe8d6473ab347810fe0a1ab` | 419194 | +| mcp.career | `14aca344f395106c761a785e22f9cbda1282f79bd81d6e4d3bde9ffd9a839196` | 543746 | `14aca344f395106c761a785e22f9cbda1282f79bd81d6e4d3bde9ffd9a839196` | 543746 | +| mcp.relationship | `625b532ce57966f2decbb8b67ee9f8b3a2809834026ae8ed4047a13f720b786e` | 537663 | `625b532ce57966f2decbb8b67ee9f8b3a2809834026ae8ed4047a13f720b786e` | 537663 | +| mcp.wealth | `1ad98c778ed2360e782fc8e61a20ffc34dcc07698207a6801c756b09699f6220` | 542507 | `1ad98c778ed2360e782fc8e61a20ffc34dcc07698207a6801c756b09699f6220` | 542507 | +| repairs | `41d48a453fbd5d616de083fe07bb8d318c3f60ad2e127764237ef6ae50cc6175` | 74700 | `41d48a453fbd5d616de083fe07bb8d318c3f60ad2e127764237ef6ae50cc6175` | 74700 | + +## 命令和结果 + +| 检查 | 命令 | 结果数字 | +| --- | --- | --- | +| 有效期为 0 时不读不写 | 调用现有的缓存读、写函数 | 读取结果为空;目录里的文件 2 个,调用前后相同;写回的有效期字段是 0,未命中 | +| 两次采集时缓存目录 | 基线采集、本分支采集各看一次目录 | 两次都是采集前 2 个文件、采集后 2 个文件,名单相同 | +| 金样、增长合同、上次并行变红的那一条 | `.venv\Scripts\python.exe -m pytest tests/test_api_service_extract_golden.py tests/test_api_server_growth_contract.py tests/test_capture_commercial_astrology_e2e_contexts.py::test_capture_writes_runtime_contexts_without_required_layer_echo -q --tb=line` | **11 通过,0 失败**,退出码 0,约 132 秒。其中金样 1、增长合同 9、上下文那一条 1 | +| 历史三栏是否还在删除行里 | `git diff origin/staging -- tests/test_api_server_growth_contract.py` | 原来的三行没有出现在删除行里 | +| 全量 pytest、Python 快速门、前端 `npm test` | 没跑 | 修复单的全量对照由验收方复跑 | +| `npm run test:db` | 没跑 | 没改数据库 | + +## 偏离 + +- 修复单写「无 BUG 编号」。本轮按仓库规则补了 BUG-1359,原因见上。 +- 没有把「删除共用缓存」和「有效期为 0」再各采一次来拆开那 122 字节。删除共用目录会再次打到别的测试,修复单不允许。金样以「缓存不生效」为准。 +- 没跑全量 `pytest -n 4`,所以不能在这里声称失败清单已与基线逐名一致。 diff --git a/scripts/research/api_service_extract_golden.py b/scripts/research/api_service_extract_golden.py index 51ccb1e6..59a95a42 100644 --- a/scripts/research/api_service_extract_golden.py +++ b/scripts/research/api_service_extract_golden.py @@ -1,6 +1,7 @@ """Canonical hashes for the compute/HTTP class split. Fictional smoke chart only. Clock fields are removed before hashing. +Absolute repository paths in string values are replaced with . Payload values are not written; the golden file stores sha256 and key-path types. """ from __future__ import annotations @@ -8,18 +9,20 @@ from __future__ import annotations import hashlib import json import os -import shutil import sys +from collections.abc import Iterator +from contextlib import contextmanager from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[2] SCRIPTS = ROOT / "scripts" GOLDEN_PATH = ROOT / "tests" / "golden" / "api_server_service_extract_v1.json" -CACHE_DIR = ROOT / "scratch" / "local" / "api_chart_cache" CLOCK_KEYS = {"cache_created_at", "cache_expires_at"} THEMES = ("career", "wealth", "relationship") BASELINE_SHA = "fbf04f0b5f31cd8421bc4546840b1c833aaed810" +CACHE_TTL_ENV = "JYOTISH_API_CHART_CACHE_TTL_SECONDS" +ROOT_PLACEHOLDER = "" # Synthetic positions reused from tests/test_offline_compute_mixins.py. # Not a birth record. @@ -42,9 +45,54 @@ def prepare_env() -> None: sys.path.insert(0, path) -def clear_chart_cache() -> None: - if CACHE_DIR.exists(): - shutil.rmtree(CACHE_DIR) +@contextmanager +def chart_cache_disabled() -> Iterator[None]: + """Turn the shared chart cache off. Do not delete the repository directory.""" + previous = os.environ.get(CACHE_TTL_ENV) + os.environ[CACHE_TTL_ENV] = "0" + try: + yield + finally: + if previous is None: + os.environ.pop(CACHE_TTL_ENV, None) + else: + os.environ[CACHE_TTL_ENV] = previous + + +def root_spellings(root: Path = ROOT) -> tuple[str, ...]: + native = os.path.normpath(str(root)) + forward = native.replace("\\", "/") + spellings = {native, forward} + if os.name == "nt": + spellings.add("\\\\?\\" + native) + return tuple(sorted(spellings, key=len, reverse=True)) + + +def neutralize_path_string(text: str, root: Path = ROOT) -> str: + for spelling in root_spellings(root): + text = text.replace(spelling, ROOT_PLACEHOLDER) + if ROOT_PLACEHOLDER not in text: + return text + pieces = text.split(ROOT_PLACEHOLDER) + rebuilt = [pieces[0]] + for piece in pieces[1:]: + if piece.startswith("\\") or piece.startswith("/"): + end = 0 + while end < len(piece) and piece[end] not in " \t\r\n\"'": + end += 1 + piece = piece[:end].replace("\\", "/") + piece[end:] + rebuilt.append(piece) + return ROOT_PLACEHOLDER.join(rebuilt) + + +def neutralize_paths(value: Any, root: Path = ROOT) -> Any: + if isinstance(value, str): + return neutralize_path_string(value, root) + if isinstance(value, dict): + return {key: neutralize_paths(item, root) for key, item in value.items()} + if isinstance(value, list): + return [neutralize_paths(item, root) for item in value] + return value def theme_body(theme: str) -> dict[str, Any]: @@ -110,7 +158,7 @@ def flatten_keypaths(value: Any, prefix: str = "", *, depth: int = 0, max_depth: def canonical_text(value: Any) -> str: - return json.dumps(strip_clock(value), sort_keys=True, ensure_ascii=False) + return json.dumps(neutralize_paths(strip_clock(value)), sort_keys=True, ensure_ascii=False) def digest(value: Any) -> dict[str, Any]: @@ -124,56 +172,55 @@ def digest(value: Any) -> dict[str, Any]: def capture_mcp(theme: str) -> Any: prepare_env() - clear_chart_cache() - from consultation_workflow_service import execute_consultation_workflow + with chart_cache_disabled(): + from consultation_workflow_service import execute_consultation_workflow - return execute_consultation_workflow(theme_body(theme), surface="skill_mcp") + return execute_consultation_workflow(theme_body(theme), surface="skill_mcp") def capture_http(theme: str) -> Any: """HTTP handler method, on the compute class after the split.""" prepare_env() - clear_chart_cache() - from jyotish_api_server import JyotishComputeService + with chart_cache_disabled(): + from jyotish_api_server import JyotishComputeService - return JyotishComputeService()._compute_consultation_workflow(theme_body(theme)) + return JyotishComputeService()._compute_consultation_workflow(theme_body(theme)) def capture_repairs() -> dict[str, Any]: prepare_env() - clear_chart_cache() - from capture_report_blocked_repairs_golden import ( - BIRTH, - MODULE_KEEP, - THEMES as REPAIR_THEMES, - VARGA_KEEP, - _body, - _handler, - _trim_workflow, - ) + with chart_cache_disabled(): + from capture_report_blocked_repairs_golden import ( + BIRTH, + MODULE_KEEP, + THEMES as REPAIR_THEMES, + VARGA_KEEP, + _body, + _handler, + _trim_workflow, + ) - payload: dict[str, Any] = { - "source": "scripts/jyotish_api_server.py _compute_consultation_workflow", - "smoke_birth": {**BIRTH, "note": "fictional capture identity; not a real person"}, - "trim": { - "keep_varga_keys": list(VARGA_KEEP), - "keep_modules": list(MODULE_KEEP), - "drop": [ - "ai_prompt_pack", - "vedastro", - "varga_spectrum", - "guided_topics", - "western_spectrum", - "technique_audit_table", - ], - }, - "themes": {}, - } - for theme in REPAIR_THEMES: - clear_chart_cache() - raw = _handler()._compute_consultation_workflow(_body(theme)) - payload["themes"][theme] = _trim_workflow(raw) - return payload + payload: dict[str, Any] = { + "source": "scripts/jyotish_api_server.py _compute_consultation_workflow", + "smoke_birth": {**BIRTH, "note": "fictional capture identity; not a real person"}, + "trim": { + "keep_varga_keys": list(VARGA_KEEP), + "keep_modules": list(MODULE_KEEP), + "drop": [ + "ai_prompt_pack", + "vedastro", + "varga_spectrum", + "guided_topics", + "western_spectrum", + "technique_audit_table", + ], + }, + "themes": {}, + } + for theme in REPAIR_THEMES: + raw = _handler()._compute_consultation_workflow(_body(theme)) + payload["themes"][theme] = _trim_workflow(raw) + return payload def capture_evidence() -> Any: @@ -207,10 +254,15 @@ def provenance() -> dict[str, Any]: "env": { "VEDASTRO_ENABLE_NETWORK": "0", "JYOTISH_SKIP_LOCAL_ENV": "1", + "JYOTISH_API_CHART_CACHE_TTL_SECONDS": "0", }, "clockFieldsRemoved": sorted(CLOCK_KEYS), + "rootPlaceholder": ROOT_PLACEHOLDER, "mcpSurface": "skill_mcp", - "note": "fictional smoke chart from the task brief; payload values are not stored", + "note": ( + "fictional smoke chart from the task brief; payload values are not stored; " + "repository absolute paths in string values are replaced with " + ), } diff --git a/tests/golden/api_server_service_extract_v1.json b/tests/golden/api_server_service_extract_v1.json index 341fa74a..b8d76524 100644 --- a/tests/golden/api_server_service_extract_v1.json +++ b/tests/golden/api_server_service_extract_v1.json @@ -44,7 +44,7 @@ "sha256": "27975ba40a9ee89fc19543a18cb2ee60fb2477b8a6e8f14004ad411adfc408a2" }, "http.career": { - "canonicalBytes": 420555, + "canonicalBytes": 420433, "keypaths": { "$": "object", "answer_policy": "object", @@ -346,10 +346,10 @@ "vedastro_snapshot_cache.require_today": "boolean", "vedastro_snapshot_cache.served_on_utc_date": "string" }, - "sha256": "6a6ca421d942e2577e587647324ef9521ea14841a74a6850c11f629df4d9ba96" + "sha256": "81a0c8429fdec0c7d5ad2ac768c0d2a0d1361c46a299fb79cdfbfa46459492a8" }, "http.relationship": { - "canonicalBytes": 414472, + "canonicalBytes": 414350, "keypaths": { "$": "object", "answer_policy": "object", @@ -651,10 +651,10 @@ "vedastro_snapshot_cache.require_today": "boolean", "vedastro_snapshot_cache.served_on_utc_date": "string" }, - "sha256": "4405ae821aef81ec03660994d9f8f4104ae3388f339407bfe2721d05d662059b" + "sha256": "db4e04f76c016cd9e44263329b4a763fc11a1573ed1910414c369f5e3f696525" }, "http.wealth": { - "canonicalBytes": 419316, + "canonicalBytes": 419194, "keypaths": { "$": "object", "answer_policy": "object", @@ -956,10 +956,10 @@ "vedastro_snapshot_cache.require_today": "boolean", "vedastro_snapshot_cache.served_on_utc_date": "string" }, - "sha256": "f22368bbde38b76dc44e2995c34f7809ca96442405c1f13e39f53c4a2dd90a62" + "sha256": "d75e8036c05ff36b10e7c53dad4203d1c819f60abfe8d6473ab347810fe0a1ab" }, "mcp.career": { - "canonicalBytes": 543868, + "canonicalBytes": 543746, "keypaths": { "$": "object", "answer_policy": "object", @@ -1276,10 +1276,10 @@ "western_evidence_packet.system": "string", "western_evidence_packet.timing_techniques": "object" }, - "sha256": "ad38a539cf9036143333acd538567b7eb94528d537c46ef326905ebc47527a05" + "sha256": "14aca344f395106c761a785e22f9cbda1282f79bd81d6e4d3bde9ffd9a839196" }, "mcp.relationship": { - "canonicalBytes": 537785, + "canonicalBytes": 537663, "keypaths": { "$": "object", "answer_policy": "object", @@ -1596,10 +1596,10 @@ "western_evidence_packet.system": "string", "western_evidence_packet.timing_techniques": "object" }, - "sha256": "be70fd603b1de52026ad40f0d6582a27cb8b148018474fcd7d8c4134509b40ae" + "sha256": "625b532ce57966f2decbb8b67ee9f8b3a2809834026ae8ed4047a13f720b786e" }, "mcp.wealth": { - "canonicalBytes": 542629, + "canonicalBytes": 542507, "keypaths": { "$": "object", "answer_policy": "object", @@ -1916,7 +1916,7 @@ "western_evidence_packet.system": "string", "western_evidence_packet.timing_techniques": "object" }, - "sha256": "9aaacf64d08f17bca2d3f6b020b1dbb1ec07eaee7727b04bba90430fcbf594d3" + "sha256": "1ad98c778ed2360e782fc8e61a20ffc34dcc07698207a6801c756b09699f6220" }, "repairs": { "canonicalBytes": 74700, @@ -1955,12 +1955,14 @@ ], "command": "python scripts/research/api_service_extract_golden.py --check", "env": { + "JYOTISH_API_CHART_CACHE_TTL_SECONDS": "0", "JYOTISH_SKIP_LOCAL_ENV": "1", "VEDASTRO_ENABLE_NETWORK": "0" }, "fictional": true, "mcpSurface": "skill_mcp", - "note": "fictional smoke chart from the task brief; payload values are not stored" + "note": "fictional smoke chart from the task brief; payload values are not stored; repository absolute paths in string values are replaced with ", + "rootPlaceholder": "" }, "kind": "sha256_and_keypath_types" } diff --git a/tests/test_api_server_growth_contract.py b/tests/test_api_server_growth_contract.py index 3dda595d..7b3cccf9 100644 --- a/tests/test_api_server_growth_contract.py +++ b/tests/test_api_server_growth_contract.py @@ -34,9 +34,10 @@ JYOTISH_API_SERVER_LINE_COUNT_CAP = JYOTISH_API_SERVER_LINE_COUNT_BASELINE + 300 # TASK-freeze-metric-change-20260915 §1. Re-measured 2026-09-16 after # TASK-api-server-backdoor-close-20260916 lifted 7 compute-only methods into # scripts/offline_compute_mixins.py (225 -> 218). -# 原值:217 +# 原值:218(删占位回退之前) # 新值:217 -# 原因:BUG-1341 已把基线降到 217。本单只把 HTTP 方法换到子类,不增删方法。 +# 原因:BUG-1341 删除 JyotishAPIHandler._fallback_dasha_periods,类方法少 1 个,没有新增方法 +# 本单数值未变,仍是 217:只把 HTTP 方法换到子类,不增删方法。 JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE = 217 # 原值:31