日期:2026-06-04 状态:修订版
把题材体系收敛到 CSV 已采用的 15 个 canonical_genre,同时保留 37 个中文题材模板作为初始化阶段的可叠加 preset。
一句话原则:
CSV canonical 是主干;taxonomy index 是用户输入层真源;模板是 preset;平台细分、套路、形式全部标签化。
webnovel-writer/templates/genres/*.md 当前实际数量是 37 个。templates/genres/*.md 只在初始化链路中直接读取:
skills/webnovel-init/SKILL.md 提示按用户题材读取模板。scripts/init_project.py 通过 templates/genres/{key}.md 拼入 设定集/世界观.md。scripts/init_project.py::_normalize_genre_key():用户输入 -> 模板文件名。scripts/reference_search.py::resolve_genre():用户输入/平台标签/legacy -> 15 canonical。scripts/data_modules/genre_aliases.py:用户输入 -> profile key/legacy profile section。scripts/reference_search.py 里 PLATFORM_TO_CANONICAL 和 _LEGACY_GENRE_MAP 已经覆盖约 40 个输入标签,和计划中的 index 高度重叠,不能继续作为第二真源。state.json 当前 schema 是 project_info.genre,不是顶层 project.genre。project.genre,包括 skills/webnovel-plan/SKILL.md、skills/webnovel-write/SKILL.md、memory_contract_adapter.py 和部分 context/profile 兼容路径。story_system_engine.py::_route() 不只是调用 resolve_genre(),还包含 route table 关键字/别名匹配、explicit genre fallback、inferred genre fallback。references/csv/题材与调性推理.csv 当前实际 route rows 是 26 条;验证应覆盖真实 CSV 全量 rows,不写死 26 或 27。references/genre-profiles.md 已定位为 fallback,高频题材主链已迁入 Story Contracts。唯一硬枚举继续使用 15 个 canonical:
都市 玄幻 仙侠 奇幻 科幻
历史 悬疑 游戏 古言 现言
幻言 年代 种田 快穿 衍生
这些值用于:
适用题材裁决规则.csv 的 题材canonical_genrereference_search.py --genrestate.json.project_info.genre新增 webnovel-writer/templates/genres/index.csv,但它不是单纯模板清单,而是题材输入层的唯一数据真源。
建议字段:
label,canonical_genre,label_type,template_file,template_type,route_tags,trope_tags,format_tags,aliases,notes
都市,都市,canonical,,canonical,,,,,
都市脑洞,都市,platform,都市脑洞.md,route,都市脑洞,,,都市奇闻,
规则怪谈,悬疑,route,规则怪谈.md,route,规则怪谈,,,规则动物园|规则类,
系统流,玄幻,trope,系统流.md,trope,,系统流,,系统|系统文,
知乎短篇,现言,format,知乎短篇.md,format,,,知乎短篇,知乎体|盐选|小程序短篇,
网游,游戏,legacy,,,,,,,
规则:
templates/genres/*.md 必须在 index 中有且只有一行 template_file 指向它。label 与 aliases 都必须解析到同一 canonical_genre。canonical_genre 必须属于 15 canonical 或 全部。template_type 只描述模板用途,不参与硬枚举。新增一个共享 loader/resolver,例如 scripts/genre_taxonomy.py:
GenreResolution(
raw_label="知乎短篇风的规则怪谈",
canonical_genre="悬疑",
matched_labels=["规则怪谈", "知乎短篇"],
template_files=["规则怪谈.md", "知乎短篇.md"],
route_tags=["规则怪谈"],
trope_tags=[],
format_tags=["知乎短篇"],
unresolved=[]
)
兼容原则:
reference_search.resolve_genre() 保留为 wrapper,只返回 canonical 或原值,用于现有调用点。_normalize_genre_key() 不再拥有 alias 字典;如果暂时保留,只能委托 taxonomy resolver 返回 template_file。data_modules/genre_aliases.py 不再维护输入 alias;只保留 profile key 映射,或委托 taxonomy 后再转 profile key。新 init 项目写入:
{
"project_info": {
"genre": "悬疑",
"genre_label": "知乎短篇风的规则怪谈",
"genre_tags": {
"route": ["规则怪谈"],
"trope": [],
"format": ["知乎短篇"],
"templates": ["规则怪谈", "知乎短篇"]
}
}
}
兼容读取顺序:
project_info.genreproject_info.genre_labelproject.genre写入新项目时不再新增顶层 project.genre。
templates/genres/*.md
templates/genres/index.csv
PLATFORM_TO_CANONICAL 和 _LEGACY_GENRE_MAP 的现有输入。scripts/genre_taxonomy.py
scripts/reference_search.py
PLATFORM_TO_CANONICAL 与 _LEGACY_GENRE_MAP。resolve_genre() 改为调用 taxonomy wrapper。scripts/init_project.py
project_info.genre 写 canonical。template_file 加载 preset,不再按原始输入精确拼路径。scripts/data_modules/genre_aliases.py
scripts/data_modules/memory_contract_adapter.py
project_info.genre/genre_label 读取,legacy project.genre 只兜底。scripts/data_modules/context_manager.py
skills/webnovel-init/SKILL.md
skills/webnovel-plan/SKILL.md、skills/webnovel-write/SKILL.md
project_info.genre 优先,legacy project.genre 兜底。scripts/validate_csv.py
reference_search resolver 兼容测试。init_project state/schema/template 加载测试。references/csv/genre-canonical.md
题材与调性推理.csv 的 题材/流派 是 route tag,不是 canonical enum。references/csv/README.md
references/index/reference-loading-map.md
references/genre-profiles.md
project.genre 文档表述修正为 project_info.genre,并标注 fallback 定位。templates/output/state-schema.md
project_info.genre_label 与 project_info.genre_tags 示例。templates/genres/ 立即拆成 canonical/ 和 presets/ 子目录。state.json,只提供兼容读取。genre-profiles.md 重新升级为主真源。范围:
templates/genres/index.csv。PLATFORM_TO_CANONICAL 和 _LEGACY_GENRE_MAP 的所有 key/value 迁入 index。templates/genres/*.md 数量与 index template_file 双向一致。template_file 存在且唯一。canonical_genre 属于 15 canonical。label/alias 唯一,不能映射到多个 canonical。不改运行逻辑。
验证:
(Get-ChildItem -Path webnovel-writer\templates\genres -Filter *.md | Measure-Object).Count
python -X utf8 webnovel-writer\scripts\validate_csv.py
范围:
GenreResolution。reference_search.resolve_genre()、init_project 模板解析、genre_aliases profile alias 写清楚委托关系。PLATFORM_TO_CANONICAL 原有用例全部通过 index resolver。_LEGACY_GENRE_MAP 原有用例全部通过 index resolver。_normalize_genre_key() 原 alias 用例全部能解析到相同模板文件。这一阶段的目标是拆掉“多真源”的设计风险,再进入调用点迁移。
范围:
reference_search.py 删除硬编码映射,改用 taxonomy。init_project.py 删除本地 alias 字典,按 GenreResolution.template_files 加载模板。story_system_engine.py 保持 _route() 的 keyword/alias/fallback 顺序,内部 canonical resolve 改用同一 wrapper。genre_aliases.py 输入 alias 迁移到 taxonomy,profile key 只处理 profile section/key 兼容。验证:
都市日常 -> 都市宫斗宅斗 -> 古言玄幻言情 -> 幻言规则怪谈 -> 悬疑网游 -> 游戏玄幻 -> canonical 玄幻,同时 init 模板可选中修仙.md克系 -> canonical 悬疑或按 index 配置,同时 init 模板选中克苏鲁.md范围:
init_project.py 写入 project_info.genre、project_info.genre_label、project_info.genre_tags。skills/webnovel-plan/SKILL.md 与 skills/webnovel-write/SKILL.md 的 genre 读取改为:
project_info.genre 优先。project.genre 兜底。memory_contract_adapter.py 与 context_manager.py 的 fallback 读取同样改为 project_info 优先。templates/output/state-schema.md。兼容策略:
project.genre 时继续可读。project.genre。验证:
webnovel-plan / webnovel-write 中 shell snippet 的读取逻辑测试或文档 grep 校验。范围:
webnovel-writer/references/csv/题材与调性推理.csv。关键词 / 意图与同义词 / 题材别名 / 题材/流派 作为 query,调用 StorySystemEngine(...).build(...)。route.canonical_genre 属于 15 canonical。route.genre_filter == route.canonical_genre,除非 canonical 是空或 全部。route_source 是预期集合之一:keyword_or_alias_match、explicit_genre_fallback、inferred_genre_fallback。验证:
$env:PYTHONUTF8='1'; python -m pytest webnovel-writer\scripts\data_modules\tests\test_story_system_engine.py -q --no-cov
$env:PYTHONUTF8='1'; python -m pytest webnovel-writer\scripts\data_modules\tests\test_story_system_cli.py -q --no-cov
范围:
webnovel-init/SKILL.md
references/csv/genre-canonical.md
references/csv/README.md
references/index/reference-loading-map.md
references/genre-profiles.md
只有前五阶段稳定后再做。
目标结构:
templates/genres/
index.csv
canonical/
都市.md
玄幻.md
presets/
都市异能.md
规则怪谈.md
知乎短篇.md
这一步路径影响大,必须单独提交。
genre-profiles.md 只在以下场景使用:
.story-system 取得 route/profile。story_contracts.master.route.primary_genre 为空,且 protagonist/state fallback 有 genre。优先级:
project_info.genre_label 或 project_info.genre 经 taxonomy resolve 后的结果。project.genre。genre_profile_excerpt 只能作为补充 context,不能覆盖 Story System contract 的 route 决策。
docs(genres): refine taxonomy convergence planchore(genres): add taxonomy index and normalize headingsfeat(genres): add taxonomy resolverrefactor(genres): migrate genre resolution call sitesfeat(init): persist canonical genre and genre tagsdocs(genres): update skill and csv taxonomy guidancerefactor(genres): split canonical and preset templates风险:CSV index 变成又一份真源。 控制:Phase 2 必须删除 Python 硬编码映射,所有 resolver 委托同一 loader。
风险:玄幻 -> 修仙.md 这类“canonical 与模板 preset 不同名”的历史行为丢失。
控制:在 index 中显式建模为 canonical_genre=玄幻、template_file=修仙.md,并加回归测试。
风险:系统流、知乎短篇 等默认 canonical 有争议。
控制:index 中标注 label_type/template_type,init 可在交互层展示 canonical 推断结果。
风险:Story System route 被 resolver 行为变化破坏。
控制:Phase 4 使用真实 题材与调性推理.csv 全量 route rows 做端到端测试。
风险:schema 读取点漏改,继续读 project.genre。
控制:Phase 3 增加 grep 校验和兼容读取测试,project_info 优先,legacy project 只兜底。
PLATFORM_TO_CANONICAL 与 _LEGACY_GENRE_MAP 不再以硬编码 dict 存在。_normalize_genre_key() 不再维护本地 alias。reference_search.py、init_project.py、genre_aliases.py 使用同一 taxonomy resolver。project_info.genre,并保存 genre_label 与 genre_tags。project.genre 仍可兼容读取,但不是新写入 schema。validate_csv.py、prompt integrity 与全量 pytest 通过。