modwright 0.1.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 (455) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +212 -0
  3. package/assets/claude/agents/mw-explore.md +46 -0
  4. package/assets/claude/agents/mw-fixture.md +33 -0
  5. package/assets/claude/agents/mw-rule-author.md +51 -0
  6. package/assets/claude/agents/mw-scaffold-author.md +48 -0
  7. package/assets/claude/skills/mw-author.md +59 -0
  8. package/assets/claude/skills/mw-new-item.md +51 -0
  9. package/assets/claude/skills/mw-new-spell.md +50 -0
  10. package/assets/claude/skills/mw-scaffold.md +59 -0
  11. package/assets/claude/skills/mw-ship.md +57 -0
  12. package/assets/claude/skills/mw-validate.md +51 -0
  13. package/assets/claude/skills/mw-verify.md +59 -0
  14. package/assets/nivalisnights/extract_resources.py +177 -0
  15. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Config.json +5 -0
  16. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/BootstrapClient.lua +55 -0
  17. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/BootstrapServer.lua +81 -0
  18. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Log.lua +96 -0
  19. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Probes.lua +1038 -0
  20. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/ProbesClient.lua +74 -0
  21. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Protocol.lua +985 -0
  22. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Watch.lua +175 -0
  23. package/bridges/bg3-se/Mods/ModWrightBridge/meta.lsx +45 -0
  24. package/bridges/bg3-se/README.md +99 -0
  25. package/bridges/bg3-se/bridge.json +68 -0
  26. package/bridges/cp2077-cet/ModWrightBridge/init.lua +375 -0
  27. package/bridges/cp2077-cet/ModWrightBridge/probes.lua +1104 -0
  28. package/bridges/cp2077-cet/ModWrightBridge/protocol.lua +1306 -0
  29. package/bridges/cp2077-cet/README.md +165 -0
  30. package/bridges/cp2077-cet/bridge.json +53 -0
  31. package/dist/core/bridge/arm.js +175 -0
  32. package/dist/core/bridge/cet-console.js +26 -0
  33. package/dist/core/bridge/cet-log.js +362 -0
  34. package/dist/core/bridge/index.js +11 -0
  35. package/dist/core/bridge/locate.js +35 -0
  36. package/dist/core/bridge/paths.js +34 -0
  37. package/dist/core/bridge/pending.js +89 -0
  38. package/dist/core/bridge/poll.js +155 -0
  39. package/dist/core/bridge/stage.js +100 -0
  40. package/dist/core/bridge/status.js +97 -0
  41. package/dist/core/bridge/types.js +145 -0
  42. package/dist/core/bridge/validate.js +107 -0
  43. package/dist/core/build/index.js +6 -0
  44. package/dist/core/build/manifest.js +47 -0
  45. package/dist/core/build/paths.js +32 -0
  46. package/dist/core/build/run.js +342 -0
  47. package/dist/core/build/stage.js +265 -0
  48. package/dist/core/build/steps/bg3.js +129 -0
  49. package/dist/core/build/steps/core.js +195 -0
  50. package/dist/core/build/steps/cyberpunk.js +95 -0
  51. package/dist/core/build/steps/listing.js +35 -0
  52. package/dist/core/build/types.js +13 -0
  53. package/dist/core/build/zip.js +148 -0
  54. package/dist/core/compat/check.js +113 -0
  55. package/dist/core/compat/pe.js +220 -0
  56. package/dist/core/compat/types.js +1 -0
  57. package/dist/core/compat/version.js +70 -0
  58. package/dist/core/deploy/guards.js +292 -0
  59. package/dist/core/deploy/index.js +5 -0
  60. package/dist/core/deploy/plan.js +696 -0
  61. package/dist/core/deploy/reload-apply.js +361 -0
  62. package/dist/core/deploy/reload.js +358 -0
  63. package/dist/core/deploy/sources.js +169 -0
  64. package/dist/core/deploy/types.js +1 -0
  65. package/dist/core/epic.js +54 -0
  66. package/dist/core/fsutil.js +191 -0
  67. package/dist/core/home.js +70 -0
  68. package/dist/core/knowledge/facts.js +229 -0
  69. package/dist/core/knowledge/index.js +4 -0
  70. package/dist/core/knowledge/search.js +112 -0
  71. package/dist/core/knowledge/store.js +123 -0
  72. package/dist/core/knowledge/types.js +1 -0
  73. package/dist/core/ledger/check.js +42 -0
  74. package/dist/core/ledger/hooks.js +80 -0
  75. package/dist/core/ledger/index.js +7 -0
  76. package/dist/core/ledger/query.js +17 -0
  77. package/dist/core/ledger/record.js +84 -0
  78. package/dist/core/ledger/status.js +53 -0
  79. package/dist/core/ledger/store.js +98 -0
  80. package/dist/core/ledger/types.js +108 -0
  81. package/dist/core/logs/index.js +4 -0
  82. package/dist/core/logs/registry.js +50 -0
  83. package/dist/core/logs/scan.js +195 -0
  84. package/dist/core/logs/triage.js +160 -0
  85. package/dist/core/logs/types.js +1 -0
  86. package/dist/core/lsx.js +10 -0
  87. package/dist/core/lz4.js +66 -0
  88. package/dist/core/mods.js +229 -0
  89. package/dist/core/process.js +49 -0
  90. package/dist/core/project/build-steps.js +83 -0
  91. package/dist/core/project/describe.js +74 -0
  92. package/dist/core/project/index.js +5 -0
  93. package/dist/core/project/load.js +146 -0
  94. package/dist/core/project/schema.js +264 -0
  95. package/dist/core/project/types.js +3 -0
  96. package/dist/core/prompts.js +72 -0
  97. package/dist/core/registry.js +35 -0
  98. package/dist/core/safety/backup.js +206 -0
  99. package/dist/core/safety/execute.js +188 -0
  100. package/dist/core/safety/index.js +5 -0
  101. package/dist/core/safety/plan.js +122 -0
  102. package/dist/core/safety/restore.js +172 -0
  103. package/dist/core/safety/types.js +1 -0
  104. package/dist/core/scaffold/bootstrap.js +7 -0
  105. package/dist/core/scaffold/catalog.js +28 -0
  106. package/dist/core/scaffold/claude-assets.js +271 -0
  107. package/dist/core/scaffold/collisions.js +64 -0
  108. package/dist/core/scaffold/emit.js +356 -0
  109. package/dist/core/scaffold/engine.js +680 -0
  110. package/dist/core/scaffold/ids.js +78 -0
  111. package/dist/core/scaffold/index.js +10 -0
  112. package/dist/core/scaffold/jsonpath.js +121 -0
  113. package/dist/core/scaffold/load.js +81 -0
  114. package/dist/core/scaffold/render.js +85 -0
  115. package/dist/core/scaffold/sequence.js +396 -0
  116. package/dist/core/scaffold/types.js +309 -0
  117. package/dist/core/steam.js +63 -0
  118. package/dist/core/surfaceutil.js +36 -0
  119. package/dist/core/testplan/check.js +67 -0
  120. package/dist/core/testplan/index.js +5 -0
  121. package/dist/core/testplan/load.js +100 -0
  122. package/dist/core/testplan/registry.js +317 -0
  123. package/dist/core/testplan/run.js +389 -0
  124. package/dist/core/testplan/types.js +82 -0
  125. package/dist/core/text/converters/bg3.js +50 -0
  126. package/dist/core/text/converters/cyberpunk.js +36 -0
  127. package/dist/core/text/converters/index.js +261 -0
  128. package/dist/core/text/diff/cr2w.js +180 -0
  129. package/dist/core/text/diff/index.js +16 -0
  130. package/dist/core/text/diff/loca.js +43 -0
  131. package/dist/core/text/diff/lsx.js +126 -0
  132. package/dist/core/text/handles.js +86 -0
  133. package/dist/core/text/index.js +6 -0
  134. package/dist/core/text/json.js +46 -0
  135. package/dist/core/text/templates/cache.js +71 -0
  136. package/dist/core/text/templates/index.js +3 -0
  137. package/dist/core/text/templates/library.js +245 -0
  138. package/dist/core/text/templates/types.js +51 -0
  139. package/dist/core/text/toolchain.js +16 -0
  140. package/dist/core/text/types.js +1 -0
  141. package/dist/core/text/validate.js +149 -0
  142. package/dist/core/text/xml.js +58 -0
  143. package/dist/core/toolchain/datafetch.js +151 -0
  144. package/dist/core/toolchain/env.js +63 -0
  145. package/dist/core/toolchain/features.js +170 -0
  146. package/dist/core/toolchain/index.js +8 -0
  147. package/dist/core/toolchain/install.js +191 -0
  148. package/dist/core/toolchain/locate.js +275 -0
  149. package/dist/core/toolchain/remember.js +84 -0
  150. package/dist/core/toolchain/run.js +318 -0
  151. package/dist/core/toolchain/specs.js +351 -0
  152. package/dist/core/toolchain/types.js +1 -0
  153. package/dist/core/types.js +1 -0
  154. package/dist/core/userconfig.js +66 -0
  155. package/dist/core/validate/context.js +139 -0
  156. package/dist/core/validate/index.js +5 -0
  157. package/dist/core/validate/registry.js +58 -0
  158. package/dist/core/validate/run.js +131 -0
  159. package/dist/core/validate/suppressions.js +91 -0
  160. package/dist/core/validate/types.js +2 -0
  161. package/dist/core/yamledit.js +51 -0
  162. package/dist/index.js +389 -0
  163. package/dist/server.js +1914 -0
  164. package/dist/surfaces/baldursgate3/compat.js +164 -0
  165. package/dist/surfaces/baldursgate3/identity.js +110 -0
  166. package/dist/surfaces/baldursgate3/index/build.js +331 -0
  167. package/dist/surfaces/baldursgate3/index/index.js +17 -0
  168. package/dist/surfaces/baldursgate3/index/info.js +40 -0
  169. package/dist/surfaces/baldursgate3/index/parsers/loca.js +41 -0
  170. package/dist/surfaces/baldursgate3/index/parsers/lsx.js +137 -0
  171. package/dist/surfaces/baldursgate3/index/parsers/stats.js +45 -0
  172. package/dist/surfaces/baldursgate3/index/parsers/treasuretable.js +57 -0
  173. package/dist/surfaces/baldursgate3/index/parsers/xmlutil.js +142 -0
  174. package/dist/surfaces/baldursgate3/index/queries.js +327 -0
  175. package/dist/surfaces/baldursgate3/index/schema.js +62 -0
  176. package/dist/surfaces/baldursgate3/index.js +3 -0
  177. package/dist/surfaces/baldursgate3/logs.js +278 -0
  178. package/dist/surfaces/baldursgate3/modsettings.js +115 -0
  179. package/dist/surfaces/baldursgate3/pak.js +153 -0
  180. package/dist/surfaces/baldursgate3/settings.js +49 -0
  181. package/dist/surfaces/baldursgate3/surface.js +232 -0
  182. package/dist/surfaces/baldursgate3/validators/bridge.js +7 -0
  183. package/dist/surfaces/baldursgate3/validators/files.js +476 -0
  184. package/dist/surfaces/baldursgate3/validators/items.js +1001 -0
  185. package/dist/surfaces/baldursgate3/validators/progression.js +734 -0
  186. package/dist/surfaces/baldursgate3/validators/stats.js +1076 -0
  187. package/dist/surfaces/baldursgate3/validators/story.js +790 -0
  188. package/dist/surfaces/baldursgate3/validators/templates.js +7 -0
  189. package/dist/surfaces/cyberpunk2077/compat.js +223 -0
  190. package/dist/surfaces/cyberpunk2077/index/build.js +231 -0
  191. package/dist/surfaces/cyberpunk2077/index/index.js +17 -0
  192. package/dist/surfaces/cyberpunk2077/index/info.js +51 -0
  193. package/dist/surfaces/cyberpunk2077/index/model.js +52 -0
  194. package/dist/surfaces/cyberpunk2077/index/parsers/tweak.js +483 -0
  195. package/dist/surfaces/cyberpunk2077/index/parsers/tweakxl-yaml.js +183 -0
  196. package/dist/surfaces/cyberpunk2077/index/queries.js +504 -0
  197. package/dist/surfaces/cyberpunk2077/index/resource.js +135 -0
  198. package/dist/surfaces/cyberpunk2077/index/schema.js +90 -0
  199. package/dist/surfaces/cyberpunk2077/index.js +3 -0
  200. package/dist/surfaces/cyberpunk2077/logs.js +742 -0
  201. package/dist/surfaces/cyberpunk2077/paths.js +12 -0
  202. package/dist/surfaces/cyberpunk2077/surface.js +218 -0
  203. package/dist/surfaces/cyberpunk2077/validators/bridge.js +7 -0
  204. package/dist/surfaces/cyberpunk2077/validators/cr2w.js +349 -0
  205. package/dist/surfaces/cyberpunk2077/validators/garments.js +1181 -0
  206. package/dist/surfaces/cyberpunk2077/validators/mesh.js +338 -0
  207. package/dist/surfaces/cyberpunk2077/validators/modsettings.js +120 -0
  208. package/dist/surfaces/cyberpunk2077/validators/packaging.js +463 -0
  209. package/dist/surfaces/cyberpunk2077/validators/red4ext.js +318 -0
  210. package/dist/surfaces/cyberpunk2077/validators/redscript.js +193 -0
  211. package/dist/surfaces/cyberpunk2077/validators/templates.js +7 -0
  212. package/dist/surfaces/cyberpunk2077/validators/tweaks.js +768 -0
  213. package/dist/surfaces/cyberpunk2077/validators/vehicles.js +531 -0
  214. package/dist/surfaces/cyberpunk2077/validators/xl.js +308 -0
  215. package/dist/surfaces/eldenring/binders.js +111 -0
  216. package/dist/surfaces/eldenring/compat.js +166 -0
  217. package/dist/surfaces/eldenring/dcx.js +49 -0
  218. package/dist/surfaces/eldenring/index.js +13 -0
  219. package/dist/surfaces/eldenring/loaders.js +132 -0
  220. package/dist/surfaces/eldenring/logs.js +67 -0
  221. package/dist/surfaces/eldenring/paramdef.js +256 -0
  222. package/dist/surfaces/eldenring/params.js +129 -0
  223. package/dist/surfaces/eldenring/surface.js +121 -0
  224. package/dist/surfaces/eldenring/tools.js +52 -0
  225. package/dist/surfaces/eldenring/validators/binders.js +193 -0
  226. package/dist/surfaces/eldenring/validators/packages.js +159 -0
  227. package/dist/surfaces/eldenring/validators/params.js +334 -0
  228. package/dist/surfaces/eldenring/validators/profiles.js +287 -0
  229. package/dist/surfaces/index.js +34 -0
  230. package/dist/surfaces/nivalisnights/compat.js +229 -0
  231. package/dist/surfaces/nivalisnights/index/build.js +310 -0
  232. package/dist/surfaces/nivalisnights/index/index.js +5 -0
  233. package/dist/surfaces/nivalisnights/index/queries.js +231 -0
  234. package/dist/surfaces/nivalisnights/index.js +4 -0
  235. package/dist/surfaces/nivalisnights/logs.js +473 -0
  236. package/dist/surfaces/nivalisnights/surface.js +195 -0
  237. package/dist/surfaces/skyrimse/archives.js +229 -0
  238. package/dist/surfaces/skyrimse/compat.js +163 -0
  239. package/dist/surfaces/skyrimse/index.js +12 -0
  240. package/dist/surfaces/skyrimse/loadorder.js +174 -0
  241. package/dist/surfaces/skyrimse/logs.js +241 -0
  242. package/dist/surfaces/skyrimse/plugins.js +149 -0
  243. package/dist/surfaces/skyrimse/records.js +94 -0
  244. package/dist/surfaces/skyrimse/surface.js +240 -0
  245. package/dist/surfaces/skyrimse/validators/archive-records.js +165 -0
  246. package/dist/surfaces/skyrimse/validators/archives.js +110 -0
  247. package/dist/surfaces/skyrimse/validators/plugins.js +374 -0
  248. package/dist/surfaces/skyrimse/validators/records.js +117 -0
  249. package/dist/surfaces/stardewvalley/compat.js +190 -0
  250. package/dist/surfaces/stardewvalley/config.js +42 -0
  251. package/dist/surfaces/stardewvalley/contentpatcher.js +203 -0
  252. package/dist/surfaces/stardewvalley/index/index.js +229 -0
  253. package/dist/surfaces/stardewvalley/index/wiki.js +190 -0
  254. package/dist/surfaces/stardewvalley/index.js +12 -0
  255. package/dist/surfaces/stardewvalley/logs.js +307 -0
  256. package/dist/surfaces/stardewvalley/mods.js +182 -0
  257. package/dist/surfaces/stardewvalley/surface.js +244 -0
  258. package/dist/surfaces/stardewvalley/validators/content.js +321 -0
  259. package/dist/surfaces/stardewvalley/validators/fields.js +147 -0
  260. package/dist/surfaces/stardewvalley/validators/loadorder.js +69 -0
  261. package/dist/surfaces/stardewvalley/validators/manifest.js +221 -0
  262. package/dist/surfaces/subnautica2/compat.js +138 -0
  263. package/dist/surfaces/subnautica2/index.js +5 -0
  264. package/dist/surfaces/subnautica2/logs.js +129 -0
  265. package/dist/surfaces/subnautica2/paks.js +173 -0
  266. package/dist/surfaces/subnautica2/surface.js +151 -0
  267. package/dist/surfaces/subnautica2/validators/packaging.js +298 -0
  268. package/dist/surfaces/valheim/compat.js +171 -0
  269. package/dist/surfaces/valheim/index/index.js +301 -0
  270. package/dist/surfaces/valheim/index/tables.js +103 -0
  271. package/dist/surfaces/valheim/index.js +11 -0
  272. package/dist/surfaces/valheim/logs.js +197 -0
  273. package/dist/surfaces/valheim/packaging.js +131 -0
  274. package/dist/surfaces/valheim/plugins.js +270 -0
  275. package/dist/surfaces/valheim/references.js +121 -0
  276. package/dist/surfaces/valheim/surface.js +223 -0
  277. package/dist/surfaces/valheim/validators/packaging.js +218 -0
  278. package/dist/surfaces/valheim/validators/plugins.js +287 -0
  279. package/dist/surfaces/valheim/validators/references.js +169 -0
  280. package/knowledge/baldursgate3/animations.gr2.yaml +166 -0
  281. package/knowledge/baldursgate3/audio.voice.yaml +182 -0
  282. package/knowledge/baldursgate3/audio.wwise.yaml +104 -0
  283. package/knowledge/baldursgate3/charactercreation.heads-hair.yaml +193 -0
  284. package/knowledge/baldursgate3/communitylibrary.shared-content.yaml +129 -0
  285. package/knowledge/baldursgate3/compatibilityframework.api.yaml +176 -0
  286. package/knowledge/baldursgate3/crash.causes.yaml +90 -0
  287. package/knowledge/baldursgate3/dialogue.dialogbank.yaml +120 -0
  288. package/knowledge/baldursgate3/dialogue.files.yaml +236 -0
  289. package/knowledge/baldursgate3/dialogue.osiris-goals.yaml +386 -0
  290. package/knowledge/baldursgate3/dialogue.yaml +122 -0
  291. package/knowledge/baldursgate3/feats.classes.yaml +164 -0
  292. package/knowledge/baldursgate3/items.cloth-physics.yaml +167 -0
  293. package/knowledge/baldursgate3/items.roottemplates.yaml +320 -0
  294. package/knowledge/baldursgate3/items.visualbank.yaml +628 -0
  295. package/knowledge/baldursgate3/journal.quests.yaml +214 -0
  296. package/knowledge/baldursgate3/loadorder.modsettings.yaml +128 -0
  297. package/knowledge/baldursgate3/localization.handles.yaml +92 -0
  298. package/knowledge/baldursgate3/misc.encoding-and-practice.yaml +111 -0
  299. package/knowledge/baldursgate3/origins.companions.yaml +144 -0
  300. package/knowledge/baldursgate3/osiris.signatures.yaml +211 -0
  301. package/knowledge/baldursgate3/packaging.meta.yaml +155 -0
  302. package/knowledge/baldursgate3/packaging.paks.yaml +79 -0
  303. package/knowledge/baldursgate3/process.launch.yaml +34 -0
  304. package/knowledge/baldursgate3/progressions.yaml +92 -0
  305. package/knowledge/baldursgate3/publishing.modio-nexus.yaml +170 -0
  306. package/knowledge/baldursgate3/publishing.toolkit-modio.yaml +135 -0
  307. package/knowledge/baldursgate3/races.progression.yaml +228 -0
  308. package/knowledge/baldursgate3/scriptextender.console.yaml +257 -0
  309. package/knowledge/baldursgate3/scriptextender.yaml +252 -0
  310. package/knowledge/baldursgate3/stats.boosts.yaml +293 -0
  311. package/knowledge/baldursgate3/stats.equipment-kits.yaml +69 -0
  312. package/knowledge/baldursgate3/stats.interrupts.yaml +45 -0
  313. package/knowledge/baldursgate3/stats.itemcombos.yaml +65 -0
  314. package/knowledge/baldursgate3/stats.spells.yaml +293 -0
  315. package/knowledge/baldursgate3/stats.weapons.yaml +210 -0
  316. package/knowledge/baldursgate3/toolchain.divine.yaml +179 -0
  317. package/knowledge/baldursgate3/toolchain.lslib-formats.yaml +242 -0
  318. package/knowledge/baldursgate3/toolkit.levels.yaml +177 -0
  319. package/knowledge/baldursgate3/ui.icons-atlas.yaml +78 -0
  320. package/knowledge/baldursgate3/ui.icons.yaml +412 -0
  321. package/knowledge/baldursgate3/ui.mcm.yaml +161 -0
  322. package/knowledge/baldursgate3/vfx.materials.yaml +174 -0
  323. package/knowledge/baldursgate3/visuals.characterfix.yaml +73 -0
  324. package/knowledge/cyberpunk2077/animations.anims.yaml +118 -0
  325. package/knowledge/cyberpunk2077/anims.archivexl.yaml +163 -0
  326. package/knowledge/cyberpunk2077/anims.authoring.yaml +579 -0
  327. package/knowledge/cyberpunk2077/anims.encoder.yaml +156 -0
  328. package/knowledge/cyberpunk2077/anims.graph.yaml +629 -0
  329. package/knowledge/cyberpunk2077/anims.method.yaml +360 -0
  330. package/knowledge/cyberpunk2077/anims.pipeline.yaml +352 -0
  331. package/knowledge/cyberpunk2077/anims.roundtrip.yaml +222 -0
  332. package/knowledge/cyberpunk2077/anims.sets.yaml +176 -0
  333. package/knowledge/cyberpunk2077/archivexl.bodytypes.yaml +149 -0
  334. package/knowledge/cyberpunk2077/archivexl.dynamic-appearances.yaml +218 -0
  335. package/knowledge/cyberpunk2077/archivexl.manifest.yaml +214 -0
  336. package/knowledge/cyberpunk2077/audio.events.yaml +168 -0
  337. package/knowledge/cyberpunk2077/audio.sounds.yaml +164 -0
  338. package/knowledge/cyberpunk2077/audio.voicesets.yaml +146 -0
  339. package/knowledge/cyberpunk2077/cet.sandbox.yaml +219 -0
  340. package/knowledge/cyberpunk2077/clothing.garments.yaml +283 -0
  341. package/knowledge/cyberpunk2077/clothing.refits.yaml +215 -0
  342. package/knowledge/cyberpunk2077/codeware.overview.yaml +111 -0
  343. package/knowledge/cyberpunk2077/codeware.systems.yaml +192 -0
  344. package/knowledge/cyberpunk2077/drones.combat.yaml +416 -0
  345. package/knowledge/cyberpunk2077/drones.control.yaml +552 -0
  346. package/knowledge/cyberpunk2077/entities.appearance.yaml +166 -0
  347. package/knowledge/cyberpunk2077/equipmentex.slots.yaml +151 -0
  348. package/knowledge/cyberpunk2077/fx.effects.yaml +246 -0
  349. package/knowledge/cyberpunk2077/fx.lights.yaml +171 -0
  350. package/knowledge/cyberpunk2077/hair.modding.yaml +145 -0
  351. package/knowledge/cyberpunk2077/items.wiring.yaml +246 -0
  352. package/knowledge/cyberpunk2077/loadorder.archives.yaml +71 -0
  353. package/knowledge/cyberpunk2077/mesh.blender.yaml +209 -0
  354. package/knowledge/cyberpunk2077/nativedb.dump.yaml +98 -0
  355. package/knowledge/cyberpunk2077/npc.appearance.yaml +275 -0
  356. package/knowledge/cyberpunk2077/npc.behaviour.yaml +128 -0
  357. package/knowledge/cyberpunk2077/npc.locomotion.yaml +490 -0
  358. package/knowledge/cyberpunk2077/packaging.deploy.yaml +175 -0
  359. package/knowledge/cyberpunk2077/packaging.variants.yaml +176 -0
  360. package/knowledge/cyberpunk2077/player.control.yaml +918 -0
  361. package/knowledge/cyberpunk2077/process.assetbuild.yaml +154 -0
  362. package/knowledge/cyberpunk2077/process.lessons.yaml +276 -0
  363. package/knowledge/cyberpunk2077/quests.minor.yaml +46 -0
  364. package/knowledge/cyberpunk2077/quests.scenes.yaml +282 -0
  365. package/knowledge/cyberpunk2077/quickhacks.tiers.yaml +41 -0
  366. package/knowledge/cyberpunk2077/red4ext.api.yaml +645 -0
  367. package/knowledge/cyberpunk2077/red4ext.plugins.yaml +124 -0
  368. package/knowledge/cyberpunk2077/redhottools.hotreload.yaml +245 -0
  369. package/knowledge/cyberpunk2077/redscript.compile-log.yaml +76 -0
  370. package/knowledge/cyberpunk2077/redscript.modules.yaml +118 -0
  371. package/knowledge/cyberpunk2077/toolchain.wolvenkit.yaml +163 -0
  372. package/knowledge/cyberpunk2077/tweakdb.loot-recipes.yaml +184 -0
  373. package/knowledge/cyberpunk2077/tweakdb.melee.yaml +352 -0
  374. package/knowledge/cyberpunk2077/tweakdb.weapons.yaml +325 -0
  375. package/knowledge/cyberpunk2077/tweakxl.hotreload.yaml +151 -0
  376. package/knowledge/cyberpunk2077/tweakxl.yaml-syntax.yaml +208 -0
  377. package/knowledge/cyberpunk2077/ui.icons.yaml +259 -0
  378. package/knowledge/cyberpunk2077/ui.inkatlas.yaml +81 -0
  379. package/knowledge/cyberpunk2077/ui.modsettings.yaml +204 -0
  380. package/knowledge/cyberpunk2077/vehicles.control.yaml +475 -0
  381. package/knowledge/cyberpunk2077/vehicles.dashboard-ui.yaml +203 -0
  382. package/knowledge/cyberpunk2077/vehicles.entities.yaml +195 -0
  383. package/knowledge/cyberpunk2077/vehicles.wiring.yaml +137 -0
  384. package/knowledge/cyberpunk2077/wolvenkit.cli.yaml +167 -0
  385. package/knowledge/cyberpunk2077/workspot.system.yaml +55 -0
  386. package/knowledge/cyberpunk2077/world.interactions.yaml +41 -0
  387. package/knowledge/cyberpunk2077/world.loot-container.yaml +167 -0
  388. package/knowledge/cyberpunk2077/world.sectors.yaml +233 -0
  389. package/knowledge/eldenring/formats.binders.yaml +237 -0
  390. package/knowledge/eldenring/launcher.me3.yaml +230 -0
  391. package/knowledge/eldenring/launcher.modengine2.yaml +119 -0
  392. package/knowledge/eldenring/params.paramdef.yaml +149 -0
  393. package/knowledge/eldenring/params.regulation.yaml +142 -0
  394. package/knowledge/eldenring/saves.online.yaml +118 -0
  395. package/knowledge/eldenring/toolchain.cli.yaml +256 -0
  396. package/knowledge/nivalisnights/modding.runtime.yaml +157 -0
  397. package/knowledge/skyrimse/archives.bsa.yaml +212 -0
  398. package/knowledge/skyrimse/install.detection.yaml +41 -0
  399. package/knowledge/skyrimse/loadorder.files.yaml +242 -0
  400. package/knowledge/skyrimse/plugins.esl.yaml +189 -0
  401. package/knowledge/skyrimse/plugins.format.yaml +224 -0
  402. package/knowledge/skyrimse/skse.runtime.yaml +283 -0
  403. package/knowledge/skyrimse/toolchain.xedit-spriggit.yaml +336 -0
  404. package/knowledge/stardewvalley/contentpatcher.format.yaml +338 -0
  405. package/knowledge/stardewvalley/distribution.channels.yaml +236 -0
  406. package/knowledge/stardewvalley/game.versions.yaml +202 -0
  407. package/knowledge/stardewvalley/install.layout.yaml +277 -0
  408. package/knowledge/stardewvalley/smapi.loader.yaml +284 -0
  409. package/knowledge/stardewvalley/smapi.logs.yaml +329 -0
  410. package/knowledge/stardewvalley/smapi.manifest.yaml +293 -0
  411. package/knowledge/stardewvalley/toolchain.build.yaml +261 -0
  412. package/knowledge/stardewvalley/wiki.dataformat.yaml +130 -0
  413. package/knowledge/subnautica2/game.build.yaml +331 -0
  414. package/knowledge/subnautica2/modding.state.yaml +210 -0
  415. package/knowledge/subnautica2/ue5.containers.yaml +72 -0
  416. package/knowledge/subnautica2/ue5.paks.yaml +203 -0
  417. package/knowledge/subnautica2/ue5.tooling.yaml +175 -0
  418. package/knowledge/subnautica2/ue5.ue4ss.yaml +203 -0
  419. package/knowledge/valheim/bepinex.loader.yaml +247 -0
  420. package/knowledge/valheim/bepinex.plugins.yaml +251 -0
  421. package/knowledge/valheim/game.versions.yaml +321 -0
  422. package/knowledge/valheim/install.layout.yaml +216 -0
  423. package/knowledge/valheim/jotunn.library.yaml +334 -0
  424. package/knowledge/valheim/thunderstore.packaging.yaml +217 -0
  425. package/knowledge/valheim/toolchain.build.yaml +111 -0
  426. package/package.json +29 -0
  427. package/scaffolds/baldursgate3/new-class-injection.yaml +148 -0
  428. package/scaffolds/baldursgate3/new-item.yaml +264 -0
  429. package/scaffolds/baldursgate3/new-mod.yaml +174 -0
  430. package/scaffolds/baldursgate3/new-passive.yaml +100 -0
  431. package/scaffolds/baldursgate3/new-project.yaml +118 -0
  432. package/scaffolds/baldursgate3/new-quest-stub.yaml +159 -0
  433. package/scaffolds/baldursgate3/new-spell.yaml +102 -0
  434. package/scaffolds/baldursgate3/new-status.yaml +119 -0
  435. package/scaffolds/baldursgate3/sequences/subclass-kit.yaml +50 -0
  436. package/scaffolds/cyberpunk2077/edit-tweak.yaml +70 -0
  437. package/scaffolds/cyberpunk2077/mesh-export-preset.yaml +170 -0
  438. package/scaffolds/cyberpunk2077/new-entity-patch.yaml +255 -0
  439. package/scaffolds/cyberpunk2077/new-garment-refit.yaml +279 -0
  440. package/scaffolds/cyberpunk2077/new-item-chain.yaml +548 -0
  441. package/scaffolds/cyberpunk2077/new-localization.yaml +130 -0
  442. package/scaffolds/cyberpunk2077/new-mod-settings.yaml +133 -0
  443. package/scaffolds/cyberpunk2077/new-player-replacer.yaml +1003 -0
  444. package/scaffolds/cyberpunk2077/new-project.yaml +157 -0
  445. package/scaffolds/cyberpunk2077/new-sector-node.yaml +156 -0
  446. package/scaffolds/cyberpunk2077/new-tweak.yaml +121 -0
  447. package/scaffolds/cyberpunk2077/new-vehicle-livery.yaml +148 -0
  448. package/scaffolds/cyberpunk2077/new-workspot-entity.yaml +177 -0
  449. package/scaffolds/cyberpunk2077/sequences/iconic-weapon.yaml +72 -0
  450. package/scaffolds/cyberpunk2077/sequences/quickhack-takeover.yaml +213 -0
  451. package/scaffolds/stardewvalley/new-cp-patch.yaml +106 -0
  452. package/scaffolds/stardewvalley/new-csharp-mod.yaml +195 -0
  453. package/scaffolds/stardewvalley/new-project.yaml +184 -0
  454. package/scaffolds/stardewvalley/sequences/content-pack.yaml +31 -0
  455. package/scripts/mw-tool.mjs +184 -0
