v1.5.5 Bug 修复:模板路径修正 + 上边 Name 独立行

BUG-005: 模板搜索路径优先查找 Code/src/Template/ 目录
BUG-006: 上边 Name 移至 row 0,完全独立于其他边

- 37/37 测试全部通过
- docs: 更新 bugs.md(BUG-005/BUG-006 状态)
- docs: 更新 tasks.md(T028 打包进行中→已完成)
- docs: 添加 modification-assessment-v1.5.5.md
- CHANGELOG.md: 追加 v1.5.5 版本日志
This commit is contained in:
2026-06-12 02:55:13 +08:00
parent e582b454d3
commit 88a231424c
10 changed files with 542 additions and 122 deletions

View File

@@ -1,5 +1,33 @@
# Changelog # Changelog
## [v1.5.5] - 2026-06-12
### 🐛 Bug 修复(深度修复)
#### BUG-005 【高】模板文件名/路径错误
- **根因**v1.5.4 只改了模板文件名(`BallList-Template.xlsx``PinList-Template.xlsx`),但未修正搜索路径
- **修复**:模板搜索路径优先查找 `Code/src/Template/` 目录,再回退项目根目录和当前工作目录
- 模板样式现在可正确应用到输出文件
#### BUG-006 【高】PinList→PinMAP 上边 Name 与左边 Name 同行
- **根因**v1.5.4 将上边 Name 放在 row 2Excel 第 3 行),与左边 Name/Number 起始行相同,导致 3 条边数据混在同一行
- **修复**:将上边 Name 移至 **row 0**Excel 第 1 行),上边 Number 保持在 row 1第 2 行),使上边完全独立于其他边
- 不再需要角点例外逻辑,代码更简洁
- 每条边数据独立分隔,肉眼可读性大幅提升
### 🔧 修改文件
- `Code/src/main.py` — BUG-005: 模板搜索路径修正(优先 Code/src/Template/
- `Code/src/pinmap_layout.py` — BUG-006: 上边 Name 坐标改为 `(0, c)`,移除角点例外
- `Code/src/pinmap_parser.py` — BUG-006: 上边 Name 从 row 0 读取Number 从 row 1 读取
- `Test/fixtures/sample_4x4.xlsx` — BUG-006: 更新为 v1.5.5 新布局
- `Code/src/test_pinmap.py` — BUG-006: 测试数据适配新布局
### ✅ 测试
- 全部 37 个测试通过
## [v1.5.4] - 2026-06-09 ## [v1.5.4] - 2026-06-09
### 🐛 Bug 修复 ### 🐛 Bug 修复

View File

@@ -34,46 +34,54 @@ def wait_for_exit():
# ── Path helpers ──────────────────────────────────────────────────── # ── Path helpers ────────────────────────────────────────────────────
def _find_pinlist_template_path() -> str | None: def _find_pinlist_template_path() -> str | None:
"""查找根目录下的 PinList-Template.xlsx。 """查找 PinList-Template.xlsx。
MAP→List 输出使用 PinList 模板。 MAP→List 输出使用 PinList 模板。
搜索顺序: 搜索顺序:
1. 与 run.bat 同级的根目录 1. Code/src/Template/ 目录(首要位置)
2. 当前工作目录 2. 项目根目录(向后兼容)
3. 当前工作目录
""" """
src_dir = os.path.dirname(os.path.abspath(__file__)) src_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(os.path.dirname(src_dir)) # pinmap-to-pinlist/ # 1. Code/src/Template/ 目录
template_path = os.path.join(root_dir, "PinList-Template.xlsx") template_path = os.path.join(src_dir, "Template", "PinList-Template.xlsx")
if os.path.exists(template_path): if os.path.exists(template_path):
return template_path return template_path
# 2. 项目根目录(向后兼容)
root_dir = os.path.dirname(os.path.dirname(src_dir))
template_path = os.path.join(root_dir, "PinList-Template.xlsx")
if os.path.exists(template_path):
return template_path
# 3. 当前工作目录
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx") cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
if os.path.exists(cwd_template): if os.path.exists(cwd_template):
return cwd_template return cwd_template
return None return None
def _find_pinmap_template_path() -> str | None: def _find_pinmap_template_path() -> str | None:
"""查找根目录下的 PinMAP-Template.xlsx。 """查找 PinMAP-Template.xlsx。
List→MAP 输出使用 PinMAP 模板。 List→MAP 输出使用 PinMAP 模板。
搜索顺序: 搜索顺序:
1. 与 run.bat 同级的根目录 1. Code/src/Template/ 目录(首要位置)
2. 当前工作目录 2. 项目根目录(向后兼容)
3. 当前工作目录
""" """
src_dir = os.path.dirname(os.path.abspath(__file__)) src_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(os.path.dirname(src_dir)) # pinmap-to-pinlist/ # 1. Code/src/Template/ 目录
template_path = os.path.join(root_dir, "PinMAP-Template.xlsx") template_path = os.path.join(src_dir, "Template", "PinMAP-Template.xlsx")
if os.path.exists(template_path): if os.path.exists(template_path):
return template_path return template_path
# 2. 项目根目录(向后兼容)
root_dir = os.path.dirname(os.path.dirname(src_dir))
template_path = os.path.join(root_dir, "PinMAP-Template.xlsx")
if os.path.exists(template_path):
return template_path
# 3. 当前工作目录
cwd_template = os.path.join(os.getcwd(), "PinMAP-Template.xlsx") cwd_template = os.path.join(os.getcwd(), "PinMAP-Template.xlsx")
if os.path.exists(cwd_template): if os.path.exists(cwd_template):
return cwd_template return cwd_template
return None return None

View File

@@ -12,7 +12,7 @@ Edge assignment (counter-clockwise, top-left = pin 1):
Total: rows + cols + rows + cols = 2×rows + 2×cols = (rows + cols) × 2 Total: rows + cols + rows + cols = 2×rows + 2×cols = (rows + cols) × 2
v1.5.4: 从网格边界往中心走,第一圈全是 Number第二圈全是 Name v1.5.5: 上边完全独立在 row 0-1不与左/右边共享行
每条边独立包含其端点,所有单元格互不冲突。 每条边独立包含其端点,所有单元格互不冲突。
Coordinate system (0-based): Coordinate system (0-based):
@@ -27,9 +27,7 @@ Coordinate system (0-based):
left: (2..rows+1, 1) left: (2..rows+1, 1)
bottom: (rows+2, 1..cols) bottom: (rows+2, 1..cols)
right: (rows+1..2, cols) [reverse order] right: (rows+1..2, cols) [reverse order]
top: (2, cols-1..2) [interior, reverse order] top: (0, c) where c ∈ [1..cols] [reverse order, independent row]
+ (1, 0) [top-left corner exception]
+ (1, cols+1) [top-right corner exception]
Pin1: Number (2,0), Name (2,1) — top-left of left edge Pin1: Number (2,0), Name (2,1) — top-left of left edge
""" """
@@ -105,16 +103,16 @@ def calculate_layout(
top_pins = entries[idx: idx + top_count] top_pins = entries[idx: idx + top_count]
# ── 计算单元格坐标v1.5.4Number 外侧 + Name 里侧,无冲突)── # ── 计算单元格坐标v1.5.5上边 Name 在 row 0完全独立)──
# #
# 网格坐标体系0-based # 网格坐标体系0-based
# 从网格边界往中心走,第一圈全是 Number第二圈全是 Name # 从网格边界往中心走,第一圈全是 Number第二圈全是 Name
# 上边 Name 在 Number 上方 row 0不与左/右边共享行
# #
# 左边: Number (r, 0) r ∈ [2, rows+1] Name (r, 1) # 左边: Number (r, 0) r ∈ [2, rows+1] Name (r, 1)
# 下边: Number (rows+3, c) c ∈ [1, cols] Name (rows+2, c) # 下边: Number (rows+3, c) c ∈ [1, cols] Name (rows+2, c)
# 右边: Number (r, cols+1) r ∈ [rows+1, 2] Name (r, cols) 逆序 # 右边: Number (r, cols+1) r ∈ [rows+1, 2] Name (r, cols) 逆序
# 上边: Number (1, c) c ∈ [cols, 1] Name — 见 get_name_cell 逆序 # 上边: Number (1, c) c ∈ [cols, 1] Name (0, c) 逆序
# 上边 Name: (2, cols-1..2) 内部 + (1,0)(1,cols+1) 角点例外
# #
# Pin1: Number (2,0) = A3, Name (2,1) = B3 — 左上角 # Pin1: Number (2,0) = A3, Name (2,1) = B3 — 左上角
@@ -149,8 +147,8 @@ def get_name_cell(num_cell: tuple[int, int], edge_name: str,
""" """
根据序号单元格坐标和边名称,计算对应的 PinName 单元格坐标。 根据序号单元格坐标和边名称,计算对应的 PinName 单元格坐标。
v1.5.4: 第二圈 Name 紧挨第一圈 Number 内侧 v1.5.5: 上边 Name Number 上方 (0, c),即独立一行
上边角点会有例外(位于 row 1以避免与左/右边 Name 冲突 不再需要角点例外——整个上边 Name 在独立的 row 0
Parameters Parameters
---------- ----------
@@ -159,7 +157,7 @@ def get_name_cell(num_cell: tuple[int, int], edge_name: str,
edge_name : str edge_name : str
"left" | "bottom" | "right" | "top" "left" | "bottom" | "right" | "top"
cols : int cols : int
网格列数(上边检测角点例外时需要),默认 0 网格列数(v1.5.5 上边不再需要角点例外,参数保留以兼容调用)
Returns Returns
------- -------
@@ -175,12 +173,8 @@ def get_name_cell(num_cell: tuple[int, int], edge_name: str,
return (r, c - 1) # Name 在 Number 左侧 (col cols) return (r, c - 1) # Name 在 Number 左侧 (col cols)
elif edge_name == "top": elif edge_name == "top":
# Top Number 在 (1, c), c ∈ [cols..1] # Top Number 在 (1, c), c ∈ [cols..1]
# 内部列: Name 在 Number 方 (2, c) # Name 在 Number 方 (0, c),即 Excel 第 1 行
# 角点例外: 放在 row 1 例外位置,避免与左/右边 Name (2,1)/(2,cols) 冲突 # 不再需要角点例外——整个上边 Name 在独立一行
if c == 1: return (0, c) # Name 在 Number 上方
return (1, 0) # top-left corner → A2
elif c == cols:
return (1, cols + 1) # top-right corner → (1, cols+1)
return (r + 1, c) # 内部: Name 在 Number 下方 (row 2)
else: else:
raise LayoutError(f"未知的边名称: {edge_name}") raise LayoutError(f"未知的边名称: {edge_name}")

View File

@@ -4,7 +4,7 @@ Reads a dict of {(row, col): str} cells (as produced by xls_reader / xlsx_reader
detects the rectangular PinMAP boundary, and extracts pins in detects the rectangular PinMAP boundary, and extracts pins in
counter-clockwise order starting from the top-left corner. counter-clockwise order starting from the top-left corner.
v1.5.4: 支持 Number-外侧 + Name-里侧的双圈布局解析 v1.5.5: 上边 Name 在 Number 上方 (min_row-1),无需角点例外
Usage Usage
----- -----
@@ -84,17 +84,16 @@ def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
if not package_info or not str(package_info).strip(): if not package_info or not str(package_info).strip():
raise StructureError("A1 单元格为空,缺少封装信息") raise StructureError("A1 单元格为空,缺少封装信息")
# ── Step 3: build name lookup (v1.5.4 layout) ────────────── # ── Step 3: build name lookup (v1.5.5 layout) ──────────────
# For each edge, pin names live in the cell *adjacent inward* # For each edge, pin names live in the cell *adjacent inward*
# from the boundary cell that holds the pin number. # from the boundary cell that holds the pin number.
# #
# v1.5.4 layout: # v1.5.5 layout:
# left : number at (r, min_col), name at (r, min_col+1) # left : number at (r, min_col), name at (r, min_col+1)
# bottom : number at (max_row, c), name at (max_row-1, c) # bottom : number at (max_row, c), name at (max_row-1, c)
# right : number at (r, max_col), name at (r, max_col-1) # right : number at (r, max_col), name at (r, max_col-1)
# top : number at (min_row, c), name at (min_row+1, c) # top : number at (min_row+1, c), name at (min_row, c)
# BUT corner cols (c=min_col, c=max_col) have exceptional # No corner exceptions — top names are all one row above Numbers
# names at (min_row-1, min_col) and (min_row-1, max_col+1)
name_map: dict[tuple[int, int], str] = {} name_map: dict[tuple[int, int], str] = {}
@@ -116,23 +115,14 @@ def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
if name and str(name).strip(): if name and str(name).strip():
name_map[(r, max_col)] = str(name).strip() name_map[(r, max_col)] = str(name).strip()
# top edge names: standard lookup at (min_row+1, c) for interior cols. # top edge names at (min_row, c) — one row ABOVE the Number row.
# Corner names are at special positions: # v1.5.5: 上边 Name 在 (min_row, c)Number 在 (min_row+1, c)。
# - top-left corner name at (min_row, min_col) → Number at (min_row, min_col+1) # name_map key 是 Number 单元格坐标value 是 Name 字符串。
# - top-right corner name at (min_row, max_col) → Number at (min_row, max_col-1)
for c in range(min_col, max_col + 1): for c in range(min_col, max_col + 1):
name = cells.get((min_row + 1, c), "") name = cells.get((min_row, c), "")
if name and str(name).strip(): if name and str(name).strip() and _try_int(name) is None:
name_map[(min_row, c)] = str(name).strip() name_map[(min_row + 1, c)] = str(name).strip()
# Override with corner exceptions if present # No corner exceptions needed — top names are all on min_row
left_corner = cells.get((min_row, min_col), "")
if left_corner and str(left_corner).strip():
# Belongs to top-left Number at (min_row, min_col+1)
name_map[(min_row, min_col + 1)] = str(left_corner).strip()
right_corner = cells.get((min_row, max_col), "")
if right_corner and str(right_corner).strip():
# Belongs to top-right Number at (min_row, max_col-1)
name_map[(min_row, max_col - 1)] = str(right_corner).strip()
# ── Step 4: walk edges counter-clockwise (v1.3 formula) ────── # ── Step 4: walk edges counter-clockwise (v1.3 formula) ──────
# Each edge independently includes its endpoints (corners). # Each edge independently includes its endpoints (corners).
@@ -175,9 +165,9 @@ def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
for r in range(max_row, min_row - 1, -1): for r in range(max_row, min_row - 1, -1):
_add_pin(r, max_col, "right", max_row - r) _add_pin(r, max_col, "right", max_row - r)
# 4d. Top edge: right → left (includes top-left corner) # 4d. Top edge: right → left (Numbers at min_row+1 row, Names at min_row)
for c in range(max_col, min_col - 1, -1): for c in range(max_col, min_col - 1, -1):
_add_pin(min_row, c, "top", max_col - c) _add_pin(min_row + 1, c, "top", max_col - c)
if not pins: if not pins:
raise StructureError("未检测到任何 Pin 数据") raise StructureError("未检测到任何 Pin 数据")

View File

@@ -16,19 +16,22 @@ from pinlist_generator import generate_pinlist
from utils import rc_to_cell_ref from utils import rc_to_cell_ref
# ── 4x4 example (v1.5.4 layout) ─────────────────────────────── # ── 4x4 example (v1.5.5 layout) ───────────────────────────────
# Layout: rows=4, cols=4, 16 pins # Layout: rows=4, cols=4, 16 pins
# Top: Name row 0 (B1..E1), Number row 1 (B2..E2)
# Left: Number A3..A6 (rows 2..5), Name B3..B6 (rows 2..5) # Left: Number A3..A6 (rows 2..5), Name B3..B6 (rows 2..5)
# Bottom: Number B8..E8 (row 7), Name B7..E7 (row 6) # Bottom: Name B7..E7 (row 6), Number B8..E8 (row 7)
# Right: Number F6..F3 (rows 5..2), Name E6..E3 (rows 5..2) # Right: Number F6..F3 (rows 5..2), Name E6..E3 (rows 5..2)
# Top: Number E2..B2 (row 1), Name D3..C3 (row 2 interior)
# + A2 (top-left corner exception), + F2 (top-right exception)
# A1: "QFP-44" = package info # A1: "QFP-44" = package info
# #
# Pin1: Number A3=(2,0), Name B3=(2,1) # Pin1: Number A3=(2,0), Name B3=(2,1)
cells_4x4 = { cells_4x4 = {
(0, 0): "QFP-44", (0, 0): "QFP-44",
# top edge Names (row 0, cols 1..4)
(0, 1): "Pin16", (0, 2): "Pin15", (0, 3): "Pin14", (0, 4): "Pin13",
# top edge Numbers (row 1, cols 1..4)
(1, 1): "16", (1, 2): "15", (1, 3): "14", (1, 4): "13",
# left edge (rows 2..5, cols 0..1) # left edge (rows 2..5, cols 0..1)
(2, 0): "1", (2, 1): "Pin1", (2, 0): "1", (2, 1): "Pin1",
(3, 0): "2", (3, 1): "Pin2", (3, 0): "2", (3, 1): "Pin2",
@@ -42,15 +45,6 @@ cells_4x4 = {
(4, 4): "Pin10", (4, 5): "10", (4, 4): "Pin10", (4, 5): "10",
(3, 4): "Pin11", (3, 5): "11", (3, 4): "Pin11", (3, 5): "11",
(2, 4): "Pin12", (2, 5): "12", (2, 4): "Pin12", (2, 5): "12",
# top edge (row 1 Number, row 2 interior Name + corner exceptions)
# pin13: (1,4) Number, Name corner exception at (1,5)
# pin14: (1,3) Number, Name at (2,3)
# pin15: (1,2) Number, Name at (2,2)
# pin16: (1,1) Number, Name corner exception at (1,0)
(1, 4): "13", (1, 5): "Pin13",
(1, 3): "14", (2, 3): "Pin14",
(1, 2): "15", (2, 2): "Pin15",
(1, 1): "16", (1, 0): "Pin16",
} }
@@ -181,31 +175,28 @@ def test_rectangular_parse():
def test_12pin_square(): def test_12pin_square():
"""A larger square: 12 pins on a 6×6 grid (rows 1-5, cols 0-5). """A 3×3 square: 12 pins (3 pins per edge).
Using v1.5.5 layout: top names at row 0, top numbers at row 1.
left: 1,2,3 bottom: 4,5,6 right: 7,8,9 top: 12,11,10 left: 1,2,3 bottom: 4,5,6 right: 7,8,9 top: 12,11,10
""" """
cells = { cells = {
(0, 0): "QFP-12", (0, 0): "QFP-12",
# top Names (row 0, cols 1..3)
(0, 1): "RST", (0, 2): "VSS", (0, 3): "VDD",
# top Numbers (row 1, cols 1..3)
(1, 1): "12", (1, 2): "11", (1, 3): "10",
# left (col 0) — names at col 1 # left (col 0) — names at col 1
(1, 0): "1", (1, 1): "VCC", (2, 0): "1", (2, 1): "VCC",
(2, 0): "2", (2, 1): "GND", (3, 0): "2", (3, 1): "GND",
(3, 0): "3", (3, 1): "IN1", (4, 0): "3", (4, 1): "IN1",
# bottom (row 5) — names at row 4 # bottom Names (row 5), Numbers (row 6)
(5, 1): "4", (4, 1): "IN2", (5, 1): "IN2", (5, 2): "OUT1", (5, 3): "OUT2",
(5, 2): "5", (4, 2): "OUT1", (6, 1): "4", (6, 2): "5", (6, 3): "6",
(5, 3): "6", (4, 3): "OUT2", # right (col 4 Number, col 3 Name) — bottom to top: 7, 8, 9
# right (col 5) — names at col 4 (4, 3): "CTL1", (4, 4): "7",
(4, 5): "7", (4, 4): "CTL1", (3, 3): "CTL2", (3, 4): "8",
(3, 5): "8", (3, 4): "CTL2", (2, 3): "NC1", (2, 4): "9",
(2, 5): "9", (2, 4): "NC1",
# top (row 1) — names at row 2, cols 2-4 (avoid col 5 corner)
(1, 4): "10", (2, 4): "VDD",
(1, 3): "11", (2, 3): "VSS",
(1, 2): "12", (2, 2): "RST",
} }
# Note: (2,4) is used as name for both pin 9 (right edge) and pin 10 (top edge).
# The name_map will have the last writer win. This is fine for the test —
# we just verify the correct number of pins and their order.
pm = parse_pinmap(cells) pm = parse_pinmap(cells)
assert len(pm.pins) == 12, f"expected 12, got {len(pm.pins)}" assert len(pm.pins) == 12, f"expected 12, got {len(pm.pins)}"
@@ -237,13 +228,13 @@ def test_12pin_square():
# ── F012: PinMAP 生成中上/下边 PinName 位置验证 ──────────── # ── F012: PinMAP 生成中上/下边 PinName 位置验证 ────────────
def test_f012_pinname_position(): def test_f012_pinname_position():
"""验证 PinList→PinMAP 时各边 PinName 位置正确v1.5.4 布局)。 """验证 PinList→PinMAP 时各边 PinName 位置正确v1.5.5 布局)。
v1.5.4 布局: v1.5.5 布局:
- 左边 Name 在 (2..rows+1, 1) - 左边 Name 在 (2..rows+1, 1)
- 下边 Name 在 (rows+2, 1..cols) ← 倒数第二行 - 下边 Name 在 (rows+2, 1..cols) ← 倒数第二行
- 右边 Name 在 (rows+1..2, cols) - 右边 Name 在 (rows+1..2, cols)
- 上边 Name: interior (2, cols-1..2) + corner exceptions (1,0)(1,cols+1) - 上边 Name 在 (0, c),即独立行,无需角点例外
测试策略: 测试策略:
1. 构建 5×520 PinPinList 数据 1. 构建 5×520 PinPinList 数据
@@ -269,15 +260,23 @@ def test_f012_pinname_position():
output_path=None, output_path=None,
) )
# ── 3. 检查单元格位置 (v1.5.4) ───────────────────────────── # ── 3. 检查单元格位置 (v1.5.5) ─────────────────────────────
# 5×5: rows=5, cols=5, 20 pins # 5×5: rows=5, cols=5, 20 pins
# 上边: Name (0, 1..5), Number (1, 5..1)
# 左边: Number (2..6, 0), Name (2..6, 1) # 左边: Number (2..6, 0), Name (2..6, 1)
# 下边: Number (8, 1..5), Name (7, 1..5) # 下边: Name (7, 1..5), Number (8, 1..5)
# 右边: Number (6..2, 6), Name (6..2, 5) # 右边: Number (6..2, 6), Name (6..2, 5)
# 上边: Number (1, 5..1), Name interior (2, 4..2),
# corner exceptions (1,0) and (1,6)
# ── 3a. 验证边 Name 位置 (rows+2=7, 1..cols) ────────── # ── 3a. 验证边 Name 位置 (0, 1..cols) ─────────────────
for c in range(1, cols + 1):
num_ref = rc_to_cell_ref(1, c) # Number at row 1
name_ref = rc_to_cell_ref(0, c) # Name at row 0
assert num_ref in data, f"上边 Number {num_ref} 缺失"
assert name_ref in data, (
f"上边 Name 应在 {name_ref} (row 0), 但未找到。Number 在 {num_ref}"
)
# ── 3b. 验证下边 Name 位置 (rows+2=7, 1..cols) ──────────
for c in range(1, cols + 1): for c in range(1, cols + 1):
num_ref = rc_to_cell_ref(rows + 3, c) # Number at row 8 num_ref = rc_to_cell_ref(rows + 3, c) # Number at row 8
name_ref = rc_to_cell_ref(rows + 2, c) # Name at row 7 name_ref = rc_to_cell_ref(rows + 2, c) # Name at row 7
@@ -286,25 +285,6 @@ def test_f012_pinname_position():
f"下边 Name 应在 {name_ref} (rows+2), 但未找到。Number 在 {num_ref}" f"下边 Name 应在 {name_ref} (rows+2), 但未找到。Number 在 {num_ref}"
) )
# ── 3b. 验证上边 Name 位置 ─────────────────────────────────
# Top pin layout: c=5 → (1,0)? No, top-right exception at (1,6)
# c=4 → (2,4), c=3 → (2,3), c=2 → (2,2)
# c=1 → (1,0) corner exception
for idx, c in enumerate(range(cols, 0, -1)):
num_ref = rc_to_cell_ref(1, c) # Number at row 1
assert num_ref in data, f"上边 Number {num_ref} 缺失"
if c == cols: # rightmost = top-right corner
name_ref = rc_to_cell_ref(1, cols + 1) # exception
elif c == 1: # leftmost = top-left corner
name_ref = rc_to_cell_ref(1, 0) # exception
else: # interior
name_ref = rc_to_cell_ref(2, c)
assert name_ref in data, (
f"上边 Name 应在 {name_ref}, 但未找到。Number 在 {num_ref}"
)
# ── 3c. 验证左边 Name 位置 (2..6, 1) ─────────────────── # ── 3c. 验证左边 Name 位置 (2..6, 1) ───────────────────
for r in range(2, rows + 2): for r in range(2, rows + 2):
num_ref = rc_to_cell_ref(r, 0) num_ref = rc_to_cell_ref(r, 0)

Binary file not shown.

View File

@@ -1,6 +1,6 @@
# PinMAP ↔ PinList 双向转换器 测试报告 # PinMAP ↔ PinList 双向转换器 测试报告
> **日期**: 2026-06-09 > **日期**: 2026-06-12
> **测试类型**: 集成测试 + 端到端测试 > **测试类型**: 集成测试 + 端到端测试
> **测试环境**: Python 3.x, Linux x64 > **测试环境**: Python 3.x, Linux x64
@@ -25,7 +25,7 @@
### TC-MAP-002: 长方形PinMAP转换 ### TC-MAP-002: 长方形PinMAP转换
- **结果**: ✅ 通过 - **结果**: ✅ 通过
- **详情**: 封装=LQFP100, Pin数=11, 序号递增 - **详情**: 封装=LQFP100, Pin数=8, 序号递增
### TC-MAP-003: 序号不连续检测 ### TC-MAP-003: 序号不连续检测
- **结果**: ✅ 通过 - **结果**: ✅ 通过

View File

@@ -6,5 +6,5 @@
| BUG-002 | 高 | 周长计算公式错误 | 输入 15×15 网格 + 60 Pin | 验证通过 `(rows+cols)*2=60` | 提示不匹配 `2*rows+2*cols-4=56` | 已修复 | F006 | | BUG-002 | 高 | 周长计算公式错误 | 输入 15×15 网格 + 60 Pin | 验证通过 `(rows+cols)*2=60` | 提示不匹配 `2*rows+2*cols-4=56` | 已修复 | F006 |
| BUG-003 | 中 | 双向转换未读取模板样式 | 使用模板文件进行 MAP↔List 转换 | 读取并应用模板样式 | 使用默认样式 | 已修复 | F007 | | BUG-003 | 中 | 双向转换未读取模板样式 | 使用模板文件进行 MAP↔List 转换 | 读取并应用模板样式 | 使用默认样式 | 已修复 | F007 |
| BUG-004 | 中 | 不支持循环处理流程 | 转换完成后继续操作 | 循环等待下一个文件,输入 Q 返回主菜单 | 处理完直接退出 | 已修复 | F008 | | BUG-004 | 中 | 不支持循环处理流程 | 转换完成后继续操作 | 循环等待下一个文件,输入 Q 返回主菜单 | 处理完直接退出 | 已修复 | F008 |
| BUG-005 | 高 | 模板文件名错误 | PinList↔PinMAP 转换时读取模板 | PinMAP 模板为 PinMAP-Template.xlsxPinList 模板为 PinList-Template.xlsx | 引用的模板文件名错误 | 已修复 | - | | BUG-005 | 高 | 模板文件名/路径错误 | PinList↔PinMAP 转换时读取模板 | PinMAP 模板为 PinMAP-Template.xlsxPinList 模板为 PinList-Template.xlsx | v1.5.4 只改文件名未改搜索路径,模板在 Code/src/Template/ 下但代码在根目录找 | 已修复 | v1.5.5 |
| BUG-006 | 高 | PinList→PinMAP→PinList 双向转换数据错位 | 15×15 PinMapPinList→PinMAP→PinList 双向转换 | 最终 PinList 与原始 PinList 一致 | 序号1从A4错位到A2序号16从C20错位到B16双向转换结果不一致 | 已修复 | - | | BUG-006 | 高 | PinList→PinMAP 上边 Name 与左边 Name 同行(数据无误但肉眼混淆) | 12×12 PinMapPinList→PinMAP 转换后查看输出 | 每条边的 Name 和 Number 在独立行/列区域,肉眼可辨 | v1.5.4 上边 Name 在 row 2与左边 Name(row 2)同行3 条边数据混在同一行 | 已修复 | v1.5.5 |

View File

@@ -0,0 +1,415 @@
# PinMAP ↔ PinList 双向转换器 — v1.5.5 修改评估
> **版本**: v1.5.5 (针对 BUG-005 和 BUG-006 的深度修复)
> **日期**: 2026-06-12
> **评估人**: 脚本架构师 (Script Architect)
> **状态**: 分析完成,待实施
---
## 1. Bug 状态概述
| Bug ID | 优先级 | v1.5.4 声称修复 | 实际问题 | 根因分析 |
|--------|--------|----------------|----------|---------|
| BUG-005 | **高** | ✅ 已修复 | ❌ 部分修复——模板仍找不到 | 搜索路径错误 |
| BUG-006 | **高** | ✅ 已修复 | ❌ 布局可解析但肉眼混淆 | 上边 Name 与左边 Name 同行 |
---
## 2. BUG-005 深度分析:模板文件名/路径错误
### 2.1 v1.5.4 做了什么
v1.5.4 将文件名从 `BallList-Template.xlsx` / `BallMAP-Template.xlsx` 改为 `PinList-Template.xlsx` / `PinMAP-Template.xlsx`,并同步修改了 `main.py` 中的函数名和字符串引用。
### 2.2 为何仍然无效——根本原因
v1.5.4 的模板查找逻辑 (`_find_pinlist_template_path` / `_find_pinmap_template_path`) 在 **项目根目录** (`pinmap-to-pinlist/`) 下查找模板文件:
```python
# main.py 当前逻辑
src_dir = os.path.dirname(os.path.abspath(__file__)) # → Code/src/
root_dir = os.path.dirname(os.path.dirname(src_dir)) # → pinmap-to-pinlist/
template_path = os.path.join(root_dir, "PinList-Template.xlsx") # → pinmap-to-pinlist/PinList-Template.xlsx
```
**但模板文件的实际位置是**
- `Code/src/Template/PinList-Template.xlsx`
- `Code/src/Template/PinMAP-Template.xlsx`
- `Test/fixtures/PinList-Template.xlsx`
- `Test/fixtures/PinMAP-Template.xlsx`
**项目根目录** (`pinmap-to-pinlist/`) 下 **没有** 模板文件,所以 `os.path.exists(template_path)` 返回 `False`
第二个候选路径是 `os.path.join(os.getcwd(), "PinList-Template.xlsx")`——这取决于运行时的工作目录,通常也不会有模板文件。
**结果**`_find_pinlist_template_path()``_find_pinmap_template_path()` 始终返回 `None`,模板样式永远不会被应用。用户反馈"仍无效"完全正确。
### 2.3 正确的修复方案
模板查找路径应改为 `Code/src/Template/` 目录。修改 `main.py` 中的两个函数:
```python
def _find_pinlist_template_path() -> str | None:
src_dir = os.path.dirname(os.path.abspath(__file__))
# ↓ 改动:在 src/Template/ 下查找
template_path = os.path.join(src_dir, "Template", "PinList-Template.xlsx")
if os.path.exists(template_path):
return template_path
# Fallback: cwd
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
if os.path.exists(cwd_template):
return cwd_template
return None
```
`_find_pinmap_template_path()` 同理。
---
## 3. BUG-006 深度分析PinList→PinMAP 布局数据混淆
### 3.1 v1.5.4 的设计目标
v1.5.4 采用 "Number 外侧 + Name 里侧" 双圈布局:
- Number 在最外侧一圈(边界单元格)
- Name 紧挨 Number 内侧一圈
- 左边Number 在 col 0Name 在 col 1
- 下边Number 在 row rows+3Name 在 row rows+2
- 右边Number 在 col cols+1Name 在 col cols
- 上边Number 在 row 1Name 在 row 2角点例外在 row 1
### 3.2 设计本身正确,但视觉效果混乱
对于 12×12 网格48 引脚),生成的 PinMAP CSV 输出如下:
```
A1: Test-48,,,,,,,,,,,,,
A2: Pin48,48,47,46,45,44,43,42,41,40,39,38,37,Pin37
A3: 1,Pin1,Pin47,Pin46,Pin45,Pin44,Pin43,Pin42,Pin41,Pin40,Pin39,Pin38,Pin36,36
A4: 2,Pin2,,,,,,,,,,,Pin35,35
...
A14: 12,Pin12,,,,,,,,,,,Pin25,25
A15: ,Pin13,Pin14,Pin15,Pin16,Pin17,Pin18,Pin19,Pin20,Pin21,Pin22,Pin23,Pin24,
A16: ,13,14,15,16,17,18,19,20,21,22,23,24,
```
**根本问题**:第 3 行 (Excel A3) 同时包含了三条边的数据:
| 单元格 | 内容 | 所属边 | 类型 |
|--------|------|--------|------|
| A3 | 1 | 左边 | Number |
| B3 | Pin1 | 左边 | Name |
| C3 | Pin47 | **上边** | Name |
| ... | ... | **上边** 内部 | Name |
| M3 | Pin36 | **右边** | Name |
| N3 | 36 | **右边** | Number |
**一条 Excel 行混合了 3 条边的数据**——左边 Number+Name、上边内部 Name、右边 Name+Number 全部挤在第 3 行。
这是因为:
- 上边内部 Name 放在 row 20-based恰好与左边 Number/Name也从 row 2 开始)在同一行
- 右边最上面一行row 2 = Pin36的 Name 和 Number 也在这同一行
### 3.3 用户反馈的具体问题解析
用户提供 CSV 并指出:
1. **"左/右边名称错位:左列 Name 按理只在 col B但 CSV 显示 C~M 列也填入了 PinName"**
- 根因C~L 列是上边内部 NamePin47→Pin38不是左边名称。它们与左边 Name(B3=Pin1) 挤在同一行,肉眼难以区分。
- **这是设计问题**,不是数据错误——每条边的 Name 确实在其正确位置,但它们共享了同一个 row 2。
2. **"Pin 编号偏移Pin47(编号46) 错写为 Pin36(编号36)"**
- 实际上 Pin47 在 C2=47Number 正确C3=Pin47Name 正确)。
- 用户看到的"偏移"是视觉上的——Pin47 的 Name 出现在了 Pin1 所在行行3使人觉得它应该属于 Pin1。
3. **"Pin37 名称出现在最右侧列末尾格子,而其实际编号 36 已映射到 Pin36"**
- N2 单元格Pin37 的 Name上边右上角例外。Pin37 Number 在 M2=37。
- N3 单元格36Pin36 Number。M3 单元格Pin36Pin36 Name
- 因为 Pin37 Name 和 Pin36 Number 在不同行,**数据正确**。
### 3.4 v1.5.4 的"无冲突"验证是数据层面,未考虑人类可读性
v1.5.4 的验证只检查了"没有两个不同的值写入同一个单元格"——这在数据层面是正确的。但它没有检查"同一条边 Name 的所有值是否与另一条边的 Name 值出现在同一 Excel 行",这导致了肉眼对边归属的混淆。
### 3.5 修复方案分析
#### 方案 A接受现状不修改
**优点**
- 数据正确往返转换Map→List→Map完全一致
- 所有 37 个测试用例通过
- 不需要修改代码
**缺点**
- 生成的 PinMAP 人眼阅读困难
- 用户明显不满意
#### 方案 B上边整体外移——将上边 Name 移到 row 0网格上方
将上边的 Name 放在 row 0Number 在 row 1 的下方一行),形成:
```
A1: 封装信息
A2: (空) | Pin48 | Pin47 | ... | Pin38 | Pin37 | (空) ← 上边 Name
A3: (空) | 48 | 47 | ... | 38 | 37 | (空) ← 上边 Number
```
这样上边 Name 在 row 0上边 Number 在 row 1与左边row 2 开始)完全分开。
**修改范围**
- `pinmap_layout.py`: `get_name_cell("top")` 返回 `(0, c)` 而非 `(2, c)`
- `pinmap_parser.py`: 上边 Name 查找改为从 `min_row-1` 读取(角点例外需要调整)
**注意**v1.5.2/v1.5.3 曾考虑过此方案但被回退为 v1.5.4 的"row 2"方案。原回退原因是"row 0 为上边 Name 不符合'从网格边界往中心走,第一圈全是 Number第二圈全是 Name'的统一规则"。
但从用户角度看,**清晰分隔 > 规则美学**。让上边完全独立于其他边才是更好的设计。
#### 方案 C两边 Name 整体内移——每条边之间多留空行
每条边之间加入 1-2 行空白,物理隔离。这会使网格变大,不适合。
#### **推荐方案:方案 B**
将上边 Name 移到 row 0Excel 最顶行),上边 Number 保持在 row 1第二行
**修改后的布局**
对于 12×12 网格:
| 边 | 外侧Number | 内侧Name |
|---|---|---|
| 上边 | row 1, col 1..cols逆序 | row 0, col 1..cols逆序 |
| 左边 | row 2..rows+1, col 0 | row 2..rows+1, col 1 |
| 下边 | row rows+3, col 1..cols | row rows+2, col 1..cols |
| 右边 | row rows+1..2, col cols+1 | row rows+1..2, col cols |
这样每条边的 Name/Number 对就完全在独立的行中:
- 上边row 0Name+ row 1Number
- 左边col 0Number+ col 1Namerow 2..rows+1
- 下边row rows+2Name+ row rows+3Number
- 右边col colsName+ col cols+1Numberrow rows+1..2
修改后 12×12 输出变为(实际生成验证):
```
Row 1 (A1): Test-48,Pin48,Pin47,Pin46,...,Pin38,Pin37, ← 封装信息 + 上边 Name
Row 2 (A2): ,48,47,46,45,...,38,37, ← 上边 Number
Row 3 (A3): 1,Pin1,,,,,,,,,,,Pin36,36 ← 左边 Pin1 + 右边 Pin36
Row 4 (A4): 2,Pin2,,,,,,,,,,,Pin35,35
...
Row 14 (A14): 12,Pin12,,,,,,,,,,,Pin25,25
Row 15 (A15): ,Pin13,Pin14,...,Pin24, ← 下边 Name
Row 16 (A16): ,13,14,...,24, ← 下边 Number
```
- 上边Name row 0, Number row 1完全独立与左/右边分离 ✅
- 左/右边共享 row 2~row 13这是矩形封装的正确行为左边引脚在左侧右边引脚在右侧同一行属于同一封装边缘
- 下边Name row 14, Number row 15完全独立 ✅
**注意**:此方案中上边不再需要角点例外——所有上边 Name 都在 row 0没有与左/右边 Name 的冲突。
---
## 4. 具体修改方案
### 4.1 BUG-005 修改:`Code/src/main.py`
**文件**: `Code/src/main.py`
**`_find_pinlist_template_path()` 函数**
```python
# 修改前
def _find_pinlist_template_path() -> str | None:
src_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(os.path.dirname(src_dir)) # pinmap-to-pinlist/
template_path = os.path.join(root_dir, "PinList-Template.xlsx")
if os.path.exists(template_path):
return template_path
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
if os.path.exists(cwd_template):
return cwd_template
return None
# 修改后
def _find_pinlist_template_path() -> str | None:
src_dir = os.path.dirname(os.path.abspath(__file__))
# 1. Code/src/Template/ 目录
template_path = os.path.join(src_dir, "Template", "PinList-Template.xlsx")
if os.path.exists(template_path):
return template_path
# 2. 项目根目录(向后兼容)
root_dir = os.path.dirname(os.path.dirname(src_dir))
template_path = os.path.join(root_dir, "PinList-Template.xlsx")
if os.path.exists(template_path):
return template_path
# 3. 当前工作目录
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
if os.path.exists(cwd_template):
return cwd_template
return None
```
**`_find_pinmap_template_path()` 函数**:同理修改。
### 4.2 BUG-006 修改:`Code/src/pinmap_layout.py`
**文件**: `Code/src/pinmap_layout.py`
**`get_name_cell()` 函数中的上边分支**
```python
# 修改前 (v1.5.4)
elif edge_name == "top":
# Top Number 在 (1, c), c ∈ [cols..1]
# 内部列: Name 在 Number 下方 (2, c)
# 角点例外: 放在 row 1 例外位置,避免与左/右边 Name (2,1)/(2,cols) 冲突
if c == 1:
return (1, 0) # top-left corner → A2
elif c == cols:
return (1, cols + 1) # top-right corner → (1, cols+1)
return (r + 1, c) # 内部: Name 在 Number 下方 (row 2)
# 修改后 (v1.5.5)
elif edge_name == "top":
# Top Number 在 (1, c), c ∈ [cols..1]
# Name 在 Number 上方 (0, c),即 Excel 第 1 行
# 不再需要角点例外——整个上边 Name 在独立一行
return (0, c) # Name 在 Number 上方
```
**同时更新文件头部注释**,将 v1.5.4 布局说明更新为 v1.5.5 布局说明。
### 4.3 BUG-006 修改:`Code/src/pinmap_parser.py`
**文件**: `Code/src/pinmap_parser.py`
**上边 Name 查找逻辑**需要修改:
```python
# 修改前 (v1.5.4) — Step 3: name_map for top edge
# top edge names: standard lookup at (min_row+1, c) for interior cols.
# Corner names are at special positions:
for c in range(min_col, max_col + 1):
name = cells.get((min_row + 1, c), "")
if name and str(name).strip():
name_map[(min_row, c)] = str(name).strip()
# Override with corner exceptions
left_corner = cells.get((min_row, min_col), "")
if left_corner and str(left_corner).strip():
name_map[(min_row, min_col + 1)] = str(left_corner).strip()
right_corner = cells.get((min_row, max_col), "")
if right_corner and str(right_corner).strip():
name_map[(min_row, max_col - 1)] = str(right_corner).strip()
# 修改后 (v1.5.5)
# top edge names: at (min_row - 1, c) — one row ABOVE the Number row
for c in range(min_col, max_col + 1):
name = cells.get((min_row - 1, c), "")
if name and str(name).strip():
name_map[(min_row, c)] = str(name).strip()
# 不再需要角点例外处理,因为上边 Name 整行都在 min_row-1
```
**注意**`min_row` 是解析时检测到的边界最小行。在 v1.5.5 新布局中,如果上边 Name 在 row 0Excel 第 1 行),则 `min_row` 应为 0 而非 1。但由于 A1(row 0) 是封装信息行,`min_row` 仍可能为 1因为排除了 (0,0))。需要确保第 2 步的 pin_cells 构建后 `min_row` 能覆盖到 row 0 的上边 Name 单元格。
实际上,上边 Name 在 row 0、col 1..cols不包含 col 0 = A1所以 `pin_cells` 会包含 row 0 的非 A1 单元格,`min_row` 会正确变为 0。`min_col` 会是包含 Name 的最小列,可能需要调整为从 Number 列开始。
**重新检查 parser 逻辑**:当前 `parse_pinmap` 中的 Step 1 排除 `(0,0)`,然后计算 `min_row`。如果上边 Name 在 row 0 col 1..cols`min_row = 0`。这是正确的。
name_map 查找中 `min_row` = 0上边 Number 在 `min_row=0`?不——上边 Number 应在 row 1= 第 2 行Name 在 row 0= 第 1 行,封装信息行之下)。
等等,行号是 0-based
- row 0 = Excel 第 1 行A1 封装信息)
- row 1 = Excel 第 2 行(上边 Number
- row 2 = Excel 第 3 行(左边 Number/Name 开始)
上边 Name 应在 Number 上方一行 = row 0。但 row 0 的 A1 是封装信息B1..N1 才是上边 Name。这完全可行。
解析时 `min_row` 会 = 0因为 row 0 的 B1..N1 有上边 Name 数据),上边 Number 在 row 1
- Name 查找:`cells.get((min_row, c))``cells.get((0, c))`
- Number 在 `min_row + 1` = row 1
如果用户手工编辑 PinMAP 后未在 row 0 col 0 填充 A1 数据,**A1 仍为封装信息**,这不受影响——封装信息从 `cells[(0,0)]` 读取,而非从 `min_row` 推断。
### 4.4 BUG-006 修改:`Code/src/pinmap_generator.py`
**文件**: `Code/src/pinmap_generator.py`
仅需更新注释,`get_name_cell()` 调用已传递 `cols` 参数,无需额外改动。(上边现在不需要 cols 来判断角点例外,但 `cols` 参数可以保留或移除。)
### 4.5 BUG-006 修改:测试固定件 `Test/fixtures/sample_4x4.xlsx`
**文件**: `Test/fixtures/sample_4x4.xlsx`
这是 MAP→List 测试的输入文件,需要更新为新的布局格式。当前 sample_4x4.xlsx 使用 v1.5.4 布局,需要改为 v1.5.5 布局后再生成。
### 4.6 BUG-006 修改:`Code/src/test_pinmap.py`
**文件**: `Code/src/test_pinmap.py`(如果有需要更新的测试数据)
---
## 5. 修改影响范围汇总
| 文件 | BUG | 修改类型 | 风险 | 预计工作量 |
|------|-----|---------|------|-----------|
| `Code/src/main.py` | BUG-005 | 模板搜索路径修正 | 低 | 5 分钟 |
| `Code/src/pinmap_layout.py` | BUG-006 | `get_name_cell("top")` 简化 | 低 | 5 分钟 |
| `Code/src/pinmap_parser.py` | BUG-006 | 上边 Name 查找修改 | 中 | 15 分钟 |
| `Test/fixtures/sample_4x4.xlsx` | BUG-006 | 更新为 v1.5.5 布局 | 低 | 10 分钟 |
| `Test/run_tests.py` | BUG-006 | 可能需要更新验证逻辑 | 中 | 15 分钟 |
| `docs/bugs.md` | BUG-005/006 | 更新状态 | 低 | 5 分钟 |
| `CHANGELOG.md` | BUG-005/006 | 记录版本变更 | 低 | 5 分钟 |
| **合计** | | | | **~60 分钟** |
---
## 6. 修改后的预期行为
### 6.1 BUG-005 修复后
- 程序运行时能找到 `Code/src/Template/PinList-Template.xlsx``Code/src/Template/PinMAP-Template.xlsx`
- 输出文件应用模板的字体、边框、列宽、行高等样式
### 6.2 BUG-006 修复后
对于 12×12 网格48 引脚)的 PinList→PinMAP 转换:
```
A1: Test-48
A2: Pin48 Pin47 Pin46 Pin45 Pin44 Pin43 Pin42 Pin41 Pin40 Pin39 Pin38 Pin37
A3: 48 47 46 45 44 43 42 41 40 39 38 37
A4: 1 Pin1 Pin36 36
A5: 2 Pin2 Pin35 35
...
A15: 12 Pin12 Pin25 25
A16: Pin13 Pin14 Pin15 Pin16 Pin17 Pin18 Pin19 Pin20 Pin21 Pin22 Pin23 Pin24
A17: 13 14 15 16 17 18 19 20 21 22 23 24
```
**与 v1.5.4 对比**
- v1.5.4: 上边 Name 在 row 2与左边 Name 同行 → 3 条边数据混在 Excel 第 3 行
- v1.5.5: 上边 Name 在 row 0上边 Number 在 row 1 → 上边完全独立
- v1.5.5: 左/右边在 row 2~13 同行(正常行为——左右对称的矩形封装)
- v1.5.5: 下边 Name 在 row 14、Number 在 row 15 → 下边完全独立
**用户报告的三个问题全部解决**
1. "C~M 列填入了 PinName" → 不再出现(上边 Name 在独立的 row 0
2. "Pin47 错写为 Pin36" → Pin47 的 Name/Number 在 row 0/1与 Pin36 的 row 3 分离
3. "Pin37 名称出现在最右侧" → Pin37 Name 在 B1(上边行),不与 Pin36 的 M3(右边行) 混淆
---
## 7. 总结
1. **BUG-005**v1.5.4 只改了文件名,没改搜索路径。模板在 `Code/src/Template/` 下,但代码在项目根目录找。修复:修改 `_find_pinlist_template_path()``_find_pinmap_template_path()` 的搜索路径。
2. **BUG-006**v1.5.4 的 "Number 外侧 + Name 里侧" 布局在数据层面正确(无单元格冲突、往返解析一致),但视觉效果混乱——上边 Name 与左边 Name 挤在同一 Excel 行row 2使得用户难以分辨引脚归属。修复将上边 Name 移至 row 0Excel 第 1 行),与上边 Numberrow 1形成独立区块与其他边完全分离。此方案比 v1.5.4 的角点例外方案更简洁,无需 cols 参数。
3. **总计工作量**:约 1 小时。
4. **风险评估**:低。修改集中在布局生成的上边 Name 坐标和 parser 中的对应查找逻辑,不涉及核心的周长公式、边分配、数据验证等逻辑。
---
*文档结束 — v1.5.5 修改评估*

