@cloud411716/fancy-webnovel 1.0.46 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/client.js +369 -0
  2. package/cordis.patch.yml +4 -13
  3. package/docs/douyin-plugin.md +118 -0
  4. package/docs/material-library/README.md +404 -0
  5. package/docs/material-library/import.js +437 -0
  6. package/docs/material-library/prompt.md +370 -0
  7. package/docs/material-library/queries.surql +94 -0
  8. package/docs/material-library/sample.jsonl +14 -0
  9. package/docs/material-library/schema.surql +46 -0
  10. package/docs/mcp-profile.example.yml +24 -0
  11. package/docs/story-graph-design.md +602 -0
  12. package/docs/story-graph-validation.md +81 -0
  13. package/docs/workspace-mcp.md +284 -0
  14. package/index.js +26 -38
  15. package/lib/atomic-write.js +109 -0
  16. package/lib/chapter-order.js +100 -0
  17. package/lib/chinese-num.js +111 -0
  18. package/lib/confirmation.js +80 -0
  19. package/lib/db-init.js +123 -0
  20. package/lib/deployed-state.js +141 -0
  21. package/lib/direction-labels.js +265 -0
  22. package/lib/error-codes.js +104 -0
  23. package/lib/infra.js +594 -0
  24. package/lib/llm-wiki.js +109 -0
  25. package/lib/material-db.js +149 -0
  26. package/lib/path-guard.js +150 -0
  27. package/lib/projects.js +400 -0
  28. package/lib/scraper-utils.js +305 -0
  29. package/lib/surreal-server.js +338 -0
  30. package/lib/topic-decision.js +860 -0
  31. package/lib/topic-fields.js +251 -0
  32. package/lib/topic-schema.js +225 -0
  33. package/lib/workspace.js +501 -0
  34. package/package.json +62 -5
  35. package/plugins/fancy-analyze/analyze-definitions.md +519 -0
  36. package/plugins/fancy-analyze/command-input.js +65 -0
  37. package/plugins/fancy-analyze/index.js +2024 -0
  38. package/plugins/fancy-analyze/ingest-coverage.js +107 -0
  39. package/plugins/fancy-analyze/schema-analyze.md +627 -0
  40. package/plugins/fancy-analyze/schema-write.md +178 -0
  41. package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
  42. package/plugins/fancy-analyze/scripts/split_source.js +292 -0
  43. package/plugins/fancy-analyze/summary-progress.js +495 -0
  44. package/plugins/fancy-analyze/templates.js +1179 -0
  45. package/plugins/fancy-bootstrap/index.js +313 -0
  46. package/plugins/fancy-bootstrap/scripts/init.js +177 -0
  47. package/plugins/fancy-bootstrap/scripts/path-utils.js +21 -0
  48. package/plugins/fancy-bootstrap/templates.js +71 -0
  49. package/plugins/fancy-db/index.js +172 -0
  50. package/plugins/fancy-douyin/deploy.js +134 -0
  51. package/plugins/fancy-douyin/index.js +551 -0
  52. package/plugins/fancy-douyin/prompts/01-analysis-prompt.md +84 -0
  53. package/plugins/fancy-douyin/scripts/crawl-note.cjs +194 -0
  54. package/plugins/fancy-douyin/scripts/lib.cjs +197 -0
  55. package/plugins/fancy-douyin/scripts/scan-account.cjs +128 -0
  56. package/plugins/fancy-douyin/scripts/start-chrome.cjs +203 -0
  57. package/plugins/fancy-douyin/state.js +197 -0
  58. package/plugins/fancy-douyin/templates.js +127 -0
  59. package/plugins/fancy-export/exporter.js +805 -0
  60. package/plugins/fancy-export/index.js +63 -0
  61. package/plugins/fancy-import/import-core.js +1118 -0
  62. package/plugins/fancy-import/import-driver.js +604 -0
  63. package/plugins/fancy-import/index.js +738 -0
  64. package/plugins/fancy-import/prompts/01-import-prompt.md +131 -0
  65. package/plugins/fancy-import/templates.js +58 -0
  66. package/plugins/fancy-material/exporter.js +153 -0
  67. package/plugins/fancy-material/genres.js +256 -0
  68. package/plugins/fancy-material/index.js +835 -0
  69. package/plugins/fancy-material/inventory.js +143 -0
  70. package/plugins/fancy-material/kinds.js +313 -0
  71. package/plugins/fancy-material/queue.js +55 -0
  72. package/plugins/fancy-material/scripts/query.js +254 -0
  73. package/plugins/fancy-material/templates.js +258 -0
  74. package/plugins/fancy-material/tool-deploy.js +138 -0
  75. package/plugins/fancy-material/validate.js +149 -0
  76. package/plugins/fancy-project/index.js +133 -0
  77. package/plugins/fancy-scan/existing-block.js +100 -0
  78. package/plugins/fancy-scan/index.js +708 -0
  79. package/plugins/fancy-scan/prompts/01-analysis-prompt.md +302 -0
  80. package/plugins/fancy-scan/scripts/append-block.js +725 -0
  81. package/plugins/fancy-scan/scripts/report-validate.js +824 -0
  82. package/plugins/fancy-scan/scripts/run-scan.js +667 -0
  83. package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +923 -0
  84. package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +469 -0
  85. package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +629 -0
  86. package/plugins/fancy-scan/templates/blocks.js +155 -0
  87. package/plugins/fancy-scan/templates/platforms.js +372 -0
  88. package/plugins/fancy-scan/templates/report-schema.js +90 -0
  89. package/plugins/fancy-scan/templates.js +694 -0
  90. package/plugins/fancy-scan/tool-deploy.js +335 -0
  91. package/plugins/fancy-scan/wait-turn.js +46 -0
  92. package/plugins/fancy-story-builder/db/README.md +106 -0
  93. package/plugins/fancy-story-builder/db/client.js +228 -0
  94. package/plugins/fancy-story-builder/db/commands.js +145 -0
  95. package/plugins/fancy-story-builder/db/ids.js +39 -0
  96. package/plugins/fancy-story-builder/db/import.js +798 -0
  97. package/plugins/fancy-story-builder/db/project.js +299 -0
  98. package/plugins/fancy-story-builder/db/query.js +361 -0
  99. package/plugins/fancy-story-builder/db/schema.js +417 -0
  100. package/plugins/fancy-story-builder/db/stage-sync.js +238 -0
  101. package/plugins/fancy-story-builder/deploy-manifest.js +239 -0
  102. package/plugins/fancy-story-builder/deslop-driver.js +821 -0
  103. package/plugins/fancy-story-builder/flesh-output.js +168 -0
  104. package/plugins/fancy-story-builder/index.js +6861 -0
  105. package/plugins/fancy-story-builder/patch-driver.js +390 -0
  106. package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +563 -0
  107. package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +602 -0
  108. package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +539 -0
  109. package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +225 -0
  110. package/plugins/fancy-story-builder/prompts/05-deslop-prompt.md +96 -0
  111. package/plugins/fancy-story-builder/purpose-template.md +83 -0
  112. package/plugins/fancy-story-builder/references/formulas-rhythm.json +302 -0
  113. package/plugins/fancy-story-builder/references/formulas-skeleton.json +402 -0
  114. package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
  115. package/plugins/fancy-story-builder/references/glossary.md +594 -0
  116. package/plugins/fancy-story-builder/references/index.md +581 -0
  117. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/00-index.md +174 -0
  118. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/01-scene.md +150 -0
  119. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/02-chapter-workflow.md +99 -0
  120. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/03-opening-chapter.md +104 -0
  121. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/04-dialogue-mastery.md +274 -0
  122. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/05-genre-formulas.md +432 -0
  123. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/06-tone-ensemble.md +259 -0
  124. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/07-ledger-audit.md +222 -0
  125. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/08-diagnosis-revision.md +205 -0
  126. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/09-reading-power.md +135 -0
  127. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/10-deslop.md +129 -0
  128. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +98 -0
  129. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/01-upgrade-rhythm.md +300 -0
  130. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/02-arc-rhythm.md +168 -0
  131. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/03-highpoint-hooks.md +189 -0
  132. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/04-satisfaction.md +226 -0
  133. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/05-foreshadow-density.md +195 -0
  134. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/06-chapter-outline.md +226 -0
  135. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +181 -0
  136. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/00-index.md +100 -0
  137. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +243 -0
  138. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/02-worldbuilding.md +245 -0
  139. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +185 -0
  140. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/04-emotion-methods.md +286 -0
  141. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +187 -0
  142. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +91 -0
  143. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +209 -0
  144. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/02-packaging.md +135 -0
  145. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +305 -0
  146. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/04-conflict.md +328 -0
  147. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/05-quota.md +187 -0
  148. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +160 -0
  149. package/plugins/fancy-story-builder/rhythm-output.js +140 -0
  150. package/plugins/fancy-story-builder/schema-setting.json +1100 -0
  151. package/plugins/fancy-story-builder/schema-setting.md +232 -0
  152. package/plugins/fancy-story-builder/scripts/ai-patterns.js +619 -0
  153. package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
  154. package/plugins/fancy-story-builder/scripts/check-guide-compliance.js +289 -0
  155. package/plugins/fancy-story-builder/scripts/content-quality.js +416 -0
  156. package/plugins/fancy-story-builder/scripts/deslop-validate.js +143 -0
  157. package/plugins/fancy-story-builder/scripts/error-classes.js +112 -0
  158. package/plugins/fancy-story-builder/scripts/flesh-validate.js +1312 -0
  159. package/plugins/fancy-story-builder/scripts/formula-engine.js +112 -0
  160. package/plugins/fancy-story-builder/scripts/graph-utils.js +1484 -0
  161. package/plugins/fancy-story-builder/scripts/label-coverage.js +171 -0
  162. package/plugins/fancy-story-builder/scripts/prompt-drift-check.js +441 -0
  163. package/plugins/fancy-story-builder/scripts/reading-power.js +298 -0
  164. package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
  165. package/plugins/fancy-story-builder/scripts/rhythm-validate.js +1312 -0
  166. package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
  167. package/plugins/fancy-story-builder/scripts/semantic-lint.js +381 -0
  168. package/plugins/fancy-story-builder/scripts/skeleton-quota.js +145 -0
  169. package/plugins/fancy-story-builder/scripts/skeleton-validate.js +1973 -0
  170. package/plugins/fancy-story-builder/scripts/stage-batches.js +1405 -0
  171. package/plugins/fancy-story-builder/scripts/stage-contract.js +429 -0
  172. package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
  173. package/plugins/fancy-story-builder/scripts/upstream-query.js +198 -0
  174. package/plugins/fancy-story-builder/scripts/v2/baseline.js +112 -0
  175. package/plugins/fancy-story-builder/scripts/v2/cast.js +517 -0
  176. package/plugins/fancy-story-builder/scripts/v2/context.js +434 -0
  177. package/plugins/fancy-story-builder/scripts/v2/contracts.js +493 -0
  178. package/plugins/fancy-story-builder/scripts/v2/core.js +419 -0
  179. package/plugins/fancy-story-builder/scripts/v2/engine.js +1068 -0
  180. package/plugins/fancy-story-builder/scripts/v2/export-markdown.js +794 -0
  181. package/plugins/fancy-story-builder/scripts/v2/genre.js +195 -0
  182. package/plugins/fancy-story-builder/scripts/v2/knowledge.js +321 -0
  183. package/plugins/fancy-story-builder/scripts/v2/macro.js +409 -0
  184. package/plugins/fancy-story-builder/scripts/v2/manifest.js +538 -0
  185. package/plugins/fancy-story-builder/scripts/v2/migrate.js +137 -0
  186. package/plugins/fancy-story-builder/scripts/v2/prompts.js +1335 -0
  187. package/plugins/fancy-story-builder/scripts/v2/rhythm.js +618 -0
  188. package/plugins/fancy-story-builder/scripts/v2/scale.js +843 -0
  189. package/plugins/fancy-story-builder/scripts/v2/snapshot.js +121 -0
  190. package/plugins/fancy-story-builder/scripts/v2/stages.js +273 -0
  191. package/plugins/fancy-story-builder/scripts/v2/writing.js +216 -0
  192. package/plugins/fancy-story-builder/scripts/writing-rules.js +167 -0
  193. package/plugins/fancy-story-builder/scripts/writing-validate.js +682 -0
  194. package/plugins/fancy-story-builder/skeleton-output.js +199 -0
  195. package/plugins/fancy-story-builder/stage-driver.js +1049 -0
  196. package/plugins/fancy-story-builder/storydb/README.md +271 -0
  197. package/plugins/fancy-story-builder/storydb/cli.js +458 -0
  198. package/plugins/fancy-story-builder/storydb/client.js +351 -0
  199. package/plugins/fancy-story-builder/storydb/compile.js +452 -0
  200. package/plugins/fancy-story-builder/storydb/context.js +288 -0
  201. package/plugins/fancy-story-builder/storydb/definitions.js +699 -0
  202. package/plugins/fancy-story-builder/storydb/driver.js +106 -0
  203. package/plugins/fancy-story-builder/storydb/dsh-tools.js +313 -0
  204. package/plugins/fancy-story-builder/storydb/export.js +175 -0
  205. package/plugins/fancy-story-builder/storydb/ids.js +31 -0
  206. package/plugins/fancy-story-builder/storydb/impact.js +170 -0
  207. package/plugins/fancy-story-builder/storydb/import.js +559 -0
  208. package/plugins/fancy-story-builder/storydb/mcp.js +99 -0
  209. package/plugins/fancy-story-builder/storydb/prompts.js +952 -0
  210. package/plugins/fancy-story-builder/storydb/stage-command.js +379 -0
  211. package/plugins/fancy-story-builder/storydb/stage.js +426 -0
  212. package/plugins/fancy-story-builder/storydb/store.js +891 -0
  213. package/plugins/fancy-story-builder/storydb/tools.js +592 -0
  214. package/plugins/fancy-story-builder/storydb/validation.js +183 -0
  215. package/plugins/fancy-story-builder/templates/v2/10-scale/scale-template.json +144 -0
  216. package/plugins/fancy-story-builder/templates/v2/20-macro/emotion-template.json +15 -0
  217. package/plugins/fancy-story-builder/templates/v2/20-macro/endings-template.json +16 -0
  218. package/plugins/fancy-story-builder/templates/v2/20-macro/lines-template.json +28 -0
  219. package/plugins/fancy-story-builder/templates/v2/20-macro/roles-template.json +10 -0
  220. package/plugins/fancy-story-builder/templates/v2/20-macro/story-template.json +19 -0
  221. package/plugins/fancy-story-builder/templates/v2/20-macro/volumes-template.json +24 -0
  222. package/plugins/fancy-story-builder/templates/v2/20-macro/world-template.json +24 -0
  223. package/plugins/fancy-story-builder/templates/v2/30-cast/characters-template.json +49 -0
  224. package/plugins/fancy-story-builder/templates/v2/30-cast/climaxes-template.json +14 -0
  225. package/plugins/fancy-story-builder/templates/v2/30-cast/conflicts-template.json +30 -0
  226. package/plugins/fancy-story-builder/templates/v2/30-cast/factions-template.json +18 -0
  227. package/plugins/fancy-story-builder/templates/v2/30-cast/foreshadows-template.json +29 -0
  228. package/plugins/fancy-story-builder/templates/v2/30-cast/information-template.json +29 -0
  229. package/plugins/fancy-story-builder/templates/v2/30-cast/items-template.json +13 -0
  230. package/plugins/fancy-story-builder/templates/v2/30-cast/locations-template.json +12 -0
  231. package/plugins/fancy-story-builder/templates/v2/30-cast/payoffs-template.json +16 -0
  232. package/plugins/fancy-story-builder/templates/v2/30-cast/relations-template.json +44 -0
  233. package/plugins/fancy-story-builder/templates/v2/40-rhythm/chapter-map-template.json +81 -0
  234. package/plugins/fancy-story-builder/templates/v2/40-rhythm/outline-template.json +162 -0
  235. package/plugins/fancy-story-builder/templates/v2/50-writing/ledger-template.json +46 -0
  236. package/plugins/fancy-story-builder/templates/v2/60-deslop/report-template.json +10 -0
  237. package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//346/227/266/345/272/217/346/230/240/345/260/204/346/250/241/346/235/277.json +427 -0
  238. package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/347/273/206/347/272/262/346/250/241/346/235/277.json +34 -0
  239. package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/350/247/204/345/210/222/346/250/241/346/235/277.json +24 -0
  240. package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//350/212/202/345/245/217/345/217/202/346/225/260/346/250/241/346/235/277.json +27 -0
  241. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//344/270/226/347/225/214/350/247/202/350/247/204/345/210/231/346/250/241/346/235/277.json +168 -0
  242. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/263/351/224/256/351/201/223/345/205/267/346/250/241/346/235/277.json +184 -0
  243. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/266/344/273/226/345/256/236/344/275/223/346/250/241/346/235/277.json +62 -0
  244. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/206/262/347/252/201/346/250/241/346/235/277.json +131 -0
  245. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/211/247/346/203/205/346/250/241/346/235/277.json +85 -0
  246. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/212/277/345/212/233/346/250/241/346/235/277.json +103 -0
  247. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/234/260/347/202/271/346/250/241/346/235/277.json +68 -0
  248. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//347/273/223/345/261/200/346/250/241/346/235/277.json +142 -0
  249. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//350/247/222/350/211/262/346/250/241/346/235/277.json +292 -0
  250. package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/205/263/347/263/273/346/250/241/346/235/277.json +266 -0
  251. package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/256/236/344/275/223/346/250/241/346/235/277.json +336 -0
  252. package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/261/202/347/272/247/346/250/241/346/235/277.json +65 -0
  253. package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//346/241/206/346/236/266/346/250/241/346/235/277.json +308 -0
  254. package/plugins/fancy-story-builder/templates.js +1446 -0
  255. package/plugins/fancy-story-builder/wiki-references.js +540 -0
  256. package/plugins/fancy-story-builder/writing-driver.js +1388 -0
  257. package/plugins/fancy-topic/index.js +1057 -0
  258. package/plugins/fancy-topic/prompts/01-exploration-prompt.md +115 -0
  259. package/plugins/fancy-topic/scripts/save-suggestion.js +219 -0
  260. package/plugins/fancy-topic/scripts/select-topic.js +317 -0
  261. package/plugins/fancy-topic/templates.js +476 -0
  262. package/plugins/fancy-topic/tool-deploy.js +200 -0
  263. package/commands/bootstrap/index.js +0 -164
  264. package/commands/bootstrap/init-project.js +0 -121
  265. package/commands/scan/index.js +0 -283
  266. package/commands/scan/platforms/fanqie.js +0 -146
  267. package/commands/scan/platforms/jinjiang.js +0 -143
  268. package/commands/scan/platforms/qidian.js +0 -102
  269. package/commands/scan/platforms/qimao.js +0 -127
  270. package/commands/scan/scraper.js +0 -195
  271. package/infra.js +0 -160