@@ -0,0 +1,680 @@
1
+ import { createHash } from "node:crypto";
2
+ import { promises as fs } from "node:fs";
3
+ import * as path from "node:path";
4
+ import { pathExists } from "../fsutil.js";
5
+ import { executePlan, planFile, planMkdir } from "../safety/index.js";
6
+ import { buildContext } from "../validate/context.js";
7
+ import { runValidators } from "../validate/run.js";
8
+ import { parseProject } from "../project/schema.js";
9
+ import { loadProject } from "../project/load.js";
10
+ import { expandBareString } from "../project/build-steps.js";
11
+ import { applyClaimTransition } from "../ledger/record.js";
12
+ import { loadClaims, saveClaims } from "../ledger/store.js";
13
+ import { loadTestPlanFile, saveTestPlan } from "../testplan/load.js";
14
+ import { testPlanRowSchema } from "../testplan/types.js";
15
+ import { lookupProbe } from "../testplan/registry.js";
16
+ import { getConverter } from "../text/converters/index.js";
17
+ import { templateContentFilename } from "../text/templates/cache.js";
18
+ import { authoringManifestSchema, templateRecordSchema, TEMPLATE_MANIFEST_FILE, AUTHORING_MANIFEST_SCHEMA_VERSION } from "../text/templates/types.js";
19
+ import { checkCollision } from "./collisions.js";
20
+ import { renderEmits } from "./emit.js";
21
+ import { collisionSlot, renderId } from "./ids.js";
22
+ import { emptyScope, interpolate, interpolateDeep, listInputItems, reindentBlock } from "./render.js";
23
+ import { DERIVED_ID_FORMS, NUMERIC_ID_FORMS } from "./types.js";
24
+ import { bridgeGameSchema } from "../bridge/types.js";
25
+ function toPosix(p) {
26
+ return p.split(path.sep).join("/");
27
+ }
28
+ function slugify(raw) {
29
+ return raw
30
+ .trim()
31
+ .toLowerCase()
32
+ .replace(/[^a-z0-9]+/g, "-")
33
+ .replace(/-+/g, "-")
34
+ .replace(/^-+|-+$/g, "");
35
+ }
36
+ function computeVersion64(version) {
37
+ if (!version)
38
+ return undefined;
39
+ const parts = version.split(".").map((p) => Number(p));
40
+ if (parts.length < 3 || parts.slice(0, 3).some((p) => !Number.isInteger(p) || p < 0))
41
+ return undefined;
42
+ const [major, minor, revision] = parts;
43
+ const value = (BigInt(major) << 55n) | (BigInt(minor) << 47n) | (BigInt(revision) << 31n) | 1n;
44
+ return value.toString();
45
+ }
46
+ function projectScope(project) {
47
+ const out = { name: project.project.name, game: project.project.game };
48
+ const moduleName = project.project.name.replace(/[^A-Za-z0-9_]/g, "");
49
+ out.moduleName = /^[A-Za-z_]/.test(moduleName) ? moduleName : `Mod${moduleName}`;
50
+ if (project.project.version)
51
+ out.version = project.project.version;
52
+ const version64 = computeVersion64(project.project.version);
53
+ if (version64)
54
+ out.version64 = version64;
55
+ for (const [k, v] of Object.entries(project.project.identity ?? {}))
56
+ out[`identity.${k}`] = v;
57
+ return out;
58
+ }
59
+ function resolveInputValue(spec, given, optional) {
60
+ const raw = given[spec.name];
61
+ if (raw !== undefined)
62
+ return raw;
63
+ if (spec.type === "string" && spec.default !== undefined)
64
+ return spec.default;
65
+ if (spec.type === "number" && spec.default !== undefined)
66
+ return String(spec.default);
67
+ if (spec.type === "list" && spec.default !== undefined)
68
+ return spec.default;
69
+ if (optional)
70
+ return undefined;
71
+ throw new Error(`scaffold: missing required input "${spec.name}" (${spec.prompt})`);
72
+ }
73
+ function getPath(obj, dotted) {
74
+ return dotted.split(".").reduce((acc, seg) => {
75
+ if (Array.isArray(acc)) {
76
+ const idx = Number(seg);
77
+ if (Number.isInteger(idx))
78
+ return acc[idx];
79
+ return acc.find((el) => el && typeof el === "object" && el.name === seg);
80
+ }
81
+ if (acc && typeof acc === "object")
82
+ return acc[seg];
83
+ return undefined;
84
+ }, obj);
85
+ }
86
+ function sameBuildStepAddition(existing, addition) {
87
+ if (existing && typeof existing === "object") {
88
+ const o = existing;
89
+ if (typeof o.step === "string" &&
90
+ typeof o.glob === "string" &&
91
+ typeof addition.step === "string" &&
92
+ typeof addition.glob === "string" &&
93
+ o.step === addition.step &&
94
+ o.glob === addition.glob) {
95
+ return true;
96
+ }
97
+ }
98
+ return canonicalJson(existing) === canonicalJson(addition);
99
+ }
100
+ function canonicalJson(value) {
101
+ if (Array.isArray(value))
102
+ return `[${value.map(canonicalJson).join(",")}]`;
103
+ if (value && typeof value === "object") {
104
+ const o = value;
105
+ return `{${Object.keys(o)
106
+ .sort()
107
+ .filter((k) => o[k] !== undefined)
108
+ .map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`)
109
+ .join(",")}}`;
110
+ }
111
+ return JSON.stringify(value) ?? "null";
112
+ }
113
+ function stepMatchesBeforeTarget(entry, target, project) {
114
+ if (typeof entry === "string") {
115
+ if (entry === target)
116
+ return true;
117
+ try {
118
+ return expandBareString(entry, project).some((s) => s.step === target);
119
+ }
120
+ catch {
121
+ return false;
122
+ }
123
+ }
124
+ if (entry && typeof entry === "object") {
125
+ const o = entry;
126
+ return o.id === target || o.step === target;
127
+ }
128
+ return false;
129
+ }
130
+ function mergeBuildSteps(existing, additions, project) {
131
+ const result = [...existing];
132
+ const warnings = [];
133
+ for (const raw of additions) {
134
+ if (!raw || typeof raw !== "object") {
135
+ if (!result.some((entry) => JSON.stringify(entry) === JSON.stringify(raw)))
136
+ result.push(raw);
137
+ continue;
138
+ }
139
+ const { before, ...stepRest } = raw;
140
+ if (result.some((entry) => sameBuildStepAddition(entry, stepRest)))
141
+ continue;
142
+ if (typeof before === "string") {
143
+ const idx = result.findIndex((entry) => stepMatchesBeforeTarget(entry, before, project));
144
+ if (idx !== -1) {
145
+ result.splice(idx, 0, stepRest);
146
+ continue;
147
+ }
148
+ warnings.push(`build step "${String(stepRest.step ?? stepRest.id ?? "?")}" asked to be inserted before "${before}", but no ` +
149
+ `build.steps entry matches that step id or kind — it was appended to the end of build.steps instead. ` +
150
+ `If it must run before another step, move it by hand in ${"modwright.json"}.`);
151
+ }
152
+ result.push(stepRest);
153
+ }
154
+ return { steps: result, warnings };
155
+ }
156
+ export async function runScaffold(options) {
157
+ const { project, surface, definition, store, install, registry } = options;
158
+ const mode = options.mode ?? "dry-run";
159
+ const now = options.now ?? (() => new Date());
160
+ const by = options.by ?? "scaffold";
161
+ const skipped = [];
162
+ const lookup = async (kind, params) => {
163
+ if (!store)
164
+ return { kind, params, count: 0, rows: [], note: "no vanilla index open" };
165
+ return surface.index.query(store, kind, params);
166
+ };
167
+ const inputValues = {};
168
+ const oneOfMembers = new Set(definition.oneOf.flat());
169
+ for (const spec of definition.inputs) {
170
+ const resolved = resolveInputValue(spec, options.inputs, oneOfMembers.has(spec.name));
171
+ if (resolved === undefined)
172
+ continue;
173
+ inputValues[spec.name] = resolved;
174
+ if (spec.type === "enum" && !spec.values.includes(inputValues[spec.name])) {
175
+ throw new Error(`scaffold "${definition.id}": input "${spec.name}" must be one of ${spec.values.join(", ")}, got "${inputValues[spec.name]}".`);
176
+ }
177
+ if (spec.type === "string" && spec.pattern && !new RegExp(spec.pattern).test(inputValues[spec.name])) {
178
+ throw new Error(`scaffold "${definition.id}": input "${spec.name}" = "${inputValues[spec.name]}" does not match pattern ${spec.pattern}.`);
179
+ }
180
+ if (spec.type === "number") {
181
+ const n = Number(inputValues[spec.name]);
182
+ if (inputValues[spec.name].trim() === "" || !Number.isFinite(n)) {
183
+ throw new Error(`scaffold "${definition.id}": input "${spec.name}" must be a number, got "${inputValues[spec.name]}".`);
184
+ }
185
+ if ((spec.min !== undefined && n < spec.min) || (spec.max !== undefined && n > spec.max)) {
186
+ throw new Error(`scaffold "${definition.id}": input "${spec.name}" = ${n} is outside ${spec.min ?? "-inf"}..${spec.max ?? "inf"}.`);
187
+ }
188
+ }
189
+ if (spec.type === "path" && spec.mustExist) {
190
+ const abs = path.isAbsolute(inputValues[spec.name]) ? inputValues[spec.name] : path.join(project.root, inputValues[spec.name]);
191
+ if (!(await pathExists(abs))) {
192
+ throw new Error(`scaffold "${definition.id}": input "${spec.name}" names a path that does not exist: ${abs}.`);
193
+ }
194
+ }
195
+ if (spec.type === "list") {
196
+ const items = listInputItems(inputValues[spec.name]);
197
+ if (items.length < spec.minItems) {
198
+ throw new Error(`scaffold "${definition.id}": input "${spec.name}" needs at least ${spec.minItems} item(s) (one per line or semicolon-separated), got ${items.length}.`);
199
+ }
200
+ if (spec.itemPattern) {
201
+ const re = new RegExp(spec.itemPattern);
202
+ const bad = items.find((it) => !re.test(it));
203
+ if (bad !== undefined) {
204
+ throw new Error(`scaffold "${definition.id}": input "${spec.name}" item "${bad}" does not match pattern ${spec.itemPattern}.`);
205
+ }
206
+ }
207
+ }
208
+ if (spec.type === "string" && spec.reindent !== undefined) {
209
+ inputValues[spec.name] = reindentBlock(inputValues[spec.name], spec.reindent);
210
+ }
211
+ }
212
+ for (const group of definition.oneOf) {
213
+ const present = group.filter((name) => inputValues[name] !== undefined);
214
+ if (present.length !== 1) {
215
+ throw new Error(`scaffold "${definition.id}": exactly one of {${group.join(", ")}} must be provided` +
216
+ (present.length === 0 ? `, but none was.` : `, not ${present.length} (${present.join(", ")}).`));
217
+ }
218
+ }
219
+ const numberInputs = new Set(definition.inputs.filter((i) => i.type === "number").map((i) => i.name));
220
+ const listInputs = new Set(definition.inputs.filter((i) => i.type === "list").map((i) => i.name));
221
+ const numberIds = new Set(definition.ids.filter((i) => NUMERIC_ID_FORMS.has(i.form)).map((i) => i.name));
222
+ const scope = { ...emptyScope(), input: inputValues, project: projectScope(project), numberInputs, listInputs, numberIds };
223
+ scope.project.today = now().toISOString().slice(0, 10);
224
+ for (const spec of definition.inputs) {
225
+ if (spec.type !== "index-ref")
226
+ continue;
227
+ if (inputValues[spec.name] === undefined)
228
+ continue;
229
+ if (!store) {
230
+ const exposed = spec.requires.flatMap((r) => [r.expose, r.through?.expose].filter((e) => e !== undefined));
231
+ if (exposed.length > 0) {
232
+ throw new Error(`scaffold "${definition.id}": input "${spec.name}" needs the ${surface.id} vanilla index — its lookup exposes ${exposed.map((e) => `\${template.${e}}`).join(", ")} to the emitted files, so there is nothing to write without it. Run build_index first.`);
233
+ }
234
+ skipped.push({
235
+ id: `${definition.id}:input:${spec.name}`,
236
+ reason: `no vanilla index built for ${surface.id} — cannot verify "${inputValues[spec.name]}" or its requires[]; the value is used as given (run build_index first to check it).`,
237
+ });
238
+ continue;
239
+ }
240
+ const params = {};
241
+ for (const [k, v] of Object.entries(spec.lookup.params))
242
+ params[k] = interpolate(v, scope);
243
+ const res = await lookup(spec.lookup.kind, params);
244
+ if (res.count === 0) {
245
+ throw new Error(`scaffold "${definition.id}": input "${spec.name}" ("${inputValues[spec.name]}") does not resolve via lookup "${spec.lookup.kind}".`);
246
+ }
247
+ for (const req of spec.requires) {
248
+ const value = getPath(res.rows[0], req.path);
249
+ if (req.expose && value !== undefined && value !== null && value !== "") {
250
+ scope.template[req.expose] = String(value);
251
+ }
252
+ if (req.through) {
253
+ if (value === undefined || value === null || value === "")
254
+ continue;
255
+ const throughParams = {};
256
+ for (const [k, v] of Object.entries(req.through.params ?? {}))
257
+ throughParams[k] = interpolate(v, scope);
258
+ const throughRes = await lookup(req.through.kind, { ...throughParams, [req.through.param]: String(value) });
259
+ if (throughRes.count === 0) {
260
+ throw new Error(`scaffold "${definition.id}": ${req.reason}`);
261
+ }
262
+ const throughValue = getPath(throughRes.rows[0], req.through.path);
263
+ const throughMatches = req.matches !== undefined ? new RegExp(req.matches).test(String(throughValue ?? "")) : throughValue !== undefined;
264
+ if (!throughMatches) {
265
+ throw new Error(`scaffold "${definition.id}": ${req.reason}`);
266
+ }
267
+ if (req.through.expose && throughValue !== undefined && throughValue !== null && throughValue !== "") {
268
+ scope.template[req.through.expose] = String(throughValue);
269
+ }
270
+ continue;
271
+ }
272
+ const matches = req.matches !== undefined ? new RegExp(req.matches).test(String(value ?? "")) : value !== undefined;
273
+ if (!matches) {
274
+ throw new Error(`scaffold "${definition.id}": ${req.reason}`);
275
+ }
276
+ }
277
+ }
278
+ for (const spec of definition.inputs) {
279
+ if (spec.type !== "template-record")
280
+ continue;
281
+ if (inputValues[spec.name] === undefined)
282
+ continue;
283
+ const given = inputValues[spec.name];
284
+ const dir = path.isAbsolute(given) ? given : path.join(project.root, given);
285
+ const recordPath = path.join(dir, "record.json");
286
+ let raw;
287
+ try {
288
+ raw = JSON.parse(await fs.readFile(recordPath, "utf8"));
289
+ }
290
+ catch (err) {
291
+ throw new Error(`scaffold "${definition.id}": input "${spec.name}" names a template-record directory with no readable record.json: ${dir} (${err.message}).`);
292
+ }
293
+ const parsed = templateRecordSchema.safeParse(raw);
294
+ if (!parsed.success) {
295
+ throw new Error(`scaffold "${definition.id}": input "${spec.name}"'s record.json at ${recordPath} is not a valid TemplateRecord: ${parsed.error.message}.`);
296
+ }
297
+ const record = parsed.data;
298
+ const contentExt = record.conversion ? getConverter(record.conversion).to : path.extname(record.sourcePath);
299
+ const contentPath = path.join(dir, templateContentFilename(record.sourcePath, contentExt));
300
+ if (!(await pathExists(contentPath))) {
301
+ throw new Error(`scaffold "${definition.id}": input "${spec.name}"'s cached content is missing: ${contentPath}.`);
302
+ }
303
+ scope.template[`${spec.name}.ref`] = record.ref;
304
+ scope.template[`${spec.name}.kind`] = record.kind;
305
+ scope.template[`${spec.name}.provenance`] = record.provenance;
306
+ scope.template[`${spec.name}.permission`] = record.permission;
307
+ scope.template[`${spec.name}.sha256`] = record.sha256;
308
+ scope.template[`${spec.name}.sourcePath`] = record.sourcePath;
309
+ scope.template[`${spec.name}.contentPath`] = contentPath;
310
+ if (spec.expectKind) {
311
+ const by = scope.template[spec.expectKind.by];
312
+ if (by === undefined || by === "") {
313
+ skipped.push({
314
+ id: `${definition.id}:input:${spec.name}:expectKind`,
315
+ reason: `\${template.${spec.expectKind.by}} is unset (nothing exposed it — the base record declares no such value), so the template's kind "${record.kind}" was not checked against the base.`,
316
+ });
317
+ }
318
+ else {
319
+ const pattern = Object.hasOwn(spec.expectKind.map, by) ? spec.expectKind.map[by] : undefined;
320
+ if (pattern === undefined) {
321
+ skipped.push({
322
+ id: `${definition.id}:input:${spec.name}:expectKind`,
323
+ reason: `no expected template kind is recorded for base type "${by}", so the template's kind "${record.kind}" was not checked.`,
324
+ });
325
+ }
326
+ else if (!new RegExp(pattern).test(record.kind)) {
327
+ throw new Error(`scaffold "${definition.id}": input "${spec.name}"'s record is labelled kind "${record.kind}" (${record.sourcePath}), but \${template.${spec.expectKind.by}} is "${by}", which needs a template whose kind matches /${pattern}/. ${spec.expectKind.reason}`);
328
+ }
329
+ }
330
+ }
331
+ }
332
+ const generatedIds = [];
333
+ const projectIdentity = project.project.identity?.uuid ?? project.project.name;
334
+ for (const idSpec of definition.ids) {
335
+ const { report: idReport, unresolvedSkips } = await allocateOneId(idSpec, scope, {
336
+ projectIdentity,
337
+ scaffoldId: definition.id,
338
+ project,
339
+ surface,
340
+ store,
341
+ install,
342
+ lookup,
343
+ force: Boolean(options.force),
344
+ });
345
+ generatedIds.push(idReport);
346
+ scope.ids[idSpec.name] = idReport.value;
347
+ for (const s of unresolvedSkips) {
348
+ skipped.push({ id: `${definition.id}:id:${idSpec.name}:${s.check}`, reason: s.reason });
349
+ }
350
+ }
351
+ const rendered = await renderEmits(definition, scope, project.root);
352
+ for (const g of rendered.guardSkipped) {
353
+ skipped.push({
354
+ id: `${definition.id}:emit:${g.op}${g.path ? `:${path.relative(project.root, g.path).split(path.sep).join("/")}` : ""}`,
355
+ reason: `not written — ${g.reason}`,
356
+ });
357
+ }
358
+ const rowProbes = definition.testRows.map((row) => buildTestRowProbes(definition, row, scope));
359
+ const operations = [
360
+ ...[...rendered.mkdirs].map((abs) => planMkdir(abs)),
361
+ ...[...rendered.content.entries()]
362
+ .filter(([abs]) => !rendered.mkdirs.has(abs))
363
+ .map(([abs, text]) => planFile(abs, text)),
364
+ ];
365
+ const patch = rendered.projectPatch;
366
+ const hasPatch = patch.sources.length + patch.steps.length + patch.deployTargets.length + patch.dependencies.length + Object.keys(patch.toolchain).length > 0;
367
+ if (hasPatch) {
368
+ const projectFilePath = project.file;
369
+ const pendingProjectText = rendered.content.get(projectFilePath);
370
+ const baseJson = pendingProjectText
371
+ ? JSON.parse(pendingProjectText)
372
+ : JSON.parse(await fs.readFile(projectFilePath, "utf8"));
373
+ if (patch.sources.length) {
374
+ const existingSources = (baseJson.sources ?? []);
375
+ const merged = [...existingSources];
376
+ for (const raw of patch.sources) {
377
+ const dup = merged.some((s) => s.id === raw.id || (typeof raw.path === "string" && s.path === raw.path));
378
+ if (!dup)
379
+ merged.push(raw);
380
+ }
381
+ baseJson.sources = merged;
382
+ }
383
+ if (patch.steps.length) {
384
+ const build = (baseJson.build ?? {});
385
+ const merged = mergeBuildSteps((build.steps ?? []), patch.steps, project.project);
386
+ build.steps = merged.steps;
387
+ for (const [n, reason] of merged.warnings.entries()) {
388
+ skipped.push({ id: `${definition.id}:patch-project:steps[${n}]`, reason });
389
+ }
390
+ if (build.outDir === undefined && patch.outDir !== undefined)
391
+ build.outDir = patch.outDir;
392
+ baseJson.build = build;
393
+ }
394
+ if (patch.deployTargets.length) {
395
+ const deploy = (baseJson.deploy ?? { targets: [] });
396
+ const existingTargets = (deploy.targets ?? []);
397
+ const fromKey = (t) => typeof t.from === "string" && t.into === undefined ? (t.from.split(/[\\/]/).pop() ?? t.from) : String(t.from);
398
+ const sameTarget = (a, b) => a.rootId === b.rootId && fromKey(a) === fromKey(b) && (a.into ?? undefined) === (b.into ?? undefined) && (a.variant ?? undefined) === (b.variant ?? undefined);
399
+ const merged = [...existingTargets];
400
+ for (const raw of patch.deployTargets) {
401
+ if (!merged.some((t) => sameTarget(t, raw)))
402
+ merged.push(raw);
403
+ }
404
+ deploy.targets = merged;
405
+ baseJson.deploy = deploy;
406
+ }
407
+ if (patch.dependencies.length)
408
+ baseJson.dependencies = [...(baseJson.dependencies ?? []), ...patch.dependencies];
409
+ if (Object.keys(patch.toolchain).length) {
410
+ const existing = (baseJson.toolchain ?? {});
411
+ baseJson.toolchain = { ...patch.toolchain, ...existing };
412
+ }
413
+ try {
414
+ parseProject(baseJson);
415
+ }
416
+ catch (err) {
417
+ throw new Error(`scaffold "${definition.id}": patch-project would leave ${path.basename(projectFilePath)} invalid: ${err.message}`);
418
+ }
419
+ const text = `${JSON.stringify(baseJson, null, 2)}\n`;
420
+ if (rendered.content.has(projectFilePath)) {
421
+ rendered.content.set(projectFilePath, text);
422
+ const existingOpIdx = operations.findIndex((op) => op.kind !== "mkdir" && "path" in op && op.path === projectFilePath);
423
+ if (existingOpIdx !== -1)
424
+ operations[existingOpIdx] = planFile(projectFilePath, text);
425
+ const renderedOp = rendered.ops.find((op) => op.absPath === projectFilePath && op.content !== undefined);
426
+ if (renderedOp) {
427
+ renderedOp.content = text;
428
+ renderedOp.sizeBytes = Buffer.byteLength(text, "utf8");
429
+ }
430
+ }
431
+ else {
432
+ rendered.content.set(projectFilePath, text);
433
+ operations.push(planFile(projectFilePath, text));
434
+ }
435
+ }
436
+ const write = await executePlan({ purpose: `scaffold ${definition.id} in ${project.project.name}`, operations, backupRoot: path.join(project.root, ".modwright", "backups") }, {
437
+ mode,
438
+ now,
439
+ confidence: {
440
+ level: "medium",
441
+ assumptions: ["Emitted content is built from this scaffold's own templates, not verified against a real toolchain."],
442
+ },
443
+ });
444
+ const files = rendered.ops.map((op) => {
445
+ const rel = toPosix(path.relative(project.root, op.absPath));
446
+ const manifestEntry = rendered.authoringManifestEntries.find((e) => e.path === rel);
447
+ return {
448
+ path: rel,
449
+ op: op.kind,
450
+ sizeBytes: op.sizeBytes,
451
+ sha256: op.content ? createHash("sha256").update(op.content).digest("hex") : undefined,
452
+ derivedFrom: manifestEntry?.derivedFrom,
453
+ rewrittenPaths: manifestEntry?.rewrittenPaths,
454
+ reason: op.reason,
455
+ };
456
+ });
457
+ const manual = definition.manual.map((m) => ({
458
+ title: interpolate(m.title, scope),
459
+ steps: m.steps.map((s) => interpolate(s, scope)),
460
+ expected: interpolate(m.expected, scope),
461
+ }));
462
+ const preconditions = manual.map((m) => m.title);
463
+ const primarySpec = definition.inputs.find((i) => i.type === "string" || i.type === "index-ref");
464
+ const primaryInputSlug = slugify(primarySpec ? inputValues[primarySpec.name] : project.project.name);
465
+ const claimId = `${slugify(project.project.name)}.${primaryInputSlug}.${interpolate(definition.claim.idSuffix, scope)}`;
466
+ const claimText = interpolate(definition.claim.claim, scope);
467
+ const entryNames = generatedIds.filter((g) => ["tweak-record", "slug", "map-key"].includes(g.form)).map((g) => g.value);
468
+ const testRows = definition.testRows.map((row, i) => {
469
+ const base = {
470
+ id: `${claimId}.${row.gate}.${i}`,
471
+ gate: row.gate,
472
+ title: interpolate(row.title, scope),
473
+ preconditions,
474
+ steps: [],
475
+ expected: interpolate(row.expected, scope),
476
+ claim: claimId,
477
+ status: "authored",
478
+ };
479
+ const probes = rowProbes[i];
480
+ if (probes)
481
+ return testPlanRowSchema.parse({ ...base, manual: false, probe: { allowMutate: false, probes } });
482
+ return testPlanRowSchema.parse({ ...base, manual: true });
483
+ });
484
+ const validatorIds = new Set([...definition.satisfies, ...registry.cheap(surface.id).map((v) => v.id)]);
485
+ const report = {
486
+ scaffold: definition.id,
487
+ game: surface.id,
488
+ mode,
489
+ project: project.project.name,
490
+ inputs: inputValues,
491
+ generatedIds,
492
+ files,
493
+ write,
494
+ satisfies: definition.satisfies,
495
+ testRows,
496
+ manual,
497
+ skipped,
498
+ confidence: write.confidence,
499
+ next: write.next,
500
+ };
501
+ if (mode === "dry-run")
502
+ return report;
503
+ if (write.applied) {
504
+ const reload = rendered.content.has(project.file) || project.sources.some((s) => !s.exists);
505
+ const validatedProject = reload ? await loadProject(project.file) : project;
506
+ const ctx = buildContext({ project: validatedProject, surface, install, store });
507
+ const fullReport = await runValidators(registry, ctx, {});
508
+ const ran = fullReport.ran.filter((id) => validatorIds.has(id));
509
+ const skippedFiltered = fullReport.skipped.filter((s) => validatorIds.has(s.id));
510
+ const findings = fullReport.findings.filter((f) => validatorIds.has(f.validatorId));
511
+ const unsuppressedBlocking = findings.filter((f) => f.severity === "block" && !f.suppressedBy);
512
+ const satisfiesSet = new Set(definition.satisfies);
513
+ const skippedSatisfies = skippedFiltered.filter((s) => satisfiesSet.has(s.id));
514
+ const cleanReasons = [
515
+ ...unsuppressedBlocking.map((f) => `${f.validatorId}: ${f.message}`),
516
+ ...skippedSatisfies.map((s) => `${s.id} (listed in satisfies) skipped: ${s.reason}`),
517
+ ];
518
+ report.validation = {
519
+ ran,
520
+ skipped: skippedFiltered,
521
+ findings,
522
+ clean: cleanReasons.length === 0,
523
+ cleanReasons,
524
+ };
525
+ const stamp = now().toISOString().replace(/[:.]/g, "-");
526
+ const manifest = {
527
+ schemaVersion: 1,
528
+ scaffold: definition.id,
529
+ inputs: inputValues,
530
+ generatedIds: generatedIds.map((g) => ({ name: g.name, slot: g.slot, form: g.form, value: g.value })),
531
+ files,
532
+ satisfies: definition.satisfies,
533
+ validation: { ran, skipped: skippedFiltered, findings },
534
+ claimId,
535
+ testRowIds: testRows.map((r) => r.id),
536
+ manual,
537
+ };
538
+ const manifestDir = path.join(project.root, ".modwright", "scaffolds");
539
+ const manifestPath = path.join(manifestDir, `${stamp}.json`);
540
+ await executePlan({
541
+ purpose: `scaffold ${definition.id} manifest`,
542
+ operations: [planMkdir(manifestDir), planFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)],
543
+ backupRoot: path.join(project.root, ".modwright", "backups"),
544
+ }, { mode: "apply", now, confidence: { level: "high", assumptions: [] } });
545
+ report.manifestPath = manifestPath;
546
+ if (rendered.authoringManifestEntries.length > 0) {
547
+ const templateManifestPath = path.join(project.root, ...TEMPLATE_MANIFEST_FILE.split("/"));
548
+ let existingEntries = [];
549
+ try {
550
+ const raw = JSON.parse(await fs.readFile(templateManifestPath, "utf8"));
551
+ const parsed = authoringManifestSchema.safeParse(raw);
552
+ if (parsed.success)
553
+ existingEntries = parsed.data.entries;
554
+ }
555
+ catch {
556
+ }
557
+ const recorded = new Set(existingEntries.map((e) => `${e.path}\0${e.templateSha256}\0${e.authoredSha256}`));
558
+ const added = rendered.authoringManifestEntries.filter((e) => !recorded.has(`${e.path}\0${e.templateSha256}\0${e.authoredSha256}`));
559
+ const nextManifest = { schemaVersion: AUTHORING_MANIFEST_SCHEMA_VERSION, entries: [...existingEntries, ...added] };
560
+ if (added.length > 0)
561
+ await executePlan({
562
+ purpose: `scaffold ${definition.id}: record template-derived files`,
563
+ operations: [planMkdir(path.dirname(templateManifestPath)), planFile(templateManifestPath, `${JSON.stringify(nextManifest, null, 2)}\n`)],
564
+ backupRoot: path.join(project.root, ".modwright", "backups"),
565
+ }, { mode: "apply", now, confidence: { level: "high", assumptions: [] } });
566
+ }
567
+ const { claims } = await loadClaims(project.root);
568
+ const result = applyClaimTransition({
569
+ claims,
570
+ id: claimId,
571
+ to: "authored",
572
+ tool: "ledger",
573
+ by,
574
+ now,
575
+ claimText,
576
+ scope: { files: files.map((f) => f.path), entries: entryNames },
577
+ note: manifestPath,
578
+ });
579
+ if (result.ok) {
580
+ await saveClaims(project.root, result.claims, { mode: "apply", now });
581
+ }
582
+ report.ledger = { claimId, claim: result.claim, written: result.ok, refusal: result.refusal };
583
+ const bridgeGame = bridgeGameSchema.safeParse(definition.game);
584
+ if (testRows.length > 0 && !bridgeGame.success) {
585
+ skipped.push({ id: `${definition.id}:test-plan`, reason: `tests/authored.yaml not written: ${definition.game} has no bridge, so no test plan can run its rows; they are listed in the report as manual steps.` });
586
+ }
587
+ else if (testRows.length > 0 && bridgeGame.success) {
588
+ const testPlanPath = path.join(project.root, "tests", "authored.yaml");
589
+ const existing = await loadTestPlanFile(testPlanPath);
590
+ const plan = existing.plan ?? {
591
+ planId: "authored",
592
+ title: "Authored rows (scaffold-proposed)",
593
+ game: bridgeGame.data,
594
+ fixture: project.project.testProfile
595
+ ? { save: project.project.testProfile.saveName, notes: project.project.testProfile.notes }
596
+ : undefined,
597
+ rows: [],
598
+ };
599
+ const fresh = new Map(testRows.map((r) => [r.id, r]));
600
+ const kept = plan.rows.map((r) => {
601
+ const next = fresh.get(r.id);
602
+ if (next)
603
+ fresh.delete(r.id);
604
+ return next ?? r;
605
+ });
606
+ plan.rows = [...kept, ...fresh.values()];
607
+ await saveTestPlan(testPlanPath, plan, { mode: "apply", now, backupRoot: path.join(project.root, ".modwright", "backups") });
608
+ report.testPlanPath = testPlanPath;
609
+ }
610
+ }
611
+ return report;
612
+ }
613
+ function buildTestRowProbes(definition, row, scope) {
614
+ if (!row.probes || !row.probes.every((p) => lookupProbe(p.kind)?.status === "attested"))
615
+ return undefined;
616
+ return row.probes.map((p, j) => buildRowProbe(definition, row.gate, p, j, scope));
617
+ }
618
+ function buildRowProbe(definition, gate, spec, index, scope) {
619
+ const entry = lookupProbe(spec.kind);
620
+ if (!entry) {
621
+ throw new Error(`scaffold "${definition.id}": test row "${gate}" names unknown probe kind "${spec.kind}".`);
622
+ }
623
+ if (entry.game !== definition.game) {
624
+ throw new Error(`scaffold "${definition.id}" (${definition.game}): test row "${gate}" names probe kind "${spec.kind}", which the registry lists as a ${entry.game} probe — run_tests refuses a row whose probe is for another game (core/testplan/check.ts), so it would never run.`);
625
+ }
626
+ const params = interpolateDeep(spec.params, scope);
627
+ const parsedParams = entry.paramsSchema.safeParse(params);
628
+ if (!parsedParams.success) {
629
+ throw new Error(`scaffold "${definition.id}": test row "${gate}" probe "${spec.kind}" params do not match its registry schema (${entry.citation}): ${parsedParams.error.message}`);
630
+ }
631
+ const expect = spec.expect ? interpolateDeep(spec.expect, scope) : undefined;
632
+ return {
633
+ id: `${gate}.${index}`,
634
+ kind: spec.kind,
635
+ context: entry.context,
636
+ effect: entry.effect,
637
+ params: parsedParams.data,
638
+ ...(expect ? { expect } : {}),
639
+ };
640
+ }
641
+ async function allocateOneId(idSpec, scope, ctx) {
642
+ let slot = interpolate(idSpec.slot, scope);
643
+ if (DERIVED_ID_FORMS.has(idSpec.form)) {
644
+ const value = renderId(idSpec.form, ctx.projectIdentity, ctx.scaffoldId, slot, idSpec.fractionBits);
645
+ return { report: { name: idSpec.name, slot, form: idSpec.form, value, collisions: [] }, unresolvedSkips: [] };
646
+ }
647
+ let attempt = 0;
648
+ for (;;) {
649
+ const value = renderId(idSpec.form, ctx.projectIdentity, ctx.scaffoldId, slot);
650
+ const result = await checkCollision({
651
+ idSpec,
652
+ value,
653
+ project: ctx.project,
654
+ surface: ctx.surface,
655
+ store: ctx.store,
656
+ install: ctx.install,
657
+ lookup: ctx.lookup,
658
+ });
659
+ if (result.hits.length === 0 || ctx.force) {
660
+ return {
661
+ report: {
662
+ name: idSpec.name,
663
+ slot,
664
+ form: idSpec.form,
665
+ value,
666
+ collisions: result.hits.map((h) => ({ check: h.check, source: h.source, detail: h.detail })),
667
+ },
668
+ unresolvedSkips: result.skipped,
669
+ };
670
+ }
671
+ if (idSpec.form === "tweak-record" || idSpec.form === "slug" || idSpec.form === "map-key") {
672
+ throw new Error(`scaffold: generated id "${idSpec.name}" = "${value}" already exists (${result.hits.map((h) => h.detail).join("; ")}). Choose a different name.`);
673
+ }
674
+ attempt++;
675
+ if (attempt > 20) {
676
+ throw new Error(`scaffold: could not allocate a collision-free id for "${idSpec.name}" after ${attempt} attempts.`);
677
+ }
678
+ slot = collisionSlot(interpolate(idSpec.slot, scope), attempt);
679
+ }
680
+ }