- template_mapper: 按 design §6.5 仅 Heading level<=1 起章,H2/H3 归入
父章 sub_headings(此前每个 heading 独立成章,无 {{section:id}} 锚点的
6 个子章生成后被静默丢弃)
- models: ChapterSpec.sub_headings 字段;to_vars 暴露 sub_headings 变量
- writer_agent: prompt 新增【小节约束】(有子节时按小节顺序以 level=2
heading 组织,不得遗漏或新增)
- docx_injector: 注入后删除同章内与已生成标题同名且完全无内容的模板裸
H2/H3(保守策略:带内容的模板子节保留)
- 真实试运行验证:7 章 / 无静默丢弃告警 / 14 个 H2 无重复
245 lines
9.4 KiB
Python
245 lines
9.4 KiB
Python
from genesis.data_models import (
|
||
CellValue,
|
||
ExcelTable,
|
||
ParsedTemplate,
|
||
ChapterMarker,
|
||
Provenance,
|
||
SheetType,
|
||
StructuredSource,
|
||
)
|
||
from genesis.writer.models import ContentBlock, ChapterContent, GenerationContext, ChapterSpec
|
||
|
||
|
||
def _cell(v):
|
||
return CellValue(value=v, provenance=Provenance("要件.xlsx", "s1", 1, "A", "列"))
|
||
|
||
|
||
def _table(name, sheet_type, headers, rows):
|
||
return ExcelTable(
|
||
name=name,
|
||
detected_type=sheet_type,
|
||
extraction_method="openpyxl",
|
||
headers=headers,
|
||
rows=[{h: _cell(v) for h, v in zip(headers, row)} for row in rows],
|
||
)
|
||
|
||
|
||
def _mk_source():
|
||
"""含 FUNCTION / DATABASE / GENERIC 三张表的 StructuredSource(模板仅占位)。"""
|
||
template = ParsedTemplate(
|
||
file_name="t.docx",
|
||
sections=[ChapterMarker(type="heading", name="x", level=1)],
|
||
placeholders={},
|
||
styles={"used": []},
|
||
)
|
||
return StructuredSource(
|
||
tables=[
|
||
_table("機能一覧", SheetType.FUNCTION, ["機能ID", "機能名"], [["F001", "止损风控"], ["F002", "订单登录"]]),
|
||
_table("DB定義", SheetType.DATABASE, ["テーブルID", "テーブル名"], [["T001", "trade_order"]]),
|
||
_table("自由記述", SheetType.GENERIC, ["text"], [["系统采用前后端分离架构"]]),
|
||
],
|
||
template=template,
|
||
rule_docs=[],
|
||
image_analyses=[],
|
||
existing_system=None,
|
||
comments=[],
|
||
)
|
||
|
||
|
||
_UNSET = object()
|
||
|
||
|
||
def _ctx(cid, title, source=_UNSET, report=None):
|
||
return GenerationContext(
|
||
chapter_id=cid, title=title,
|
||
template_marker=ChapterSpec(chapter_id=cid, title=title, section_placeholder=f"section:{cid}"),
|
||
structured_source=None if source is None else (
|
||
_mk_source() if source is _UNSET else source
|
||
),
|
||
write_rules=[], design_rules=[], template_styles=set(),
|
||
impact_report=report,
|
||
)
|
||
|
||
|
||
def test_content_block_defaults():
|
||
b = ContentBlock(block_id="b1", type="paragraph", text="你好")
|
||
assert b.level is None
|
||
assert b.source_uris == []
|
||
|
||
|
||
def test_chapter_content_holds_blocks():
|
||
b = ContentBlock(block_id="b1", type="heading", level=2, text="标题")
|
||
c = ChapterContent(chapter_id="db_design", version=1, title="DB 设计", blocks=[b])
|
||
assert c.blocks[0].type == "heading"
|
||
assert c.version == 1
|
||
|
||
|
||
def test_generation_context_impact_field_default_none():
|
||
ctx = GenerationContext(
|
||
chapter_id="db_design", title="DB 设计",
|
||
template_marker=ChapterSpec(chapter_id="db_design", title="DB 设计", section_placeholder="{{section:db_design}}"),
|
||
structured_source=None, write_rules=["规则1"], design_rules=["规则2"],
|
||
template_styles={"Heading 1"},
|
||
)
|
||
assert ctx.impact_report is None
|
||
# to_vars 始终提供 impact 变量;无影响调查书时为空串
|
||
assert ctx.to_vars()["impact"] == ""
|
||
|
||
|
||
def test_generation_context_impact_var_formats_report():
|
||
from genesis.data_models import ChangeAnalysis, ChangeElement, ChangeType, ImpactReport
|
||
ca = ChangeAnalysis(
|
||
project_type="enhancement",
|
||
new_elements=[ChangeElement("F001", "機能", "止损风控", ChangeType.NEW)],
|
||
modified_elements=[],
|
||
deleted_elements=[],
|
||
unchanged_elements=[],
|
||
warnings=[],
|
||
)
|
||
report = ImpactReport(metadata={"version": "v1"}, change_analysis=ca, summary={"new": 1})
|
||
# function_list 章节对应 ElementType=機能 → 该要素应保留
|
||
ctx = _ctx("function_list", "機能一覧", source=None, report=report)
|
||
impact = ctx.to_vars()["impact"]
|
||
assert "止损风控" in impact
|
||
assert "F001" in impact
|
||
|
||
|
||
def _impact_ctx(report):
|
||
# function_list 章节对应 ElementType=機能 → [削除] 機能要素可保留
|
||
return _ctx("function_list", "機能一覧", source=None, report=report)
|
||
|
||
|
||
def test_impact_var_empty_when_report_without_analysis():
|
||
from genesis.data_models import ImpactReport
|
||
report = ImpactReport(metadata={"version": "v1"}, change_analysis=None, summary={})
|
||
assert _impact_ctx(report).to_vars()["impact"] == ""
|
||
|
||
|
||
def test_impact_var_formats_deleted_and_warnings():
|
||
from genesis.data_models import ChangeAnalysis, ChangeElement, ChangeType, ImpactReport, ImpactWarning
|
||
ca = ChangeAnalysis(
|
||
project_type="enhancement",
|
||
new_elements=[],
|
||
modified_elements=[],
|
||
deleted_elements=[ChangeElement("F030", "機能", "旧功能", ChangeType.DELETED)],
|
||
unchanged_elements=[],
|
||
warnings=[ImpactWarning("F030", "缺少既存対応")],
|
||
)
|
||
report = ImpactReport(metadata={}, change_analysis=ca, summary={"deleted": 1, "warnings": 1})
|
||
impact = _impact_ctx(report).to_vars()["impact"]
|
||
assert "[削除]" in impact and "旧功能" in impact
|
||
assert "[警告]" in impact and "缺少既存対応" in impact
|
||
|
||
|
||
def test_chapter_spec_placeholder_optional():
|
||
s = ChapterSpec(chapter_id="x", title="X", section_placeholder=None)
|
||
assert s.section_placeholder is None
|
||
|
||
|
||
# ---------- 章节级数据注入(design.md §6.8 ①:selector=该章数据) ----------
|
||
|
||
def test_to_vars_data_scoped_by_chapter():
|
||
"""db_design 章的 data 只含 DATABASE 表 + GENERIC 自由記述,不含 FUNCTION 表。"""
|
||
data = _ctx("db_design", "DB設計").to_vars()["data"]
|
||
assert "テーブルID" in data and "trade_order" in data # DB 表内容
|
||
assert "前后端分离" in data # GENERIC 通用背景
|
||
assert "止损风控" not in data and "機能ID" not in data # FUNCTION 表被排除
|
||
|
||
|
||
def test_to_vars_data_function_chapter_gets_function_table():
|
||
data = _ctx("function_list", "機能一覧").to_vars()["data"]
|
||
assert "F001" in data and "止损风控" in data
|
||
assert "trade_order" not in data # DB 表被排除
|
||
|
||
|
||
def test_to_vars_data_introduction_gets_all_tables():
|
||
"""introduction 概览章注入全部类型表。"""
|
||
data = _ctx("introduction", "はじめに").to_vars()["data"]
|
||
assert "F001" in data and "trade_order" in data
|
||
|
||
|
||
def test_to_vars_data_empty_when_no_matching_table():
|
||
"""无对应类型表(如 report_list 无 REPORT 表)→ 仅 GENERIC 背景,不报错。"""
|
||
data = _ctx("report_list", "帳票一覧").to_vars()["data"]
|
||
assert "帳票ID" not in data
|
||
assert "前后端分离" in data # GENERIC 始终保留
|
||
|
||
|
||
def test_to_vars_data_none_safe_and_source_key_removed():
|
||
"""structured_source=None → data 为空串;旧的全量 source 键不再提供。"""
|
||
vars_ = _ctx("db_design", "DB設計", source=None).to_vars()
|
||
assert vars_["data"] == ""
|
||
assert "source" not in vars_
|
||
|
||
|
||
def test_to_vars_data_renders_markdown_table_shape():
|
||
"""表渲染为可读 Markdown 形态(表名行 + 表头行 + 分隔行)。"""
|
||
data = _ctx("db_design", "DB設計").to_vars()["data"]
|
||
assert "DB定義" in data
|
||
assert "| テーブルID | テーブル名 |" in data
|
||
assert "---" in data
|
||
assert "| T001 | trade_order |" in data
|
||
|
||
|
||
def test_impact_filtered_by_chapter_element_type():
|
||
"""db_design 章 impact 只显示 DB 要素;機能要素被过滤。"""
|
||
from genesis.data_models import ChangeAnalysis, ChangeElement, ChangeType, ImpactReport
|
||
ca = ChangeAnalysis(
|
||
project_type="enhancement",
|
||
new_elements=[
|
||
ChangeElement("T001", "DB", "订单表", ChangeType.NEW),
|
||
ChangeElement("F001", "機能", "止损风控", ChangeType.NEW),
|
||
],
|
||
modified_elements=[],
|
||
deleted_elements=[],
|
||
unchanged_elements=[],
|
||
warnings=[],
|
||
)
|
||
report = ImpactReport(metadata={}, change_analysis=ca, summary={"new": 2})
|
||
impact = _ctx("db_design", "DB設計", source=None, report=report).to_vars()["impact"]
|
||
assert "T001" in impact and "订单表" in impact
|
||
assert "F001" not in impact and "止损风控" not in impact
|
||
|
||
|
||
def test_impact_introduction_shows_all_elements():
|
||
"""introduction 章(None 过滤)展示全部要素。"""
|
||
from genesis.data_models import ChangeAnalysis, ChangeElement, ChangeType, ImpactReport
|
||
ca = ChangeAnalysis(
|
||
project_type="enhancement",
|
||
new_elements=[ChangeElement("T001", "DB", "订单表", ChangeType.NEW),
|
||
ChangeElement("F001", "機能", "止损风控", ChangeType.NEW)],
|
||
modified_elements=[], deleted_elements=[], unchanged_elements=[], warnings=[],
|
||
)
|
||
report = ImpactReport(metadata={}, change_analysis=ca, summary={"new": 2})
|
||
impact = _ctx("introduction", "はじめに", source=None, report=report).to_vars()["impact"]
|
||
assert "T001" in impact and "F001" in impact
|
||
|
||
|
||
# ---------- 子节结构注入(design.md §6.5:H2/H3 归入本章) ----------
|
||
|
||
def test_chapter_spec_sub_headings_default_empty():
|
||
s = ChapterSpec(chapter_id="x", title="X")
|
||
assert s.sub_headings == []
|
||
|
||
|
||
def test_to_vars_exposes_sub_headings():
|
||
spec = ChapterSpec(
|
||
chapter_id="function_list", title="2. 機能一覧",
|
||
section_placeholder="section:function_list",
|
||
sub_headings=["2.1 機能一覧表", "2.2 機能詳細"],
|
||
)
|
||
ctx = GenerationContext(
|
||
chapter_id=spec.chapter_id, title=spec.title,
|
||
template_marker=spec,
|
||
structured_source=None, write_rules=[], design_rules=[],
|
||
template_styles=set(),
|
||
)
|
||
v = ctx.to_vars()
|
||
assert "2.1 機能一覧表" in v["sub_headings"]
|
||
assert "2.2 機能詳細" in v["sub_headings"]
|
||
|
||
|
||
def test_to_vars_sub_headings_empty_when_none():
|
||
vars_ = _ctx("db_design", "DB設計").to_vars()
|
||
assert vars_["sub_headings"] == ""
|