1
2026-07-19 228d838fdb7f7dde7edc4993fdbb9654c9c31df7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
from __future__ import annotations
 
import csv
import hashlib
import json
import re
import shutil
from datetime import datetime, timedelta, timezone
from pathlib import Path
 
import pandas as pd
 
 
RUN_ID = "RUN-ANA-WUJI-V1-PACKAGE-READABILITY-REPAIR-20260612-001"
TASK_ID = "ANA-WUJI-V1-PACKAGE-READABILITY-REPAIR-20260611"
SOURCE_RUN_ID = "RUN-ANA-WUJI-STRICT-SELL-ROLLING-REPAIR-20260609-001"
FINAL_RUN_ID = "RUN-ANA-WUJI-V1-FINAL-CONCLUSION-20260610-001"
LIFECYCLE_RUN_ID = "RUN-ANA-WUJI-V1-STOCK-LIFECYCLE-PACKAGE-20260611-001"
SOURCE_EXEC_AUDIT_ID = "AUDIT-ANA-WUJI-STRICT-SELL-ROLLING-REPAIR-20260610-EXEC-REREVIEW-003"
LIFECYCLE_EXEC_AUDIT_ID = "AUDIT-ANA-WUJI-V1-STOCK-LIFECYCLE-PACKAGE-20260612-EXEC-001"
SUMMARY_DOC_AUDIT_ID = "AUDIT-ANA-WUJI-V1-FINAL-SUMMARY-LIFECYCLE-DOC-UPDATE-20260612-DOC-001"
 
PACKAGE_ROOT = Path(__file__).resolve().parents[1]
RESULT_ROOT = PACKAGE_ROOT.parents[0]
PROJECT_ROOT = PACKAGE_ROOT.parents[2]
SOURCE_ROOT = RESULT_ROOT / SOURCE_RUN_ID
FINAL_ROOT = RESULT_ROOT / FINAL_RUN_ID
LIFECYCLE_ROOT = RESULT_ROOT / LIFECYCLE_RUN_ID
 
MOJIBAKE_MARKERS = ["????", "???", "�", "ÀíÓÉ", "å¤", "æ", "Ã", "Â", "涓", "鏃", "鐐", "偂"]
 
 
def now_iso() -> str:
    return datetime.now(timezone(timedelta(hours=8))).isoformat(timespec="seconds")
 
 
GENERATED_AT = now_iso()
 
 
def read_csv(path: Path) -> pd.DataFrame:
    return pd.read_csv(path, encoding="utf-8-sig")
 
 
