修复拆类金样换机器就对不齐的问题,避免测试删掉共用排盘缓存。

指纹不再包含仓库绝对路径;采集时关闭缓存而不是删除目录。基线与本分支 8 个哈希一致,定向 pytest 11 通过。

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-16 11:48:27 +08:00
co-authored by Grok (agent loop)
parent 14ad076fe4
commit a7fe7e9dd5
5 changed files with 182 additions and 60 deletions
+14
View File
@@ -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 扩展前缀)换成 `<ROOT>`,后面的分隔符统一成 `/`。不按字段名挑选。采集期间设置 `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。
@@ -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 的 `\\?\` 前缀),换成 `<ROOT>`,其后的分隔符统一成 `/`。不按字段名挑选。
- 采集不再删除 `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`,所以不能在这里声称失败清单已与基线逐名一致。
+97 -45
View File
@@ -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 <ROOT>.
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 = "<ROOT>"
# 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 <ROOT>"
),
}
+15 -13
View File
@@ -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 <ROOT>",
"rootPlaceholder": "<ROOT>"
},
"kind": "sha256_and_keypath_types"
}
+3 -2
View File
@@ -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