From 75925168c7c302a32b45c862edc526a756659c89 Mon Sep 17 00:00:00 2001 From: lhl Date: Sun, 9 Aug 2026 04:13:01 +0800 Subject: [PATCH] =?UTF-8?q?test:=20MIXED=20=E6=B7=B7=E5=90=88=E6=A0=B7?= =?UTF-8?q?=E6=9C=AC=20+=20=E7=AB=AF=E5=88=B0=E7=AB=AF=E6=AE=B5=E8=90=BD?= =?UTF-8?q?=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _AI_USAGE_LOG.md | 1 + samples/要件定義_混合型.xlsx | Bin 0 -> 5178 bytes tests/test_real_samples.py | 23 ++++++++++++++++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 samples/要件定義_混合型.xlsx diff --git a/_AI_USAGE_LOG.md b/_AI_USAGE_LOG.md index 00a1f64..0284886 100644 --- a/_AI_USAGE_LOG.md +++ b/_AI_USAGE_LOG.md @@ -35,3 +35,4 @@ | 2026-08-09 03:59 | Agent 实现 | 里程碑2.5 Task9 实现:段落分割器 split_paragraphs(MIXED 完整段落解析第一步,纯函数)。新建 src/genesis/parsers/paragraph_splitter.py(_is_blank_row 行空白判定 + split_paragraphs(matrix)→list[(start,end)] 含端点 0-based,以全空行为界返回段落区间,空矩阵→[])与 tests/test_paragraph_splitter.py(5 用例:空矩阵/单段落无空行/中间空行分割/尾部空行不产生多余段落/前导空行从首个非空行开始);TDD 验证 RED(ModuleNotFoundError: No module named 'genesis.parsers.paragraph_splitter')→ GREEN(5 passed);pytest 全量 46 passed(41 baseline + 5 新增);不依赖 openpyxl 与 data_models;提交 34ef28b | src/genesis/parsers/paragraph_splitter.py, tests/test_paragraph_splitter.py | deepseek-v4-flash-free | | 2026-08-09 04:20 | Agent 实现 | 里程碑2.5 Task10 实现:data_models 扩展 MixedParagraph/MixedSheet 段落容器,并新增 ExcelParseResult.mixed 字段(默认空列表)。TDD:先追加 3 个测试(字段存在性与默认行为)确认 RED(ImportError: cannot import name 'MixedParagraph'),再在 data_models.py 末尾追加两个 dataclass(顶部 from typing 加 Literal、dataclasses 加 field),excel_parser.py 更新导入并为 ExcelParseResult 加 mixed 字段,确认 GREEN(3 passed),全量 pytest 49 passed(46 baseline + 3)。未修改既有类与字段,仅追加。 | src/genesis/data_models.py, src/genesis/parsers/excel_parser.py, tests/test_excel_parser.py | deepseek-v4-flash-free | | 2026-08-09 04:06 | Agent 实现 | 里程碑2.5 Task11 实现:MIXED 完整段落装配(分割→逐段最优解析)。excel_parser.py 新增 MIXED 分支:整表先 forward_fill(合并单元格)再按段切片;对每段 classify_sheet,TABLE 段用 find_header_row(seg)(段内相对,<0 回落 0)并抽出段内子 map seg_fmt_map(物理行 pr→段内 pr-s,修复 formatting_map 坐标错位)传给 extract_table(header_row=header_row);非表格段归 FREE_TEXT(防二次 MIXED 递归),走 extract_text_blocks + build_free_text_table 构造 kind="free_text"。MixedParagraph 段落追加到 MixedSheet 并进 result.mixed,表/自由文本表进 result.tables。原 else 分支保留为 TABLE 专用,既有 TABLE/FREE_TEXT 路径不变。TDD:先追加 2 测试(段落分割/碎片捕获、表格段不在物理行 0 时的取消线回填)确认 RED(result.mixed 空 + IndexError),再实现确认 GREEN(9 passed),全量 pytest 51 passed(49 baseline + 2)。提交见 git log | src/genesis/parsers/excel_parser.py, tests/test_excel_parser.py | deepseek-v4-flash-free | +| 2026-08-09 09:40 | 测试验证 | 里程碑2.5 Task12 实现:混合型样本 + 端到端验证。生成 samples/要件定義_混合型.xlsx(機能一覧 表格:表头+3 数据行,空行后接「・改修ポイント」「■対象期間」两条连续碎片文本,无空行);tests/test_real_samples.py 追加 test_mixed_sample_segments_detected(断言 mixed 段落数==2、kinds==["table","free_text"]、表格段第一批数据行機能ID=="F101"、table.rows==3、碎片段 text 含 改修ポイント/対象期間)。验证:real_samples 全绿 4 passed(3 baseline+1 新增,无 skip);pytest 全量 52 passed(51 baseline + 1);实际样本 split_paragraphs 产出两段(0,3)+(5,6)(表格段+碎片段)。提交见 git log | samples/要件定義_混合型.xlsx, tests/test_real_samples.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free | diff --git a/samples/要件定義_混合型.xlsx b/samples/要件定義_混合型.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b14737430813e28f8e22d630a4881a10d24712cb GIT binary patch literal 5178 zcmZ`-1yqv_+a93OB_Q41Atll=Wx_~lBpfw51i@gEN_WZVFz6cH5<~ih36riVEg+5G z)bsxDEC2Ux=Xsv9b9UW(?rYZ_&!u}0?+P^l03ZO|Whb-*_G;fv#eN#Z9u(LE;{H(A z%iY6U)W*X@eE=!6**-G;Z zMDh86kDJ{yo)xJojeesM?!~Z~469UhZ&yrz3gUf4xnQeejfJFzB>8s!I(b`D1#u6{ zrjm3kA(HI*dFUDLpX{VtWO5Q-0{}940RSp2JC9vNeH`sv?S7xdfAX_wHfMmArFgzp zK7z5|+GMyLqoyj(oXNUUHX;$XuFWbFWBa}@;f_A+>`F2zRiyZ0Tmm34W7tW3q+53}5ii8(j1#EeSu64I*s*)L zR$N60662pH-sEZQaNJ<_4kr^S zjYjXqY8!L(DY&40B(lnmKYJDj%9aF-*EKuAVuAS`eWhliUF;DA1A8T-xf~o_n^V!naeDGXiAb>Q)!@Absm3H55WUJpsyjgs`jHCb?Z!nrgusw8zLfPLsH>b8+B zkyR)YahdQsiR8*%(VG(CFRwfXd@W1r^& zn^Z(83@MD|9ltI zbRi;fpM9_Efnx={a(lD$D^cBgc*9JW4SN;(5peaB60wpn=|h907?={w8FA&WO*)2% zt&22=hlbUd(iD`C+`@-T5;Z(P_BDEf?#+wa;6~f4zXk5_Hq>xE0|5OXlUL1)q}8DT z;{>;QQiD(Lvk;0L|3&JO)uf03?c4p`g(yyPxKYVd#;63Pf zEE6i2QI!eE{t}*~^p;gZF8Mx9`T0)q*YM&5X;dhb_A}v8?Dtl7p(E~ve&_WkuFuL# zw;iC*ZVQgdNXs`h(4hM!ZtQxZ(zd20hxi|&0{Z(Js!j{8M-BBPRrZeB%CDJkJ5)p3 zL1i>qe_6*a@TJ6&bMWsg8StW*Pfi*%4Y!mDZyrF5Hsm>BmnK7MNR;ND$Va6Uk74d1Qg)auM;MjP!6d zB;}55yBostjhX;1(4I$IP_q@ZmMz+QANS_BxaFXRkV9T+d^=UeDM>jcdR%U!93hj{wX&bQ8e7mr{g-pPE?!w0H1g(W*xwA-@G-$gH= zCquGW&OjK1dd>C6#ctD+74A-cw_JIWS%N@Bi%*TD8lzAl?>7BD3j5ER)MlB7qPzp| z(|j=14of`FRBx0XxR)wW6e;-V;DL~2JgR?#OnXZ8L5EO#k-RM3;F~cVw{kE9rVU