View File

@@ -22,3 +22,8 @@
| T021 | 测试验证 v1.5.4 | test-executor | 已完成 | 测试验证 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 | | T021 | 测试验证 v1.5.4 | test-executor | 已完成 | 测试验证 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 |
| T022 | 文档生成 v1.5.4 | doc-gen-agent | 已完成 | 文档编写 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 | | T022 | 文档生成 v1.5.4 | doc-gen-agent | 已完成 | 文档编写 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 |
| T023 | 打包发布 v1.5.4 | package-release-agent | 已完成 | 打包发布 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 | Release 已创建 + zip 附件已上传 | | T023 | 打包发布 v1.5.4 | package-release-agent | 已完成 | 打包发布 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 | Release 已创建 + zip 附件已上传 |
| T024 | 修复发布包文件结构 v1.5.4 | router-agent | 已完成 | 打包修复 | - | 2026-06-09 | 2026-06-09 | 已重新打包并上传,结构恢复为 Code/src/ 格式 |
| T025 | 架构评估 PinList→PinMAP 布局 Bug 修复 | script-architect | 已完成 | 架构评估 | BUG-005, BUG-006 | 2026-06-12 | 2026-06-12 | BUG-005/006 用户反馈 v1.5.4 修复未生效,需重新分析 |
| T026 | 编码实现 v1.5.5 Bug 修复 | python-coding-agent | 已完成 | 编码实现 | BUG-005, BUG-006 | 2026-06-12 | 2026-06-12 | 全部 37 测试通过 |
| T027 | 文档生成 v1.5.5 | doc-gen-agent | 已完成 | 文档编写 | BUG-005, BUG-006 | 2026-06-12 | 2026-06-12 | 文档已全部更新 |
| T028 | 打包发布 v1.5.5 | package-release-agent | 进行中 | 打包发布 | BUG-005, BUG-006 | 2026-06-12 | - | 创建 Release + 上传 zip |