@@ -0,0 +1,538 @@
1
+ /**
2
+ * v2/manifest.js —— 项目级依赖图与实体索引。
3
+ *
4
+ * 设计原则:
5
+ * - 业务 JSON 只声明 _meta.upstream(我来自哪里);
6
+ * - 下游关系全部由 manifest 计算(谁引用了我);
7
+ * - patch 只按图遍历,不靠 LLM 猜依赖。
8
+ */
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import {
12
+ SCHEMA_VERSION,
13
+ V2_STAGES,
14
+ V2_STAGE_BY_ID,
15
+ STAGE_ORDER,
16
+ asArray,
17
+ bytesHash,
18
+ contentHash,
19
+ hashFile,
20
+ nowIso,
21
+ readJson,
22
+ readStageStatus,
23
+ relProjectPath,
24
+ stageDir,
25
+ stageHash,
26
+ writeJsonAtomic,
27
+ } from './core.js';
28
+
29
+ export const MANIFEST_PATH = ['设定', 'manifest.json'];
30
+
31
+ const STAGE_UPSTREAM = {
32
+ '00-baseline': [],
33
+ '10-scale': ['00-baseline'],
34
+ '20-macro': ['10-scale'],
35
+ '30-cast': ['10-scale', '20-macro'],
36
+ '40-rhythm': ['10-scale', '20-macro', '30-cast'],
37
+ '50-writing': ['40-rhythm'],
38
+ '60-deslop': ['50-writing'],
39
+ };
40
+
41
+ const ENTITY_TYPE_BY_KEY = {
42
+ characters: '人物',
43
+ factions: '势力',
44
+ locations: '地点',
45
+ items: '道具',
46
+ conflicts: '冲突',
47
+ foreshadows: '伏笔',
48
+ climaxes: '高潮',
49
+ payoffs: '爽点',
50
+ endings: '结局',
51
+ world_rules: '世界观',
52
+ main_line: '主线',
53
+ branches: '支线',
54
+ roles: '角色功能位',
55
+ character_relations: '关系',
56
+ faction_relations: '势力关系',
57
+ memberships: '势力成员',
58
+ information_gaps: '信息差',
59
+ chapters: '章',
60
+ chapters_map: '章',
61
+ };
62
+
63
+ const ENTITY_PREFIX = {
64
+ C: '人物',
65
+ F: '势力',
66
+ L: '地点',
67
+ P: '道具',
68
+ X: '冲突',
69
+ FS: '伏笔',
70
+ CL: '高潮',
71
+ PL: '爽点',
72
+ E: '结局',
73
+ R: '世界观',
74
+ M: '主线',
75
+ B: '支线',
76
+ REL: '关系',
77
+ FREL: '势力关系',
78
+ MEM: '势力成员',
79
+ IG: '信息差',
80
+ CH: '章',
81
+ ROLE: '角色功能位',
82
+ };
83
+
84
+ function stageFiles(projectRoot, stageId) {
85
+ const dir = stageDir(projectRoot, stageId);
86
+ if (!fs.existsSync(dir)) return [];
87
+ const out = [];
88
+ const walk = (current) => {
89
+ let names = [];
90
+ try {
91
+ names = fs.readdirSync(current, { withFileTypes: true });
92
+ } catch {
93
+ return;
94
+ }
95
+ for (const entry of names) {
96
+ const name = entry.name;
97
+ if (name.startsWith('.')) continue;
98
+ if (name === '_context' || name === 'bundle.json' || name === 'README.md') continue;
99
+ const full = path.join(current, name);
100
+ if (entry.isDirectory()) {
101
+ if (name === 'outlines' || name === 'chapters' || name === 'timelines') walk(full);
102
+ continue;
103
+ }
104
+ if (name.endsWith('.json') || name.endsWith('.md')) out.push(full);
105
+ }
106
+ };
107
+ walk(dir);
108
+ return out.sort();
109
+ }
110
+
111
+ function fileNodeId(projectRoot, stageId, file) {
112
+ const rel = path.relative(stageDir(projectRoot, stageId), file).split(path.sep).join('/');
113
+ if (stageId === '40-rhythm' && rel.startsWith('outlines/')) {
114
+ return '40-rhythm.outline.' + rel.replace(/^outlines\//, '').replace(/\.json$/, '');
115
+ }
116
+ if (stageId === '50-writing' && rel.startsWith('chapters/')) {
117
+ return '50-writing.chapter.' + rel.replace(/^chapters\//, '').replace(/\.md$/, '');
118
+ }
119
+ return stageId + '.' + rel.replace(/\.(json|md)$/, '').replace(/\//g, '.');
120
+ }
121
+
122
+ function expectedUpstream(stageId, rel) {
123
+ if (stageId === '00-baseline') return [];
124
+ if (stageId === '10-scale') return ['00-baseline'];
125
+ if (stageId === '20-macro') return ['10-scale'];
126
+ if (stageId === '30-cast') return ['10-scale', '20-macro'];
127
+ if (stageId === '40-rhythm') {
128
+ if (rel.startsWith('outlines/')) return ['40-rhythm.chapter_map', '10-scale', '20-macro', '30-cast'];
129
+ if (rel.startsWith('timelines/') || rel === 'index.json') return ['40-rhythm.chapter_map'];
130
+ return ['10-scale', '20-macro', '30-cast'];
131
+ }
132
+ if (stageId === '50-writing') return ['40-rhythm'];
133
+ if (stageId === '60-deslop') return ['50-writing'];
134
+ return [];
135
+ }
136
+
137
+ function metaUpstream(data) {
138
+ const raw = data && data._meta && data._meta.upstream;
139
+ if (!Array.isArray(raw)) return null;
140
+ return raw.map((x) => (typeof x === 'string' ? x : x && x.node_id)).filter(Boolean);
141
+ }
142
+
143
+ function entityTypeOf(key, id) {
144
+ if (key && ENTITY_TYPE_BY_KEY[key]) return ENTITY_TYPE_BY_KEY[key];
145
+ const prefix = String(id || '').match(/^(FREL|REL|MEM|ROLE|FS|CL|PL|IG|CH|[CFLPXEMRB])/);
146
+ if (prefix) return ENTITY_PREFIX[prefix[1]] || '';
147
+ return '';
148
+ }
149
+
150
+ function scanEntities(value, out, keyHint) {
151
+ if (Array.isArray(value)) {
152
+ for (const item of value) scanEntities(item, out, keyHint);
153
+ return;
154
+ }
155
+ if (!value || typeof value !== 'object') return;
156
+ const entityId = typeof value.id === 'string' && value.id
157
+ ? value.id
158
+ : (typeof value.chapter_id === 'string' && value.chapter_id ? value.chapter_id : null);
159
+ if (entityId) {
160
+ const type = entityTypeOf(keyHint, entityId);
161
+ if (type) {
162
+ out.push({
163
+ id: entityId,
164
+ type,
165
+ name: value.name || value.title || '',
166
+ key: keyHint || '',
167
+ display_no: value.display_no,
168
+ });
169
+ }
170
+ }
171
+ for (const [key, child] of Object.entries(value)) {
172
+ if (key === '_meta') continue;
173
+ scanEntities(child, out, key);
174
+ }
175
+ }
176
+
177
+ function tokensIn(text) {
178
+ const set = new Set();
179
+ const re = /\b(?:ROLE-[A-Z0-9-]+|IG-[A-Za-z0-9-]+|[A-Z]{1,3}-?\d{3,})\b/g;
180
+ let m;
181
+ while ((m = re.exec(text)) !== null) set.add(m[0]);
182
+ return set;
183
+ }
184
+
185
+ function stageNode(projectRoot, stageId) {
186
+ const spec = V2_STAGE_BY_ID[stageId];
187
+ const status = readStageStatus(projectRoot, stageId, { allowLegacy: false });
188
+ return {
189
+ kind: 'stage',
190
+ stage: stageId,
191
+ label: spec.label,
192
+ path: path.join('设定', spec.dir).split(path.sep).join('/'),
193
+ status: status.status,
194
+ hash: stageHash(projectRoot, stageId),
195
+ upstream: STAGE_UPSTREAM[stageId] || [],
196
+ files: [],
197
+ updated_at: nowIso(),
198
+ };
199
+ }
200
+
201
+ /**
202
+ * 构建 manifest。write=true 时写回 设定/manifest.json。
203
+ */
204
+ export function buildManifest(projectRoot, opts = {}) {
205
+ const nodes = {};
206
+ const edges = [];
207
+ const entities = {};
208
+ const allFiles = [];
209
+ const textCache = new Map();
210
+
211
+ for (const stage of V2_STAGES) {
212
+ if (!fs.existsSync(stageDir(projectRoot, stage.id))) continue;
213
+ nodes[stage.id] = stageNode(projectRoot, stage.id);
214
+ }
215
+
216
+ // 阶段边
217
+ for (const [to, ups] of Object.entries(STAGE_UPSTREAM)) {
218
+ if (!nodes[to]) continue;
219
+ for (const from of ups) {
220
+ if (!nodes[from]) continue;
221
+ edges.push({ from, to, kind: from === '10-scale' ? 'quantity' : (from === '40-rhythm' ? 'outline' : 'content') });
222
+ }
223
+ }
224
+
225
+ // 文件节点
226
+ const fileNodeByPath = new Map();
227
+ for (const stage of V2_STAGES) {
228
+ if (!nodes[stage.id]) continue;
229
+ for (const file of stageFiles(projectRoot, stage.id)) {
230
+ const rel = path.relative(stageDir(projectRoot, stage.id), file).split(path.sep).join('/');
231
+ const nodeId = fileNodeId(projectRoot, stage.id, file);
232
+ const data = file.endsWith('.json') ? readJson(file, null) : null;
233
+ const upstream = data ? (metaUpstream(data) || expectedUpstream(stage.id, rel)) : [];
234
+ const node = {
235
+ kind: 'file',
236
+ stage: stage.id,
237
+ path: relProjectPath(projectRoot, file),
238
+ status: (() => {
239
+ const s = readStageStatus(projectRoot, stage.id);
240
+ return s.status;
241
+ })(),
242
+ hash: file.endsWith('.json') ? hashFile(file) : bytesHash(fs.readFileSync(file)),
243
+ upstream,
244
+ downstream: [],
245
+ file: rel,
246
+ updated_at: nowIso(),
247
+ };
248
+ nodes[nodeId] = node;
249
+ nodes[stage.id].files.push(nodeId);
250
+ fileNodeByPath.set(file, nodeId);
251
+ allFiles.push({ file, nodeId, stage: stage.id, rel, data });
252
+ for (const up of upstream) {
253
+ edges.push({ from: up, to: nodeId, kind: 'file' });
254
+ }
255
+ }
256
+ }
257
+
258
+ // 文件节点的 upstream 若指向阶段节点,补阶段节点文件列表(manifest 自身可追)
259
+ // 反向边:所有节点
260
+ const downstream = new Map();
261
+ for (const [id, node] of Object.entries(nodes)) downstream.set(id, new Set());
262
+ for (const e of edges) {
263
+ if (!downstream.has(e.from)) downstream.set(e.from, new Set());
264
+ downstream.get(e.from).add(e.to);
265
+ }
266
+ for (const [id, set] of downstream.entries()) {
267
+ if (nodes[id]) nodes[id].downstream = [...set].sort();
268
+ }
269
+
270
+ // 实体索引
271
+ const entityRows = [];
272
+ for (const entry of allFiles) {
273
+ if (!entry.data) continue;
274
+ const rows = [];
275
+ scanEntities(entry.data, rows, '');
276
+ for (const row of rows) {
277
+ if (!entities[row.id]) {
278
+ entities[row.id] = {
279
+ type: row.type,
280
+ name: row.name || '',
281
+ display_no: row.display_no,
282
+ source: entry.nodeId,
283
+ source_path: nodes[entry.nodeId].path,
284
+ downstream: [],
285
+ };
286
+ entityRows.push(row.id);
287
+ }
288
+ }
289
+ }
290
+
291
+ // 文本引用索引:一次扫描每个文件,用 token 正则匹配实体 ID。
292
+ for (const entry of allFiles) {
293
+ let text = textCache.get(entry.file);
294
+ if (text === undefined) {
295
+ try {
296
+ text = fs.readFileSync(entry.file, 'utf-8');
297
+ } catch {
298
+ text = '';
299
+ }
300
+ textCache.set(entry.file, text);
301
+ }
302
+ const tokens = tokensIn(text);
303
+ for (const token of tokens) {
304
+ const row = entities[token];
305
+ if (!row) continue;
306
+ if (row.source === entry.nodeId) continue;
307
+ if (!row.downstream.includes(entry.nodeId)) row.downstream.push(entry.nodeId);
308
+ }
309
+ }
310
+ // 章实体额外关联已写正文:正文文件标题只有“第N章”,文本里没有 CH001 字样,
311
+ // 单靠 ID 扫描会漏掉它。
312
+ for (const [id, row] of Object.entries(entities)) {
313
+ if (row.type !== '章' || !row.display_no) continue;
314
+ const manuscriptNode = '50-writing.chapter.第' + String(row.display_no).padStart(3, '0') + '章';
315
+ if (nodes[manuscriptNode] && manuscriptNode !== row.source && !row.downstream.includes(manuscriptNode)) {
316
+ row.downstream.push(manuscriptNode);
317
+ }
318
+ }
319
+ for (const row of Object.values(entities)) row.downstream.sort();
320
+
321
+ const stageStatus = {};
322
+ for (const stage of V2_STAGES) {
323
+ if (nodes[stage.id]) stageStatus[stage.id] = nodes[stage.id].status;
324
+ }
325
+ const project = readJson(path.join(stageDir(projectRoot, '00-baseline'), 'input.json'), {}) || {};
326
+ const manifest = {
327
+ schema_version: SCHEMA_VERSION,
328
+ generated_at: nowIso(),
329
+ project: {
330
+ book_name: project.book_name || project.name || '',
331
+ target_chapters: Number(project.target_chapters || 0) || null,
332
+ target_words: Number(project.target_words || 0) || null,
333
+ },
334
+ stages: stageStatus,
335
+ nodes,
336
+ edges,
337
+ entities,
338
+ };
339
+
340
+ if (opts.write !== false) {
341
+ writeJsonAtomic(path.join(projectRoot, ...MANIFEST_PATH), manifest);
342
+ }
343
+ return manifest;
344
+ }
345
+
346
+ export function readManifest(projectRoot) {
347
+ return readJson(path.join(projectRoot, ...MANIFEST_PATH), null);
348
+ }
349
+
350
+ /** 规划/校验用:相对路径 → 文件节点。 */
351
+ export function fileNodeByProjectPath(manifest, rel) {
352
+ for (const [id, node] of Object.entries(manifest.nodes || {})) {
353
+ if (node.kind === 'file' && node.path === rel) return { id, ...node };
354
+ }
355
+ return null;
356
+ }
357
+
358
+ /** 反向依赖闭包:传入节点 ID,返回所有下游节点(含自身)。 */
359
+ export function reverseClosure(manifest, nodeIds) {
360
+ const nodes = manifest.nodes || {};
361
+ const result = new Set();
362
+ const queue = [...nodeIds];
363
+ while (queue.length) {
364
+ const id = queue.shift();
365
+ if (!id || result.has(id)) continue;
366
+ result.add(id);
367
+ const node = nodes[id];
368
+ if (!node) continue;
369
+ for (const next of asArray(node.downstream)) queue.push(next);
370
+ }
371
+ return result;
372
+ }
373
+
374
+ /** 找一条请求里点名到的实体(按 ID 或名称)。 */
375
+ export function findEntities(manifest, text) {
376
+ const request = String(text || '');
377
+ const found = [];
378
+ for (const [id, entity] of Object.entries(manifest.entities || {})) {
379
+ if (request.includes(id) || (entity.name && entity.name.length >= 2 && request.includes(entity.name))) {
380
+ found.push({ id, ...entity });
381
+ }
382
+ }
383
+ return found;
384
+ }
385
+
386
+ /**
387
+ * 生成 patch 影响计划。
388
+ * 优先级:请求中明确点名的实体 → 字段/阶段关键词 → 全下游。
389
+ */
390
+ export function buildPatchPlan(projectRoot, userRequest, manifest) {
391
+ const m = manifest || readManifest(projectRoot) || buildManifest(projectRoot, { write: false });
392
+ const requested = findEntities(m, userRequest);
393
+ const affectedNodes = new Set();
394
+ const reasons = [];
395
+
396
+ for (const entity of requested) {
397
+ affectedNodes.add(entity.source);
398
+ affectedNodes.add(entity.id);
399
+ for (const id of asArray(entity.downstream)) affectedNodes.add(id);
400
+ // 关系/成员关系不是章节地图里直接引用的 ID,但它们的语义会进入每章的关系呈现。
401
+ // 依赖图无法从文本反查“这条关系被哪些章节写到”,所以按保守规则把节奏与正文纳入。
402
+ if (['关系', '势力关系', '势力成员'].includes(entity.type)) {
403
+ affectedNodes.add('40-rhythm');
404
+ affectedNodes.add('50-writing');
405
+ }
406
+ reasons.push('实体 ' + entity.id + (entity.name ? '(' + entity.name + ')' : ''));
407
+ }
408
+
409
+ // 字段/阶段关键词兜底:不点名实体时,至少把相关阶段及其下游拉进来。
410
+ const stageHints = [];
411
+ if (/数量|配额|章节数|卷数|幕数|字数|规模|quota|scale/i.test(userRequest)) stageHints.push('10-scale');
412
+ if (/世界观|主题|主线|支线|结局|卷纲|幕纲|情绪|角色功能|macro/i.test(userRequest)) stageHints.push('20-macro');
413
+ if (/人物|角色|势力|地点|道具|冲突|伏笔|高潮|爽点|关系|信息差|cast/i.test(userRequest)) stageHints.push('30-cast');
414
+ if (/关系|成员|立场|态度/i.test(userRequest)) stageHints.push('40-rhythm', '50-writing');
415
+ if (/章节|细纲|节奏|钩子|时间线|rhythm/i.test(userRequest)) stageHints.push('40-rhythm');
416
+ if (/正文|章节内容|writing/i.test(userRequest)) stageHints.push('50-writing');
417
+ for (const stageId of stageHints) {
418
+ if (m.nodes && m.nodes[stageId]) {
419
+ affectedNodes.add(stageId);
420
+ for (const id of asArray(m.nodes[stageId].downstream)) affectedNodes.add(id);
421
+ reasons.push('阶段 ' + stageId);
422
+ }
423
+ }
424
+
425
+ // 没识别出任何范围:全部已生成阶段(保守处理,要求确认)。
426
+ if (!affectedNodes.size) {
427
+ for (const [id, node] of Object.entries(m.nodes || {})) {
428
+ if (node.kind === 'stage') affectedNodes.add(id);
429
+ }
430
+ reasons.push('未识别到具体实体,按全量下游处理');
431
+ }
432
+
433
+ const closure = reverseClosure(m, [...affectedNodes]);
434
+ const files = [];
435
+ const stages = new Set();
436
+ for (const id of closure) {
437
+ const node = m.nodes && m.nodes[id];
438
+ if (!node) continue;
439
+ if (node.kind === 'stage') {
440
+ stages.add(node.stage || id);
441
+ for (const fileId of asArray(node.files)) {
442
+ const f = m.nodes[fileId];
443
+ if (f && f.kind === 'file') files.push(f.path);
444
+ }
445
+ } else if (node.kind === 'file') {
446
+ stages.add(node.stage);
447
+ files.push(node.path);
448
+ }
449
+ }
450
+ const uniqFiles = [...new Set(files)].sort();
451
+ const stageList = STAGE_ORDER.filter((s) => stages.has(s));
452
+ const writingAffected = stageList.includes('50-writing') || uniqFiles.some((f) => f.includes('50-writing'));
453
+
454
+ return {
455
+ request: userRequest,
456
+ reasons: [...new Set(reasons)],
457
+ entities: requested.map((e) => ({ id: e.id, type: e.type, name: e.name, source: e.source_path || e.source })),
458
+ stages: stageList,
459
+ files: uniqFiles,
460
+ requires_confirmation: writingAffected || requested.length === 0,
461
+ writing_affected: writingAffected,
462
+ manifest_generated_at: m.generated_at || null,
463
+ };
464
+ }
465
+
466
+ /** 校验 manifest 自身与磁盘是否一致。 */
467
+ export function verifyManifest(projectRoot, manifest) {
468
+ const m = manifest || readManifest(projectRoot);
469
+ const errors = [];
470
+ const warnings = [];
471
+ if (!m) {
472
+ return { ok: false, errors: [{ code: 'MANIFEST_MISSING', message: '设定/manifest.json 不存在' }], warnings };
473
+ }
474
+ const current = buildManifest(projectRoot, { write: false });
475
+ const nodeIds = new Set([...Object.keys(m.nodes || {}), ...Object.keys(current.nodes || {})]);
476
+ for (const id of nodeIds) {
477
+ const oldNode = (m.nodes || {})[id];
478
+ const newNode = (current.nodes || {})[id];
479
+ if (!oldNode && newNode) {
480
+ errors.push({ code: 'MANIFEST_NODE_ADDED', path: newNode.path, message: '新增节点未写入 manifest:' + id });
481
+ continue;
482
+ }
483
+ if (oldNode && !newNode) {
484
+ errors.push({ code: 'MANIFEST_NODE_REMOVED', path: oldNode.path, message: '节点已从磁盘删除但 manifest 未更新:' + id });
485
+ continue;
486
+ }
487
+ if (oldNode && newNode && oldNode.hash !== newNode.hash) {
488
+ errors.push({ code: 'MANIFEST_HASH_MISMATCH', path: newNode.path, message: '文件内容变化但 manifest 未更新:' + id });
489
+ }
490
+ }
491
+ // 上游哈希是否与 _meta 声明一致(stale 检测)
492
+ for (const [id, node] of Object.entries(current.nodes || {})) {
493
+ if (node.kind !== 'file') continue;
494
+ const entry = current.nodes[id];
495
+ const data = readJson(path.join(projectRoot, entry.path), null);
496
+ const ups = metaUpstream(data);
497
+ if (!ups) continue;
498
+ for (const upId of ups) {
499
+ const up = current.nodes[upId];
500
+ if (!up) {
501
+ warnings.push({ code: 'MANIFEST_UPSTREAM_UNKNOWN', path: entry.path, message: '上游节点不存在:' + upId });
502
+ continue;
503
+ }
504
+ const declared = asArray(data._meta.upstream).find((x) => x && x.node_id === upId);
505
+ if (declared && declared.hash && up.hash && declared.hash !== up.hash) {
506
+ errors.push({
507
+ code: 'MANIFEST_UPSTREAM_STALE',
508
+ path: entry.path,
509
+ message: '上游已变化,本文件标记 stale:' + upId,
510
+ expected: up.hash,
511
+ actual: declared.hash,
512
+ hint: '重新运行当前阶段,或运行 patch 让依赖图逐层更新',
513
+ });
514
+ }
515
+ }
516
+ }
517
+ return { ok: errors.length === 0, errors, warnings, manifest: current };
518
+ }
519
+
520
+ /**
521
+ * 给业务 JSON 填 _meta.upstream。读取上游文件当前哈希。
522
+ * 返回 meta 对象(不含 content_hash;content_hash 由写盘方补)。
523
+ */
524
+ export function makeUpstreamMeta(projectRoot, nodeIds) {
525
+ const current = buildManifest(projectRoot, { write: false });
526
+ const out = [];
527
+ for (const nodeId of asArray(nodeIds)) {
528
+ const node = current.nodes && current.nodes[nodeId];
529
+ if (!node) continue;
530
+ out.push({
531
+ node_id: nodeId,
532
+ path: node.path,
533
+ hash: node.hash,
534
+ snapshot: node.kind === 'stage' ? '_context/' + nodeId.split('-').pop() + '.json' : undefined,
535
+ });
536
+ }
537
+ return out;
538
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * v2/migrate.js —— 旧 1.x 项目迁移。
3
+ *
4
+ * 迁移原则:
5
+ * - 旧数据不删除,复制到 设定/legacy/v1/ 只读保留;
6
+ * - 能确定性复现的数量先迁移(容量、配额、卷/幕区间);
7
+ * - 宏观/实体/节奏不伪造:缺少新契约字段的内容不硬塞,交给用户确认后重新生成或 patch。
8
+ */
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import {
12
+ V1_STAGE_DIR,
13
+ ensureDir,
14
+ fileExists,
15
+ readJson,
16
+ stageDir,
17
+ stageFile,
18
+ writeJsonAtomic,
19
+ writeTextAtomic,
20
+ } from './core.js';
21
+ import { buildScale, finalizeScale, renderScaleMd, validateScale } from './scale.js';
22
+
23
+ function copyDir(src, dest) {
24
+ if (!fs.existsSync(src)) return false;
25
+ ensureDir(dest);
26
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
27
+ const from = path.join(src, entry.name);
28
+ const to = path.join(dest, entry.name);
29
+ if (entry.isDirectory()) copyDir(from, to);
30
+ else if (entry.isFile()) fs.copyFileSync(from, to);
31
+ }
32
+ return true;
33
+ }
34
+
35
+ function oldFramework(projectRoot) {
36
+ return readJson(path.join(projectRoot, '设定', 'raw', 'sources', '骨架', '框架.json'), null);
37
+ }
38
+
39
+ function oldHierarchy(projectRoot) {
40
+ return readJson(path.join(projectRoot, '设定', 'raw', 'sources', '骨架', '层级.json'), null);
41
+ }
42
+
43
+ /**
44
+ * 生成 scale:优先沿用旧框架的 capacity / quota / volume_ratio。
45
+ */
46
+ export function migrateScale(projectRoot, topicData, formulasDoc, opts = {}) {
47
+ const framework = oldFramework(projectRoot) || {};
48
+ const cap = framework.capacity || {};
49
+ const topic = {
50
+ ...topicData,
51
+ target_chapters: Number(cap.target_chapters) || topicData.target_chapters,
52
+ target_words: Number(cap.target_words) || topicData.target_words,
53
+ };
54
+ const scale = buildScale(topic, formulasDoc, opts);
55
+ const oldQuota = framework.quota || {};
56
+ const q = scale.quotas;
57
+ const set = (obj, key, value) => {
58
+ if (value === undefined || value === null) return;
59
+ const n = Number(value);
60
+ if (Number.isFinite(n)) obj[key] = n;
61
+ };
62
+ set(q.characters, 'protagonist', oldQuota.character_counts && oldQuota.character_counts.protagonist);
63
+ set(q.characters, 'core_supporting', oldQuota.character_counts && oldQuota.character_counts.core_supporting);
64
+ set(q.characters, 'volume_supporting', oldQuota.character_counts && oldQuota.character_counts.volume_supporting);
65
+ set(q.characters, 'core_antagonist', oldQuota.character_counts && oldQuota.character_counts.core_antagonist);
66
+ set(q.characters, 'volume_antagonist', oldQuota.character_counts && oldQuota.character_counts.volume_antagonist);
67
+ set(q, 'factions', oldQuota.faction_count);
68
+ set(q, 'faction_relations', oldQuota.faction_relation_count);
69
+ set(q, 'locations', oldQuota.location_count);
70
+ set(q.items, 'core', oldQuota.item_counts && oldQuota.item_counts.core);
71
+ set(q.items, 'branch', oldQuota.item_counts && oldQuota.item_counts.branch);
72
+ set(q.items, 'symbolic', oldQuota.item_counts && oldQuota.item_counts.symbolic);
73
+ set(q.conflicts, 'core', oldQuota.conflict_counts && oldQuota.conflict_counts.core);
74
+ set(q.conflicts, 'phase', oldQuota.conflict_counts && oldQuota.conflict_counts.phase);
75
+ set(q.conflicts, 'branch', oldQuota.conflict_counts && oldQuota.conflict_counts.branch);
76
+ set(q.foreshadows, 'small', oldQuota.foreshadow_counts && oldQuota.foreshadow_counts.small);
77
+ set(q.foreshadows, 'medium', oldQuota.foreshadow_counts && oldQuota.foreshadow_counts.medium);
78
+ set(q.foreshadows, 'large', oldQuota.foreshadow_counts && oldQuota.foreshadow_counts.large);
79
+ set(q.climaxes, 'small', oldQuota.climax_counts && oldQuota.climax_counts.small);
80
+ set(q.climaxes, 'medium', oldQuota.climax_counts && oldQuota.climax_counts.medium);
81
+ set(q.climaxes, 'large', oldQuota.climax_counts && oldQuota.climax_counts.large);
82
+ set(q.pleasures, 'small', oldQuota.pleasure_counts && oldQuota.pleasure_counts.small);
83
+ set(q.pleasures, 'medium', oldQuota.pleasure_counts && oldQuota.pleasure_counts.medium);
84
+ set(q.pleasures, 'large', oldQuota.pleasure_counts && oldQuota.pleasure_counts.large);
85
+ set(q.world_rules, 'core', oldQuota.world_rule_counts && oldQuota.world_rule_counts.core);
86
+ set(q.world_rules, 'information', oldQuota.world_rule_counts && oldQuota.world_rule_counts.information);
87
+ set(q.world_rules, 'social', oldQuota.world_rule_counts && oldQuota.world_rule_counts.social);
88
+ set(q, 'endings', oldQuota.ending_count);
89
+ set(q, 'branches', oldQuota.branch_count);
90
+ set(q, 'main_lines', oldQuota.main_line_count);
91
+ if (oldQuota.relations && typeof oldQuota.relations === 'object') {
92
+ set(q.relations, 'character', oldQuota.relations.character);
93
+ set(q.relations, 'faction', oldQuota.relations.faction);
94
+ set(q.relations, 'membership', oldQuota.relations.membership);
95
+ }
96
+ finalizeScale(scale, formulasDoc, opts);
97
+ return scale;
98
+ }
99
+
100
+ export function migrateV1(projectRoot, topicData, formulasDoc, opts = {}) {
101
+ const legacyRoot = path.join(projectRoot, '设定', 'legacy', 'v1');
102
+ ensureDir(legacyRoot);
103
+ const copied = [];
104
+ for (const name of Object.values(V1_STAGE_DIR)) {
105
+ const src = path.join(projectRoot, '设定', 'raw', 'sources', name);
106
+ if (!fs.existsSync(src)) continue;
107
+ copyDir(src, path.join(legacyRoot, name));
108
+ copied.push(name);
109
+ }
110
+
111
+ const scale = migrateScale(projectRoot, topicData, formulasDoc, opts);
112
+ const validation = validateScale(scale, formulasDoc, opts);
113
+ if (validation.ok) {
114
+ writeJsonAtomic(stageFile(projectRoot, '10-scale', 'scale.json'), scale);
115
+ writeTextAtomic(stageFile(projectRoot, '10-scale', 'scale.md'), renderScaleMd(scale));
116
+ }
117
+
118
+ // 迁移报告:明确哪些东西没有伪造。
119
+ const hierarchy = oldHierarchy(projectRoot);
120
+ const report = {
121
+ schema_version: '2.1',
122
+ migrated_at: new Date().toISOString(),
123
+ status: validation.ok ? 'MIGRATED_PARTIAL' : 'MIGRATED_SCALE_INVALID',
124
+ legacy_dirs_copied: copied,
125
+ scale_ok: validation.ok,
126
+ scale_errors: validation.errors,
127
+ volumes_in_legacy: Array.isArray(hierarchy && hierarchy.volumes) ? hierarchy.volumes.length : 0,
128
+ notes: [
129
+ '旧 骨架/血肉/节奏 已复制到 设定/legacy/v1/,只读保留。',
130
+ '10-scale 已按旧框架的 capacity/quota 迁移。',
131
+ '20-macro / 30-cast / 40-rhythm 不自动伪造:旧数据缺少新契约字段时,硬转换会产出看似完整但不可校验的内容。',
132
+ '下一步:检查 10-scale/scale.json,然后依次运行 /fancy-story-builder macro、cast、rhythm。',
133
+ ],
134
+ };
135
+ writeJsonAtomic(path.join(projectRoot, '设定', 'legacy', 'v1', 'migration-report.json'), report);
136
+ return report;
137
+ }