migrate-v6-knowledge.mjs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. #!/usr/bin/env node
  2. import { promises as fs } from 'node:fs'
  3. import path from 'node:path'
  4. import { fileURLToPath } from 'node:url'
  5. /**
  6. * 一次性知识层平移(M4 P4)。真源选定(用户指令:题材以 CSV 为准,不维护双表):
  7. * 题材模板 → genre-index.csv + 题材与调性推理.csv(CSV 权威;弃 markdown genre-profiles/genre-tropes)
  8. * 爽点节奏 → 爽点与节奏.csv
  9. * 追读力 → reading-power-taxonomy.md
  10. * 逐源清 v6 遗毒:CSV 删 适用技能/推荐检索表 列;模板剥「创意约束(Pack)」段;reading-power 清 v6 skill 头。
  11. * 输出 v7/references/,并生成迁移报告。
  12. */
  13. const v7 = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
  14. const repoRoot = path.join(v7, '..')
  15. const v6ref = path.join(repoRoot, 'webnovel-writer', 'references')
  16. const v6genres = path.join(repoRoot, 'webnovel-writer', 'templates', 'genres')
  17. const out = path.join(v7, 'references')
  18. const log = []
  19. async function write(rel, content) {
  20. const full = path.join(out, rel)
  21. await fs.mkdir(path.dirname(full), { recursive: true })
  22. await fs.writeFile(full, content, 'utf8')
  23. }
  24. const stripBOM = (s) => s.replace(/^/, '')
  25. function dropCsvColumns(csv, dropNames) {
  26. const lines = stripBOM(csv).split(/\r?\n/).filter((l) => l.length > 0)
  27. const header = lines[0].split(',')
  28. const keep = header.map((h, i) => ({ h: h.trim(), i })).filter((x) => !dropNames.includes(x.h)).map((x) => x.i)
  29. return lines.map((l) => { const c = l.split(','); return keep.map((i) => c[i] ?? '').join(',') }).join('\n') + '\n'
  30. }
  31. function stripConstraintSection(md) {
  32. const lines = md.split(/\r?\n/)
  33. const res = []
  34. let i = 0
  35. while (i < lines.length) {
  36. if (/^##\s*创意约束/.test(lines[i])) {
  37. while (res.length && res[res.length - 1].trim() === '') res.pop()
  38. i++
  39. while (i < lines.length && !/^##\s/.test(lines[i]) && !/^---\s*$/.test(lines[i])) i++
  40. res.push('')
  41. continue
  42. }
  43. res.push(lines[i])
  44. i++
  45. }
  46. return res.join('\n')
  47. }
  48. function cleanReadingPower(md) {
  49. const kept = md.split(/\r?\n/).filter((l) => !/主服务 skill|次服务 skill|内容层级/.test(l))
  50. return kept.join('\n').replace(/Step 1\.5 \/ Context Agent \/ Checkers/g, '细纲与两审')
  51. }
  52. async function main() {
  53. // 1. 题材索引(CSV 权威,清洁)
  54. await write('题材模板/genre-index.csv', stripBOM(await fs.readFile(path.join(v6ref, 'taxonomy', 'genre-index.csv'), 'utf8')))
  55. log.push('题材模板/genre-index.csv ← taxonomy/genre-index.csv(清 BOM)')
  56. // 2. 题材路由推理(删 v6 列)
  57. const tone = dropCsvColumns(await fs.readFile(path.join(v6ref, 'csv', '题材与调性推理.csv'), 'utf8'), ['适用技能', '推荐基础检索表', '推荐动态检索表'])
  58. await write('题材模板/题材与调性推理.csv', tone)
  59. log.push('题材模板/题材与调性推理.csv ← csv/题材与调性推理.csv(删列:适用技能/推荐基础检索表/推荐动态检索表)')
  60. // 3. 爽点与节奏(删 v6 列)
  61. const pacing = dropCsvColumns(await fs.readFile(path.join(v6ref, 'csv', '爽点与节奏.csv'), 'utf8'), ['适用技能'])
  62. await write('爽点节奏/爽点与节奏.csv', pacing)
  63. log.push('爽点节奏/爽点与节奏.csv ← csv/爽点与节奏.csv(删列:适用技能)')
  64. // 4. 追读力(清 v6 skill 头)
  65. await write('追读力/reading-power-taxonomy.md', cleanReadingPower(await fs.readFile(path.join(v6ref, 'reading-power-taxonomy.md'), 'utf8')))
  66. log.push('追读力/reading-power-taxonomy.md ← reading-power-taxonomy.md(删主/次服务 skill 与内容层级行;Step1.5/Context Agent/Checkers→细纲与两审)')
  67. // 5. 题材模板正文(剥创意约束段;系统流另修 v6 命令)
  68. const genreFiles = (await fs.readdir(v6genres)).filter((f) => f.endsWith('.md')).sort()
  69. let fixed = 0
  70. for (const f of genreFiles) {
  71. let md = stripConstraintSection(await fs.readFile(path.join(v6genres, f), 'utf8'))
  72. if (md.includes('/webnovel-write')) {
  73. md = md.replace(/在 `\/webnovel-write` 中,/g, '起草细纲时,').replace(/\/webnovel-write/g, '写章流程')
  74. fixed++
  75. }
  76. await write(path.join('题材模板', 'genres', f), md)
  77. }
  78. log.push(`题材模板/genres/ ← templates/genres/(${genreFiles.length} 个,全剥创意约束段,${fixed} 个另修 v6 命令引用)`)
  79. // 6. 迁移报告
  80. const report = [
  81. '# 知识层迁移报告(M4 P4)',
  82. '',
  83. '> 用户指令(2026-06-27):题材以 CSV 最新版为准,不要像 v6 维护两张表。',
  84. '',
  85. '## 真源选定',
  86. '',
  87. '| 知识体 | v7 唯一真源 | 弃用(双表/v6) |',
  88. '|---|---|---|',
  89. '| 题材模板 | `题材模板/genre-index.csv` + `题材与调性推理.csv` + `genres/*.md` | `genre-profiles.md`、`skills/.../genre-tropes.md`、`anti-trope-*.md`(markdown 双表,不迁) |',
  90. '| 爽点与节奏 | `爽点节奏/爽点与节奏.csv` | 老 markdown 重复部分 |',
  91. '| 追读力 | `追读力/reading-power-taxonomy.md` | (唯一源) |',
  92. '',
  93. '## 逐源清洗',
  94. '',
  95. ...log.map((l) => `- ${l}`),
  96. '',
  97. '## 保留',
  98. '- craft 内容(题材原型/流派/调性/节奏/毒点/钩子兑现)架构无关,原样保留。',
  99. '- CSV 保持 CSV 形态(机器友好、即单源),不复刻 markdown 表。',
  100. '',
  101. '## 备注',
  102. '- 「卡点」(付费/精准卡点)为短篇平台真实术语,与 spec 退场的「卡」(停滞义)不同,保留。',
  103. ].join('\n') + '\n'
  104. await write('迁移报告.md', report)
  105. console.log('知识层迁移完成:')
  106. for (const l of log) console.log(' - ' + l)
  107. }
  108. main().catch((e) => {
  109. console.error(e.message)
  110. process.exit(1)
  111. })