File size: 13,914 Bytes
ede84b4 | 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 | #!/usr/bin/env python3
"""modutech_v30_1.json → modutech_v30_2.json — แก้ถ้อยคำ wet_note_th อย่างเดียว (11 ต.ค. 2026)
ปัญหาใน v30 / v30_1 (ดู RESULTS_phase2_answer_review.md · เรื่อง "เกรด PA")
temperature_effective.wet_note_th ทั้ง 75 ข้อความสร้างจากแม่แบบเดียว:
"ไม่มีข้อมูลสภาวะเปียกของเกรด PA ค่านี้คำนวณจากชั้นที่มีข้อมูลเท่านั้น ต้องยืนยันก่อนเสนอ"
1. เรียก PA ว่า "เกรด" ทั้งที่ PA เป็นวัสดุพิน — ลูกค้าเข้าใจว่าสายพานเกรด PA
2. ไม่บอกว่าชั้นไหน (สายพาน/พิน) ขาดข้อมูล และตัวเลขแต่ละตัวมาจากไหน
3. "ชั้นที่มีข้อมูล" เป็นศัพท์ภายใน · "ก่อนเสนอ" เป็นคำของฝ่ายขาย ไม่ใช่ของลูกค้า
4. รุ่นที่สายพานกับพินเป็นวัสดุเดียวกันพิมพ์ซ้ำ "เกรด PA, PA"
สิ่งที่สคริปต์นี้ทำ
สร้าง wet_note_th ใหม่จาก source_layers ของแต่ละ variant (ไม่ใช่แก้คำทีละตัว)
- บอกว่าวัสดุชั้นไหนไม่มีค่าเปียก และมีค่าแห้งหรือไม่
- บอกที่มาของค่าสูงสุดและค่าต่ำสุดของช่วงเปียก โดยไล่หาค่าที่ตรงจากชั้นจริง
ถ้าหาที่มาไม่เจอ → หยุด ไม่เดา
- ปิดท้าย "ต้องให้ฝ่ายเทคนิคยืนยันก่อนใช้งานจริง"
ของเดิมเก็บไว้ใน temperature_effective.wet_note_th_v30 (เก็บของดิบคู่ของที่ซ่อม)
ไม่แตะ: ตัวเลขทุกตัว · wet_status · wet/dry · limiters · temperature.wet_note_th (ฟิลด์อื่น)
· จำนวนนับ · ไฟล์ต้นทาง (เขียนไฟล์ใหม่เสมอ)
รัน: python patch_v30_2.py [modutech_v30_1.json] [modutech_v30_2.json] [--changelog CHANGELOG_v30_2.md]
"""
from __future__ import annotations
import copy
import hashlib
import json
import re
import sys
LAYER_TH = {"belt_material": "วัสดุสายพาน", "pin_material": "วัสดุพิน"}
FORBIDDEN = ("เกรด", "ก่อนเสนอ", "ชั้นที่มีข้อมูล", "ระบบ", "ห้าม")
CLOSING = "ต้องยืนยันกับพนักงานฝ่ายขายก่อนใช้งานจริง"
def fingerprint(obj) -> str:
return hashlib.sha256(json.dumps(obj, ensure_ascii=False, sort_keys=True, default=str).encode()).hexdigest()
def c(x: float) -> str:
return f"{x:g}"
def rng(a) -> str:
return f"{c(a[0])} ถึง {c(a[1])} °C"
def layers(te: dict) -> list[tuple[str, dict]]:
L = te["source_layers"]
return [(k, L[k]) for k in ("belt_material", "pin_material") if L.get(k)]
def missing_phrase(te: dict) -> str:
"""'วัสดุพิน PA (มีแต่ค่าแห้ง -35 ถึง 120 °C)' · รวมเป็นวลีเดียวถ้าสองชั้นเป็นวัสดุเดียวกัน"""
miss = [(k, l) for k, l in layers(te) if l.get("wet") is None]
if not miss:
raise SystemExit("variant partial_data แต่ไม่มีชั้นไหนขาดค่าเปียก — หยุด")
def dry(l):
return f"มีแต่ค่าแห้ง {rng(l['dry'])}" if l.get("dry") else "ไม่มีทั้งค่าแห้งและค่าเปียก"
if len(miss) == 2 and miss[0][1]["code"] == miss[1][1]["code"]:
l = miss[0][1]
return f"วัสดุ {l['code']} ซึ่งใช้ทั้งสายพานและพิน ({dry(l)})"
return " และ".join(f"{LAYER_TH[k]} {l['code']} ({dry(l)})" for k, l in miss)
def origin(te: dict, cond_val: float, idx: int) -> str:
"""ที่มาของค่าขอบเขตช่วงเปียก idx=0 ต่ำสุด · idx=1 สูงสุด — ไล่ตามลำดับที่ gate ใช้คำนวณ"""
# 1) ค่าเปียกของวัสดุชั้นที่มีข้อมูลเปียก
for k, l in layers(te):
if l.get("wet") and abs(l["wet"][idx] - cond_val) < 1e-6:
return f"ค่าสภาวะเปียกของ{LAYER_TH[k]} {l['code']}"
# 2) ตารางสเปกของรุ่น (เล่มพิมพ์ช่วงเดียว ไม่แยกเปียก/แห้ง)
st = te["source_layers"].get("spec_table") or {}
key = ("min_c", "max_c")[idx]
if st.get(key) is not None and abs(st[key] - cond_val) < 1e-6:
return f"ช่วงอุณหภูมิในตารางสเปกของรุ่น (หน้า {st['page']})"
# 3) ค่าแห้งของวัสดุ (ไม่มีค่าเปียก จึงใช้ค่าแห้งเป็นเพดาน)
same = len({l["code"] for _, l in layers(te)}) == 1 and len(layers(te)) == 2
for k, l in layers(te):
if l.get("dry") and abs(l["dry"][idx] - cond_val) < 1e-6:
who = f" {l['code']}" if same else f"{LAYER_TH[k]} {l['code']}" # วัสดุเดียวกันสองชั้น ไม่ต้องระบุชั้น
return f"ค่าสภาวะแห้งของ{who}"
raise SystemExit(f"หาที่มาของ {cond_val} ไม่เจอ — หยุด ไม่เดา\n{json.dumps(te, ensure_ascii=False)}")
def new_note(te: dict) -> str:
st = te["wet_status"]
if st == "not_recommended":
bad = [(k, l) for k, l in layers(te) if l.get("wet_status") == "not_recommended"]
page = bad[0][1]["page"]
codes = sorted({l["code"] for _, l in bad})
if len(bad) == 2 and len(codes) == 1:
who = f"วัสดุ {codes[0]} ซึ่งใช้ทั้งสายพานและพินของรุ่นนี้"
else:
who = " และ".join(f"{LAYER_TH[k]} {l['code']}" for k, l in bad)
return f"แคตตาล็อกหน้า {page} ระบุว่า{who} ไม่แนะนำให้ใช้ในสภาวะเปียก"
if st != "partial_data":
raise SystemExit(f"wet_status {st!r} ไม่อยู่ในขอบเขตแพตช์ — หยุด")
w = te["wet"]
lo, hi = origin(te, w["min_c"], 0), origin(te, w["max_c"], 1)
head = f"แคตตาล็อกไม่มีค่าสภาวะเปียกของ{missing_phrase(te)}"
if lo == hi:
body = f"ช่วง {c(w['min_c'])} ถึง {c(w['max_c'])} °C ในสภาวะเปียกมาจาก{hi}"
else:
body = (f"ค่าสูงสุด {c(w['max_c'])} °C ในสภาวะเปียกมาจาก{hi} "
f"ส่วนค่าต่ำสุด {c(w['min_c'])} °C มาจาก{lo}")
return f"{head} {body} {CLOSING}"
def check(note: str, te: dict) -> list[str]:
errs = [f"มีคำต้องห้าม '{f}'" for f in FORBIDDEN if f in note]
for _, l in layers(te):
if l["code"] not in note and l.get("wet") is None:
errs.append(f"ไม่ได้เอ่ยถึง {l['code']} ที่ขาดค่าเปียก")
if te.get("wet"):
for k in ("min_c", "max_c"):
if not re.search(rf"(?<![\d.-]){re.escape(c(te['wet'][k]))}(?![\d.])", note):
errs.append(f"ไม่มีตัวเลข {k}={te['wet'][k]}")
return errs
def scrub(doc: dict) -> dict:
d = copy.deepcopy(doc)
for p in d["products"]:
for v in p["specifications"].get("variants") or []:
te = v.get("temperature_effective") or {}
te.pop("wet_note_th", None)
te.pop("wet_note_th_v30", None)
return d
def main() -> int:
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("src", nargs="?", default="modutech_v30_1.json")
ap.add_argument("dst", nargs="?", default="modutech_v30_2.json")
ap.add_argument("--changelog")
a = ap.parse_args()
src, dst, cl = a.src, a.dst, a.changelog
if src == dst:
raise SystemExit("ห้ามเขียนทับไฟล์ต้นทาง")
data = json.load(open(src, encoding="utf-8"))
shadow = copy.deepcopy(data)
rows, errs = [], []
for p in data["products"]:
for v in p["specifications"].get("variants") or []:
te = v.get("temperature_effective") or {}
if "wet_note_th" not in te:
continue
if "wet_note_th_v30" in te:
raise SystemExit(f"{v['variant_id']}: มี wet_note_th_v30 อยู่แล้ว — ไฟล์นี้ถูกแพตช์แล้ว หยุด")
old = te["wet_note_th"]
new = new_note(te)
for e in check(new, te):
errs.append(f"{v['variant_id']}: {e}")
te["wet_note_th_v30"] = old
te["wet_note_th"] = new
rows.append((p["belt_code"], v["variant_id"], te["wet_status"], old, new))
print(f"1) สร้าง wet_note_th ใหม่ {len(rows)} ข้อความ")
if errs:
raise SystemExit("❌ ตรวจข้อความไม่ผ่าน:\n " + "\n ".join(errs))
print(" ✅ ไม่มี เกรด/ก่อนเสนอ/ชั้นที่มีข้อมูล · ทุกวัสดุที่ขาดค่าเปียกถูกเอ่ยชื่อ · ตัวเลขตรงกับช่วงเปียก")
print("2) เทียบกับ shadow copy")
if fingerprint(scrub(shadow)) != fingerprint(scrub(data)):
raise SystemExit("❌ มีอย่างอื่นเปลี่ยนนอกจาก wet_note_th — ไม่เขียนไฟล์")
print(" ✅ นอกจาก wet_note_th / wet_note_th_v30 ไม่มีอะไรเปลี่ยน")
print("3) จำนวนนับ")
n_pair = sum(len(p.get("industries") or []) for p in data["products"])
n_app = sum(len(a.get("applications") or []) for p in data["products"] for a in p.get("industries") or [])
print(f" สินค้า {len(data['products'])} · คู่ {n_pair} · การใช้งาน {n_app}")
if (len(data["products"]), n_pair, n_app) != (68, 211, 651):
raise SystemExit("❌ จำนวนนับเปลี่ยน")
json.dump(data, open(dst, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
print(f"\nเขียน {dst} แล้ว · {src} ไม่ถูกแตะต้อง")
if cl:
write_changelog(cl, src, dst, rows)
print(f"เขียน {cl} แล้ว")
return 0
def write_changelog(path, src, dst, rows):
groups: dict[tuple, list] = {}
for code, vid, st, old, new in rows:
groups.setdefault((old, new), []).append((code, vid))
out = [f"# CHANGELOG {dst}", "",
f"สร้างจาก `{src}` ด้วย `patch_v30_2.py` · แก้ `temperature_effective.wet_note_th` อย่างเดียว "
f"{len(rows)} variant · ของเดิมเก็บใน `wet_note_th_v30`", "",
"ตัวเลข สถานะ และฟิลด์อื่นทุกตัวเหมือน v30_1 (ตรวจด้วย shadow copy)", "",
"## ทำไมต้องแก้", "",
"- ข้อความเดิมเรียกวัสดุพินว่า \"เกรด\" (เช่น \"เกรด PA\") ลูกค้าเข้าใจว่าสายพานเป็นเกรด PA",
"- ไม่บอกว่าวัสดุชั้นไหนขาดค่าเปียก และตัวเลขแต่ละตัวมาจากไหน",
"- \"ชั้นที่มีข้อมูล\" เป็นศัพท์ภายใน · \"ก่อนเสนอ\" เป็นคำของฝ่ายขาย",
"- รุ่นที่สายพานกับพินเป็นวัสดุเดียวกันพิมพ์ซ้ำ \"PA, PA\"", "",
f"## ก่อน → หลัง ({len(groups)} แบบข้อความ)", ""]
for i, ((old, new), vs) in enumerate(sorted(groups.items(), key=lambda kv: -len(kv[1])), 1):
out += [f"### {i}. {len(vs)} variant", "",
"รุ่น: " + " · ".join(f"{code} (`{vid}`)" for code, vid in vs), "",
f"- ก่อน: {old}", f"- หลัง: {new}", ""]
open(path, "w", encoding="utf-8").write("\n".join(out))
if __name__ == "__main__":
sys.exit(main())
|