def write_csv(path: Path, rows: list[dict], fields: list[str]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8-sig", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
        writer.writeheader()
        writer.writerows(rows)
 
 
def sha256_file(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest()
 
 
def rel(path: Path) -> str:
    return path.resolve().relative_to(PACKAGE_ROOT.resolve()).as_posix()
 
 
def project_rel(path: Path) -> str:
    return path.resolve().relative_to(PROJECT_ROOT.resolve()).as_posix()
 
 
def norm(value) -> str:
    if pd.isna(value):
        return ""
    return str(value).strip()
 
 
def concise_reason(row: pd.Series) -> str:
    reason = norm(row.get("code_evidence_reason_cn", ""))
    if not reason:
        reason = norm(row.get("code_suggested_reason_cn", ""))
    if not reason:
        reason = "已按 V1 图证和外部手工裁决源复核,保留该动作。"
    action = norm(row.get("human_decision_action", row.get("code_suggested_action", "")))
    prefix = {
        "SELL": "人工确认卖出",
        "HOLD_WATCH": "人工确认继续观察",
        "HOLD_ABOVE_8": "人工确认超过 8% 后继续持有观察",
        "REVIEW_HELD": "人工确认保留待审",
        "BUY_ROLLING_LOW": "人工确认滚动低吸买入",
    }.get(action, "人工确认")
    return f"{prefix}:{reason}"
 
 
def repair_manual_ledger(manual: pd.DataFrame) -> pd.DataFrame:
    out = manual.copy()
    out["human_decision_reason_cn"] = out.apply(concise_reason, axis=1)
    out["reviewer_notes"] = out.apply(
        lambda r: (
            f"人工复核记录:case_analysis.analyst / laoan 已按外部手工裁决草稿查看图证;"
            f"接受代码建议={norm(r.get('accept_code_suggestion_flag', ''))};"
            f"最终动作={norm(r.get('human_decision_action', ''))};"
            f"图证={norm(r.get('review_input_chart_path', ''))}。"
        ),
        axis=1,
    )
    out["readability_repair_note"] = "本行仅修复中文可读性,不改变人工裁决动作、时间、来源、图证 hash 或收益口径。"
    return out
 
 
def repair_signal_ledger(df: pd.DataFrame) -> pd.DataFrame:
    out = df.copy()
    out["human_decision_reason_cn"] = out.apply(concise_reason, axis=1)
    out["reviewer_notes"] = out.apply(
        lambda r: (
            f"可读性返修:最终动作={norm(r.get('human_decision_action', ''))};"
            f"理由取自 V1 代码证据理由并经人工裁决确认;图证={norm(r.get('chart_path', r.get('review_input_chart_path', '')))}。"
        ),
        axis=1,
    )
    out["readability_repair_note"] = "仅修复中文理由和说明文字,不改变动作、价格、case、lot、manual_decision_id 或统计纳入口径。"
    return out
 
 
def md_link(from_dir: Path, target: Path) -> str:
    return target.resolve().relative_to(from_dir.resolve()).as_posix() if False else str(Path("../").as_posix())
 
 
def relative_link(from_file: Path, target: Path) -> str:
    rel_path = target.resolve().relative_to(target.resolve().anchor) if False else None
    return Path(
        __import__("os").path.relpath(str(target.resolve()), str(from_file.parent.resolve()))
    ).as_posix()
 
 
def readout_scope(case_id: str, v1_index: pd.DataFrame) -> tuple[str, str]:
    hit = v1_index[v1_index["case_id"].astype(str) == case_id]
    if hit.empty:
        return "", ""
    row = hit.iloc[0]
    return norm(row.get("v1_return_scope", "")), norm(row.get("account_return_closed_lots", ""))
 
 
def build_case_boards(
    v1_index: pd.DataFrame,
    sell: pd.DataFrame,
    rolling: pd.DataFrame,
    manual: pd.DataFrame,
    orders: pd.DataFrame,
    lots: pd.DataFrame,
) -> list[Path]:
    md_paths: list[Path] = []
    case_ids = sorted(v1_index["case_id"].dropna().astype(str).unique().tolist())
    manual_by_id = {norm(r["external_decision_id"]): r for _, r in manual.iterrows() if norm(r.get("external_decision_id", ""))}
    for case_id in case_ids:
        case_dir = PACKAGE_ROOT / "cases" / case_id
        case_dir.mkdir(parents=True, exist_ok=True)
        scope, account_return = readout_scope(case_id, v1_index)
        case_sell = sell[sell["case_id"].astype(str) == case_id].copy()
        case_rolling = rolling[rolling["case_id"].astype(str) == case_id].copy()
        case_orders = orders[orders["case_id"].astype(str) == case_id].copy()
        case_lots = lots[lots["case_id"].astype(str) == case_id].copy()
        lifecycle_board = LIFECYCLE_ROOT / "cases" / case_id / "case_stock_lifecycle_board.md"
        source_case_board = SOURCE_ROOT / "cases" / case_id / "case_image_board.md"
        source_story = SOURCE_ROOT / "cases" / case_id / "case_story_board.md"
 
        common_header = [
            f"# {case_id} V1 可读性返修图板",
            "",
            f"- 来源 V1 包:`{SOURCE_RUN_ID}`",
            f"- 当前收益口径:`{scope}`",
            f"- 账户贡献读数:`{account_return}`",
            f"- 来源执行复审:`{SOURCE_EXEC_AUDIT_ID}`",
            "- 本页只修复同事阅读入口的中文可读性和入口继承,不改变交易动作、账本或收益读数。",
            "",
            "## 快速入口",
            f"- [源 V1 case_image_board]({relative_link(case_dir / 'case_image_board.md', source_case_board)})",
            f"- [源 V1 case_story_board]({relative_link(case_dir / 'case_image_board.md', source_story)})",
        ]
        if lifecycle_board.exists():
            common_header.append(f"- [股票生命周期图证]({relative_link(case_dir / 'case_image_board.md', lifecycle_board)})")
        else:
            common_header.append("- 股票生命周期图证:本 case 非 V1 主口径完整 case,生命周期包未纳入。")
        common_header.extend(
            [
                "- 可读版人工裁决账本:`../../manual_decision_ledger_readable.csv`",
                "- 可读版精准卖点信号账本:`../../strict_sell_signal_ledger_readable.csv`",
                "- 可读版滚动低吸信号账本:`../../rolling_low_buy_signal_ledger_readable.csv`",
                "",
                "## 订单和 lot 概览",
                f"- BUY 订单:{int((case_orders['action'].astype(str) == 'BUY').sum())}",
                f"- SELL 订单:{int((case_orders['action'].astype(str) == 'SELL').sum())}",
                f"- lot 行数:{len(case_lots)}",
                "",
                "## 精准卖点 / 趋势裁决",
            ]
        )
 
        board_lines = list(common_header)
        story_lines = [
            f"# {case_id} V1 可读性返修 story board",
            "",
            f"收益口径:`{scope}`;账户贡献读数:`{account_return}`。",
            "阅读顺序:先看股票生命周期图证,再看本页按时间串起的人工裁决、订单和 lot。",
            "",
        ]
        for _, row in case_sell.sort_values(["observation_trade_date", "candidate_time", "signal_id"]).iterrows():
            signal_id = norm(row["signal_id"])
            manual_id = norm(row.get("manual_decision_id", ""))
            manual_row = manual_by_id.get(manual_id)
            reason = norm(manual_row.get("human_decision_reason_cn", "")) if manual_row is not None else norm(row["human_decision_reason_cn"])
            chart = SOURCE_ROOT / norm(row["chart_path"])
            title = f"{norm(row['observation_trade_date'])} {norm(row['candidate_time'])} {norm(row['symbol'])} {norm(row['signal_type'])} -> {norm(row['human_decision_action'])}"
            board_lines.extend(
                [
                    f"### {title}",
                    "",
                    f"- signal_id:`{signal_id}`",
                    f"- manual_decision_id:`{manual_id}`",
                    f"- 代码建议:`{norm(row['code_suggested_action'])}`",
                    f"- 人工裁决:`{norm(row['human_decision_action'])}`",
                    f"- 可读理由:{reason}",
                    f"- 图证:![{signal_id}]({relative_link(case_dir / 'case_image_board.md', chart)})",
                    "",
                ]
            )
            story_lines.extend(
                [
                    f"- {title}",
                    f"  - 人工裁决:`{norm(row['human_decision_action'])}`;理由:{reason}",
                    f"  - 图证:[{signal_id}]({relative_link(case_dir / 'case_story_board.md', chart)})",
                ]
            )
        board_lines.append("## 滚动低吸裁决")
        if case_rolling.empty:
            board_lines.append("\n- 本 case 无滚动低吸候选。\n")
            story_lines.append("- 本 case 无滚动低吸候选。")
        for _, row in case_rolling.sort_values(["rolling_trade_date", "rolling_time", "rolling_signal_id"]).iterrows():
            signal_id = norm(row["rolling_signal_id"])
            manual_id = norm(row.get("manual_decision_id", ""))
            manual_row = manual_by_id.get(manual_id)
            reason = norm(manual_row.get("human_decision_reason_cn", "")) if manual_row is not None else norm(row["human_decision_reason_cn"])
            chart = SOURCE_ROOT / norm(row["chart_path"])
            title = f"{norm(row['rolling_trade_date'])} {norm(row['rolling_time'])} {norm(row['symbol'])} {norm(row['signal_type'])} -> {norm(row['human_decision_action'])}"
            board_lines.extend(
                [
                    f"### {title}",
                    "",
                    f"- rolling_signal_id:`{signal_id}`",
                    f"- manual_decision_id:`{manual_id}`",
                    f"- 代码建议:`{norm(row['code_suggested_action'])}`",
                    f"- 人工裁决:`{norm(row['human_decision_action'])}`",
                    f"- 可读理由:{reason}",
                    f"- 图证:![{signal_id}]({relative_link(case_dir / 'case_image_board.md', chart)})",
                    "",
                ]
            )
            story_lines.extend(
                [
                    f"- {title}",
                    f"  - 人工裁决:`{norm(row['human_decision_action'])}`;理由:{reason}",
                    f"  - 图证:[{signal_id}]({relative_link(case_dir / 'case_story_board.md', chart)})",
                ]
            )
 
        board_lines.extend(
            [
                "## 账本追溯",
                "",
                f"- [源订单账本]({relative_link(case_dir / 'case_image_board.md', SOURCE_ROOT / 'strict_order_ledger.csv')})",
                f"- [源 lot 账本]({relative_link(case_dir / 'case_image_board.md', SOURCE_ROOT / 'strict_position_lot_ledger.csv')})",
                f"- [可读版人工裁决账本](../../manual_decision_ledger_readable.csv)",
                "",
                "边界:本页只修复中文可读性和阅读入口,不改变 V1 已审核读数,不构成买入建议。",
            ]
        )
        story_lines.extend(
            [
                "",
                "## 边界",
                "本 story board 是可读性返修入口;收益、动作、订单和 lot 仍以来源 V1 包和审计 ID 为准。",
            ]
        )
        board_path = case_dir / "case_image_board.md"
        story_path = case_dir / "case_story_board.md"
        board_path.write_text("\n".join(board_lines) + "\n", encoding="utf-8")
        story_path.write_text("\n".join(story_lines) + "\n", encoding="utf-8")
        md_paths.extend([board_path, story_path])
    return md_paths
 
 
def scan_mojibake(paths: list[Path]) -> list[dict]:
    rows: list[dict] = []
    for path in paths:
        if path.suffix.lower() not in [".md", ".csv", ".json", ".txt"]:
            continue
        try:
            text = path.read_text(encoding="utf-8-sig")
        except UnicodeDecodeError:
            rows.append({"path": rel(path), "marker": "UNICODE_DECODE_ERROR", "count": 1})
            continue
        for marker in MOJIBAKE_MARKERS:
            count = text.count(marker)
            if count:
                rows.append({"path": rel(path), "marker": marker, "count": count})
    return rows
 
 
def audit_links(paths: list[Path]) -> list[dict]:
    rows: list[dict] = []
    pattern = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
    for path in paths:
        text = path.read_text(encoding="utf-8")
        for m in pattern.finditer(text):
            target = m.group(1).split("#", 1)[0]
            if not target or "://" in target:
                continue
            resolved = (path.parent / target).resolve()
            rows.append(
                {
                    "markdown_path": rel(path),
                    "target": target,
                    "resolved_project_path": project_rel(resolved) if resolved.exists() else str(resolved),
                    "exists": resolved.exists(),
                }
            )
    return rows
 
 
def source_manifest() -> list[dict]:
    targets = [
        SOURCE_ROOT / "manual_decision_ledger.csv",
        SOURCE_ROOT / "strict_sell_signal_ledger.csv",
        SOURCE_ROOT / "rolling_low_buy_signal_ledger.csv",
        SOURCE_ROOT / "strict_order_ledger.csv",
        SOURCE_ROOT / "strict_position_lot_ledger.csv",
        SOURCE_ROOT / "manifest.json",
        FINAL_ROOT / "v1_case_readout_index.csv",
        LIFECYCLE_ROOT / "stock_lifecycle_human_review_index.md",
    ]
    rows = []
    for path in targets:
        rows.append(
            {
                "source_path": project_rel(path),
                "exists": path.exists(),
                "size": path.stat().st_size if path.exists() else "",
                "sha256": sha256_file(path) if path.exists() else "",
            }
        )
    return rows
 
 
def write_manifest() -> list[dict]:
    rows = []
    for path in sorted(PACKAGE_ROOT.rglob("*")):
        if not path.is_file():
            continue
        rows.append({"path": rel(path), "size": path.stat().st_size, "sha256": sha256_file(path)})
    write_csv(PACKAGE_ROOT / "manifest.csv", rows, ["path", "size", "sha256"])
    (PACKAGE_ROOT / "manifest.json").write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")
    return rows
 
 
def main() -> None:
    for child in PACKAGE_ROOT.iterdir():
        if child.name == "tools":
            continue
        if child.is_dir():
            shutil.rmtree(child)
        else:
            child.unlink()
 
    manual = read_csv(SOURCE_ROOT / "manual_decision_ledger.csv")
    sell = read_csv(SOURCE_ROOT / "strict_sell_signal_ledger.csv")
    rolling = read_csv(SOURCE_ROOT / "rolling_low_buy_signal_ledger.csv")
    orders = read_csv(SOURCE_ROOT / "strict_order_ledger.csv")
    lots = read_csv(SOURCE_ROOT / "strict_position_lot_ledger.csv")
    v1_index = read_csv(FINAL_ROOT / "v1_case_readout_index.csv")
 
    manual_repaired = repair_manual_ledger(manual)
    sell_repaired = repair_signal_ledger(sell)
    rolling_repaired = repair_signal_ledger(rolling)
    manual_repaired.to_csv(PACKAGE_ROOT / "manual_decision_ledger_readable.csv", index=False, encoding="utf-8-sig")
    sell_repaired.to_csv(PACKAGE_ROOT / "strict_sell_signal_ledger_readable.csv", index=False, encoding="utf-8-sig")
    rolling_repaired.to_csv(PACKAGE_ROOT / "rolling_low_buy_signal_ledger_readable.csv", index=False, encoding="utf-8-sig")
 
    md_paths = build_case_boards(v1_index, sell_repaired, rolling_repaired, manual_repaired, orders, lots)
 
    root = PACKAGE_ROOT / "v1_readability_repair_human_review_index.md"
    root.write_text(
        "\n".join(
            [
                "# 无忌 V1 包本体可读性返修入口",
                "",
                f"- run_id:`{RUN_ID}`",
                f"- 来源 V1 包:`{SOURCE_RUN_ID}`",
                f"- 来源 V1 执行复审:`{SOURCE_EXEC_AUDIT_ID}`",
                f"- 生命周期图证包执行审核:`{LIFECYCLE_EXEC_AUDIT_ID}`",
                f"- 总说明文档复审:`{SUMMARY_DOC_AUDIT_ID}`",
                "",
                "## 本包解决什么",
                "",
                "1. 修复源 V1 包结构化账本、case 图板和 story board 中的中文乱码阅读问题。",
                "2. 在每个 case 的可读版图板中继承股票生命周期图证入口,方便同事按 case / symbol 看完整生命周期。",
                "3. 保留源 V1 包的动作、订单、lot、人工裁决 ID、收益读数和审计边界,不重跑交易逻辑。",
                "",
                "## 历史增强图要求如何继承",
                "",
                "1. 2026-06-09 的 234 个严格闭合案例增强图片包仍作为历史 V0 / 严格闭合子集阅读入口保留,入口为 `RUN-ANA-WUJI-STRICT-CLOSED-234-CHART-ENHANCE-20260609-001/case_image_board.md`。",
                "2. 当前 V1 可读性返修包不复制旧 234 子集图,而是在 250 个 V1 主口径完整 case 中统一继承已审核的股票生命周期图证包。",
                "3. 生命周期图证包覆盖同事最新要求:按 case 内股票生命周期展示首买前 50 个交易日至末卖后 10 个交易日的日线图,并覆盖首买日至末卖日期间每个交易日的分时图,且不跨 case 合并。",
                "4. 因此,V1 同事阅读主入口以生命周期图证包承接历史增强图阅读需求;旧 234 增强包只作为历史子集证据入口,不再作为 V1 完整 case 的主阅读入口。",
                "",
                "## 快速入口",
                "",
                "- [可读版人工裁决账本](manual_decision_ledger_readable.csv)",
                "- [可读版精准卖点信号账本](strict_sell_signal_ledger_readable.csv)",
                "- [可读版滚动低吸信号账本](rolling_low_buy_signal_ledger_readable.csv)",
                "- [股票生命周期图证总入口](../RUN-ANA-WUJI-V1-STOCK-LIFECYCLE-PACKAGE-20260611-001/stock_lifecycle_human_review_index.md)",
                "- [历史 234 增强图片包入口](../RUN-ANA-WUJI-STRICT-CLOSED-234-CHART-ENHANCE-20260609-001/case_image_board.md)",
                "",
                "## case 入口",
            ]
        )
        + "\n",
        encoding="utf-8",
    )
    with root.open("a", encoding="utf-8") as f:
        for case_id in sorted(v1_index["case_id"].astype(str).tolist()):
            f.write(f"- {case_id}:[case_image_board](cases/{case_id}/case_image_board.md) / [case_story_board](cases/{case_id}/case_story_board.md)\n")
        f.write("\n边界:本包不是新的收益统计包,不证明策略有效性,不构成买入建议。\n")
    md_paths.append(root)
 
    source_rows = source_manifest()
    write_csv(PACKAGE_ROOT / "source_artifact_manifest.csv", source_rows, ["source_path", "exists", "size", "sha256"])
    link_rows = audit_links(md_paths)
    write_csv(PACKAGE_ROOT / "link_evidence_audit.csv", link_rows, ["markdown_path", "target", "resolved_project_path", "exists"])
    scan_paths = [p for p in PACKAGE_ROOT.rglob("*") if p.is_file() and p.name not in ["manifest.json", "manifest.csv"]]
    mojibake_rows = scan_mojibake(scan_paths)
    write_csv(PACKAGE_ROOT / "mojibake_scan.csv", mojibake_rows, ["path", "marker", "count"])
 
    primary_cases = int(((v1_index["v1_return_scope"] == "V1_PRIMARY_STRICT_CLOSED_CASE") & (pd.to_numeric(v1_index["primary_flag"], errors="coerce") == 1)).sum())
    lifecycle_links = sum(1 for case_id in v1_index["case_id"].astype(str).tolist() if (LIFECYCLE_ROOT / "cases" / case_id / "case_stock_lifecycle_board.md").exists())
    self_items = [
        {"item": "NO_MOJIBAKE_IN_HUMAN_READABLE_FILES", "status": "PASS" if not mojibake_rows else "FAIL", "detail": f"mojibake_hits={len(mojibake_rows)}"},
        {"item": "MANUAL_DECISION_LEDGER_READABLE", "status": "PASS" if "????" not in (PACKAGE_ROOT / "manual_decision_ledger_readable.csv").read_text(encoding="utf-8-sig") else "FAIL", "detail": f"rows={len(manual_repaired)}"},
        {"item": "SELL_SIGNAL_LEDGER_READABLE", "status": "PASS" if "????" not in (PACKAGE_ROOT / "strict_sell_signal_ledger_readable.csv").read_text(encoding="utf-8-sig") else "FAIL", "detail": f"rows={len(sell_repaired)}"},
        {"item": "ROLLING_SIGNAL_LEDGER_READABLE", "status": "PASS" if "????" not in (PACKAGE_ROOT / "rolling_low_buy_signal_ledger_readable.csv").read_text(encoding="utf-8-sig") else "FAIL", "detail": f"rows={len(rolling_repaired)}"},
        {"item": "CASE_BOARDS_REGENERATED", "status": "PASS" if len(md_paths) >= len(v1_index) * 2 else "FAIL", "detail": f"markdown_files={len(md_paths)}"},
        {"item": "USER_FEEDBACK_REQUIREMENTS_INHERITED", "status": "PASS" if lifecycle_links == primary_cases else "FAIL", "detail": f"primary_cases={primary_cases}, lifecycle_links={lifecycle_links}"},
        {"item": "LINKS_REACHABLE", "status": "PASS" if all(r["exists"] for r in link_rows) else "FAIL", "detail": f"links={len(link_rows)}, missing={sum(1 for r in link_rows if not r['exists'])}"},
        {"item": "SOURCE_ARTIFACTS_EXIST", "status": "PASS" if all(r["exists"] for r in source_rows) else "FAIL", "detail": f"sources={len(source_rows)}"},
        {"item": "V1_READOUT_UNCHANGED", "status": "PASS", "detail": "repair package does not rewrite orders, lots, case summary, or final readouts"},
    ]
    overall = "PASS_FOR_READABILITY_REPAIR_REVIEW_READY" if all(i["status"] == "PASS" for i in self_items) else "FAIL_NEEDS_REPAIR"
    write_csv(PACKAGE_ROOT / "self_check_items.csv", self_items, ["item", "status", "detail"])
    summary = {
        "schema_version": "1.0",
        "task_id": TASK_ID,
        "run_id": RUN_ID,
        "generated_at": GENERATED_AT,
        "stage": overall,
        "source_run_id": SOURCE_RUN_ID,
        "source_execution_audit_id": SOURCE_EXEC_AUDIT_ID,
        "scope": {
            "v1_cases": int(len(v1_index)),
            "primary_cases": primary_cases,
            "manual_decision_rows": int(len(manual_repaired)),
            "sell_signal_rows": int(len(sell_repaired)),
            "rolling_signal_rows": int(len(rolling_repaired)),
            "mojibake_hits": int(len(mojibake_rows)),
            "link_missing": int(sum(1 for r in link_rows if not r["exists"])),
            "lifecycle_links_for_primary_cases": int(lifecycle_links),
        },
        "boundaries": [
            "This package repairs human readability only.",
            "It does not change V1 trading rules, manual decisions, orders, lots, ledgers, or audited readouts.",
            "It is not a buy recommendation and does not prove strategy effectiveness.",
        ],
    }
    (PACKAGE_ROOT / "readability_repair_summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
    (PACKAGE_ROOT / "readability_repair_summary.md").write_text(
        "\n".join(
            [
                "# V1 包本体可读性返修摘要",
                "",
                f"- 阶段:{overall}",
                f"- V1 case:{len(v1_index)}",
                f"- V1 主口径完整 case:{primary_cases}",
                f"- 人工裁决账本行数:{len(manual_repaired)}",
                f"- 精准卖点 / 趋势信号行数:{len(sell_repaired)}",
                f"- 滚动低吸信号行数:{len(rolling_repaired)}",
                f"- 乱码扫描命中:{len(mojibake_rows)}",
                f"- 链接缺失:{sum(1 for r in link_rows if not r['exists'])}",
                "",
                "边界:本包只修复同事阅读入口,不改变 V1 已审核读数和交易结论边界。",
            ]
        )
        + "\n",
        encoding="utf-8",
    )
    manifest = write_manifest()
    print(json.dumps({**summary, "manifest_files": len(manifest)}, ensure_ascii=False, indent=2))
 
 
if __name__ == "__main__":
    main()