&={Nue}F7Vru2RCq-=JqyzPg$-}ThEr= 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 literal 0 HcmV?d00001 diff --git a/tests/test_real_samples.py b/tests/test_real_samples.py index ad0d305..7abd076 100644 --- a/tests/test_real_samples.py +++ b/tests/test_real_samples.py @@ -36,4 +36,25 @@ def test_free_text_sample_detected(): if not p.exists(): pytest.skip("样本缺失") result = ExcelParser().parse(p) - assert any(t.extraction_method == "llm_from_free_text" for t in result.tables) \ No newline at end of file + assert any(t.extraction_method == "llm_from_free_text" for t in result.tables) + + +def test_mixed_sample_segments_detected(): + p = _x("要件定義_混合型.xlsx") + if not p.exists(): + pytest.skip("样本缺失") + result = ExcelParser().parse(p) + by_name = {t.name: t for t in result.tables} + assert "機能一覧" in by_name + assert result.mixed, "混合样本应产出段落" + mixed = result.mixed[0] + assert len(mixed.paragraphs) == 2 # 表格段 + 碎片段(・/■ 连续) + assert [p.kind for p in mixed.paragraphs] == ["table", "free_text"] + # 表格段无碎片污染 + table = [p.table for p in mixed.paragraphs if p.kind == "table"][0] + assert table.rows[0]["機能ID"].value == "F101" + assert len(table.rows) == 3 + # 碎片段含 ・ 与 ■ 两行文本 + ft = [p for p in mixed.paragraphs if p.kind == "free_text"][0] + assert "改修ポイント" in (ft.text or "") + assert "対象期間" in (ft.text or "") \ No newline at end of file