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,768 @@
1
+ import * as path from "node:path";
2
+ import * as YAML from "yaml";
3
+ import { walk } from "../../../core/fsutil.js";
4
+ import { parseTweakXLYaml } from "../index/parsers/tweakxl-yaml.js";
5
+ const GAME = "cyberpunk2077";
6
+ const GROUPS = ["tweaks"];
7
+ const MOD_TYPES = ["gameplay-tweak", "weapon", "item", "loot"];
8
+ function isYamlPath(p) {
9
+ return /\.ya?ml$/i.test(p);
10
+ }
11
+ function isWithin(parent, child) {
12
+ const rel = path.relative(parent, child);
13
+ return rel === "" || (!(rel === ".." || rel.startsWith(`..${path.sep}`)) && !path.isAbsolute(rel));
14
+ }
15
+ function isUnderTweaksSource(absPath, project) {
16
+ return project.sources.some((s) => s.kind === "tweaks" && s.exists && isWithin(s.absPath, absPath));
17
+ }
18
+ const APPLIES_DESC = 'every .yaml/.yml file under a source of kind "tweaks"';
19
+ function appliesToYaml(target, project) {
20
+ return target.kind === "file" && isYamlPath(target.path) && isUnderTweaksSource(target.path, project);
21
+ }
22
+ async function findRecordsInFile(ctx, file) {
23
+ let text;
24
+ try {
25
+ text = await ctx.readText(file);
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ try {
31
+ return parseTweakXLYaml(text, file);
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ const projectTweakDataCache = new WeakMap();
38
+ async function collectTweaksSourceFiles(project) {
39
+ const files = [];
40
+ for (const source of project.sources) {
41
+ if (source.kind !== "tweaks" || !source.exists)
42
+ continue;
43
+ if (isYamlPath(source.absPath)) {
44
+ files.push(source.absPath);
45
+ continue;
46
+ }
47
+ const walked = await walk(source.absPath, { limit: 20_000 });
48
+ for (const rel of walked.files) {
49
+ if (isYamlPath(rel))
50
+ files.push(path.join(source.absPath, rel));
51
+ }
52
+ }
53
+ files.sort((a, b) => {
54
+ const an = path.basename(a);
55
+ const bn = path.basename(b);
56
+ return an < bn ? -1 : an > bn ? 1 : 0;
57
+ });
58
+ return files;
59
+ }
60
+ function getProjectTweakData(ctx) {
61
+ const cached = projectTweakDataCache.get(ctx.project);
62
+ if (cached)
63
+ return cached;
64
+ const promise = (async () => {
65
+ const files = await collectTweaksSourceFiles(ctx.project);
66
+ const namesDefined = new Set();
67
+ const allRecords = [];
68
+ for (const file of files) {
69
+ const records = await findRecordsInFile(ctx, file);
70
+ if (!records)
71
+ continue;
72
+ for (const r of records) {
73
+ namesDefined.add(r.name);
74
+ allRecords.push(r);
75
+ }
76
+ }
77
+ return { files, namesDefined, allRecords };
78
+ })();
79
+ projectTweakDataCache.set(ctx.project, promise);
80
+ return promise;
81
+ }
82
+ function finding(validatorId, severity, file, message, evidence, extra = {}) {
83
+ return { validatorId, severity, file, message, evidence, ...extra };
84
+ }
85
+ const YAML_LINT_ID = "cp2077.tweaks.yaml-lint";
86
+ const YAML_LINT_EVIDENCE = [
87
+ "cp2077.tweakdb.melee.tweakxl-log-required-after-yaml-changes",
88
+ ];
89
+ export const yamlLintValidator = {
90
+ id: YAML_LINT_ID,
91
+ game: GAME,
92
+ groups: GROUPS,
93
+ modTypes: MOD_TYPES,
94
+ summary: "Every .yaml under a tweaks source parses as YAML: no tab indentation, no duplicate mapping keys.",
95
+ appliesTo: appliesToYaml,
96
+ appliesToDescription: APPLIES_DESC,
97
+ needs: { project: true },
98
+ cost: "edit",
99
+ severity: "block",
100
+ evidence: YAML_LINT_EVIDENCE,
101
+ async run(ctx) {
102
+ if (ctx.target.kind !== "file")
103
+ return [];
104
+ const file = ctx.target.path;
105
+ let text;
106
+ try {
107
+ text = await ctx.readText(file);
108
+ }
109
+ catch (err) {
110
+ return [finding(YAML_LINT_ID, "block", file, `Could not read this file: ${err.message}`, YAML_LINT_EVIDENCE)];
111
+ }
112
+ const lineCounter = new YAML.LineCounter();
113
+ const doc = YAML.parseDocument(text, { lineCounter });
114
+ return doc.errors.map((err) => {
115
+ const pos = err.pos ? lineCounter.linePos(err.pos[0]) : undefined;
116
+ return finding(YAML_LINT_ID, "block", file, err.message.split("\n")[0] ?? err.message, YAML_LINT_EVIDENCE, {
117
+ line: pos?.line,
118
+ column: pos?.col,
119
+ });
120
+ });
121
+ },
122
+ };
123
+ const BASE_EXISTS_ID = "cp2077.tweaks.base-exists";
124
+ const BASE_EXISTS_EVIDENCE = [
125
+ "cp2077.process.tweakdb-ground-truth",
126
+ "cp2077.tweakdb.melee.attack-records-live-in-per-family-packages",
127
+ ];
128
+ export const baseExistsValidator = {
129
+ id: BASE_EXISTS_ID,
130
+ game: GAME,
131
+ groups: GROUPS,
132
+ modTypes: MOD_TYPES,
133
+ summary: "Every $base: value resolves to a record in the vanilla index or one this project's own tweak files define.",
134
+ appliesTo: appliesToYaml,
135
+ appliesToDescription: APPLIES_DESC,
136
+ needs: { project: true, index: true },
137
+ cost: "edit",
138
+ severity: "block",
139
+ evidence: BASE_EXISTS_EVIDENCE,
140
+ async run(ctx) {
141
+ if (ctx.target.kind !== "file")
142
+ return [];
143
+ const file = ctx.target.path;
144
+ const records = await findRecordsInFile(ctx, file);
145
+ if (!records)
146
+ return [];
147
+ const { namesDefined } = await getProjectTweakData(ctx);
148
+ const modDeps = (ctx.project.project.dependencies ?? []).filter((d) => d.kind === "mod");
149
+ const out = [];
150
+ for (const rec of records) {
151
+ if (!rec.base || namesDefined.has(rec.base))
152
+ continue;
153
+ const res = await ctx.lookup("record", { name: rec.base });
154
+ if (res.count > 0)
155
+ continue;
156
+ if (modDeps.length > 0) {
157
+ out.push(finding(BASE_EXISTS_ID, "warn", file, `$base: ${rec.base} (used by ${rec.name}) does not resolve in the vanilla index or this project's own tweak files. It may be defined by a declared mod dependency (${modDeps.map((d) => d.id).join(", ")}) — if not, this is a typo and TweakXL drops the whole record silently, logging only to TweakXL-<timestamp>.log.`, BASE_EXISTS_EVIDENCE, { line: rec.line }));
158
+ }
159
+ else {
160
+ out.push(finding(BASE_EXISTS_ID, "block", file, `$base: ${rec.base} (used by ${rec.name}) does not exist in the vanilla index or in this project's own tweak files. TweakXL drops the whole record and logs only to TweakXL-<timestamp>.log.`, BASE_EXISTS_EVIDENCE, { line: rec.line }));
161
+ }
162
+ }
163
+ return out;
164
+ },
165
+ };
166
+ const TYPE_KNOWN_ID = "cp2077.tweaks.type-known";
167
+ const TYPE_KNOWN_EVIDENCE = ["ModWright research"];
168
+ export const typeKnownValidator = {
169
+ id: TYPE_KNOWN_ID,
170
+ game: GAME,
171
+ groups: GROUPS,
172
+ modTypes: MOD_TYPES,
173
+ summary: "Every $type: value names a type attested in the vanilla corpus, as a record $type or an inline-struct $type.",
174
+ appliesTo: appliesToYaml,
175
+ appliesToDescription: APPLIES_DESC,
176
+ needs: { index: true },
177
+ cost: "edit",
178
+ severity: "block",
179
+ evidence: TYPE_KNOWN_EVIDENCE,
180
+ async run(ctx) {
181
+ if (ctx.target.kind !== "file")
182
+ return [];
183
+ const file = ctx.target.path;
184
+ const records = await findRecordsInFile(ctx, file);
185
+ if (!records)
186
+ return [];
187
+ const out = [];
188
+ for (const rec of records) {
189
+ if (!rec.type)
190
+ continue;
191
+ const res = await ctx.lookup("type", { type: rec.type });
192
+ if (res.count > 0)
193
+ continue;
194
+ out.push(finding(TYPE_KNOWN_ID, "block", file, `$type: ${rec.type} (on ${rec.name}) is not attested anywhere in the vanilla corpus, as a record $type or an inline-struct $type.`, TYPE_KNOWN_EVIDENCE, { line: rec.line }));
195
+ }
196
+ return out;
197
+ },
198
+ };
199
+ const APPEND_LIST_ID = "cp2077.tweaks.append-on-list-flat";
200
+ const APPEND_LIST_EVIDENCE = [
201
+ "cp2077.equipment-ex.slots.placementslots-append-syntax",
202
+ "cp2077.tweakdb.melee.iconic-quality-pattern-confirmed-across-mods",
203
+ "cp2077.loot.debug-recipe-list-registration",
204
+ ];
205
+ async function chainFlatKind(ctx, baseName, flatName) {
206
+ const res = await ctx.lookup("chain", { name: baseName });
207
+ const row = res.rows[0];
208
+ return row?.flats?.find((f) => f.name === flatName)?.kind;
209
+ }
210
+ function isListKind(kind) {
211
+ return kind === "array" || kind.endsWith("[]");
212
+ }
213
+ export const appendOnListFlatValidator = {
214
+ id: APPEND_LIST_ID,
215
+ game: GAME,
216
+ groups: GROUPS,
217
+ modTypes: MOD_TYPES,
218
+ summary: "!append / !append-once / !remove are used only on a flat whose effective type on the $base chain is an array.",
219
+ appliesTo: appliesToYaml,
220
+ appliesToDescription: APPLIES_DESC,
221
+ needs: { index: true },
222
+ cost: "edit",
223
+ severity: "block",
224
+ evidence: APPEND_LIST_EVIDENCE,
225
+ async run(ctx) {
226
+ if (ctx.target.kind !== "file")
227
+ return [];
228
+ const file = ctx.target.path;
229
+ const records = await findRecordsInFile(ctx, file);
230
+ if (!records)
231
+ return [];
232
+ const { allRecords } = await getProjectTweakData(ctx);
233
+ const out = [];
234
+ for (const rec of records) {
235
+ for (const flat of rec.flats) {
236
+ if (flat.op === "set")
237
+ continue;
238
+ const knownKind = rec.base ? await chainFlatKind(ctx, rec.base, flat.name) : undefined;
239
+ if (knownKind !== undefined) {
240
+ if (!isListKind(knownKind)) {
241
+ out.push(finding(APPEND_LIST_ID, "block", file, `"${flat.name}" is tagged !${flat.op} on ${rec.name}, but on its $base chain this flat is a ${knownKind}, not a list. TweakXL's append/remove tags only make sense on an array flat.`, APPEND_LIST_EVIDENCE, { line: rec.line }));
242
+ }
243
+ continue;
244
+ }
245
+ const declaredArrayElsewhere = allRecords.some((r) => r !== rec && r.flats.some((f) => f.name === flat.name && f.op === "set" && f.kind === "array"));
246
+ if (!declaredArrayElsewhere) {
247
+ out.push(finding(APPEND_LIST_ID, "info", file, `Cannot tell whether "${flat.name}" (on ${rec.name}) is a list flat: its $base chain (${rec.base ?? "none — this record has no $base"}) is not in the vanilla index, and no plain array declaration of this flat exists elsewhere in this project.`, APPEND_LIST_EVIDENCE, { line: rec.line }));
248
+ }
249
+ }
250
+ }
251
+ return out;
252
+ },
253
+ };
254
+ const INSTANCES_ID = "cp2077.tweaks.instances-resolve";
255
+ const INSTANCES_EVIDENCE = ["ModWright research"];
256
+ const VAR_RE = /\$\(([^)]+)\)/g;
257
+ function collectVarNamesFromText(text, out) {
258
+ for (const m of text.matchAll(VAR_RE))
259
+ out.add(m[1]);
260
+ }
261
+ function collectVarNamesFromNode(node, out) {
262
+ if (YAML.isScalar(node)) {
263
+ if (typeof node.value === "string")
264
+ collectVarNamesFromText(node.value, out);
265
+ }
266
+ else if (YAML.isSeq(node)) {
267
+ for (const item of node.items)
268
+ collectVarNamesFromNode(item, out);
269
+ }
270
+ else if (YAML.isMap(node)) {
271
+ for (const pair of node.items)
272
+ collectVarNamesFromNode(pair.value, out);
273
+ }
274
+ }
275
+ function substituteTemplateText(text, bindings) {
276
+ return text.replace(VAR_RE, (whole, name) => (name in bindings ? bindings[name] : whole));
277
+ }
278
+ export const instancesResolveValidator = {
279
+ id: INSTANCES_ID,
280
+ game: GAME,
281
+ groups: GROUPS,
282
+ modTypes: MOD_TYPES,
283
+ summary: "Every $(var) in an $instances template is bound by every instance entry, and the expansion produces distinct record names.",
284
+ appliesTo: appliesToYaml,
285
+ appliesToDescription: APPLIES_DESC,
286
+ needs: {},
287
+ cost: "edit",
288
+ severity: "block",
289
+ evidence: INSTANCES_EVIDENCE,
290
+ async run(ctx) {
291
+ if (ctx.target.kind !== "file")
292
+ return [];
293
+ const file = ctx.target.path;
294
+ let text;
295
+ try {
296
+ text = await ctx.readText(file);
297
+ }
298
+ catch {
299
+ return [];
300
+ }
301
+ const doc = YAML.parseDocument(text);
302
+ if (doc.errors.length > 0)
303
+ return [];
304
+ const root = doc.contents;
305
+ if (!root || !YAML.isMap(root))
306
+ return [];
307
+ const out = [];
308
+ for (const pair of root.items) {
309
+ if (!YAML.isScalar(pair.key) || !YAML.isMap(pair.value))
310
+ continue;
311
+ const rawName = String(pair.key.value);
312
+ const instancesPair = pair.value.items.find((p) => YAML.isScalar(p.key) && String(p.key.value) === "$instances");
313
+ if (!instancesPair || !YAML.isSeq(instancesPair.value))
314
+ continue;
315
+ const bindingsList = [];
316
+ let malformed = false;
317
+ for (const entry of instancesPair.value.items) {
318
+ if (!YAML.isMap(entry)) {
319
+ malformed = true;
320
+ continue;
321
+ }
322
+ const bindings = {};
323
+ for (const p of entry.items) {
324
+ if (!YAML.isScalar(p.key))
325
+ continue;
326
+ const key = String(p.key.value);
327
+ bindings[key] = YAML.isScalar(p.value) && p.value.value !== null ? String(p.value.value) : "";
328
+ }
329
+ bindingsList.push(bindings);
330
+ }
331
+ if (malformed || bindingsList.length === 0)
332
+ continue;
333
+ const varNames = new Set();
334
+ collectVarNamesFromText(rawName, varNames);
335
+ for (const bodyPair of pair.value.items) {
336
+ if (YAML.isScalar(bodyPair.key) && String(bodyPair.key.value) === "$instances")
337
+ continue;
338
+ collectVarNamesFromNode(bodyPair.value, varNames);
339
+ }
340
+ for (const v of varNames) {
341
+ const missingIn = bindingsList.filter((b) => !(v in b));
342
+ if (missingIn.length > 0) {
343
+ out.push(finding(INSTANCES_ID, "block", file, `$(${v}) is used in the $instances template for "${rawName}" but ${missingIn.length} of ${bindingsList.length} instance entries do not define it — TweakXL leaves the literal "$(${v})" in the expanded name/value for those.`, INSTANCES_EVIDENCE));
344
+ }
345
+ }
346
+ const expandedNames = bindingsList.map((b) => substituteTemplateText(rawName, b));
347
+ const counts = new Map();
348
+ for (const name of expandedNames)
349
+ counts.set(name, (counts.get(name) ?? 0) + 1);
350
+ for (const [name, count] of counts) {
351
+ if (count > 1) {
352
+ out.push(finding(INSTANCES_ID, "warn", file, `The $instances expansion for "${rawName}" produces the record name "${name}" from ${count} different instance entries — TweakXL will make one overwrite the other.`, INSTANCES_EVIDENCE));
353
+ }
354
+ }
355
+ }
356
+ return out;
357
+ },
358
+ };
359
+ const FLAT_NAME_EXISTS_ID = "cp2077.tweaks.flat-name-exists";
360
+ const FLAT_NAME_EXISTS_EVIDENCE = [
361
+ "cp2077.process.tweakdb-ground-truth",
362
+ "cp2077.tweakdb.melee.thermal-damage-flag-required-for-burn-application",
363
+ ];
364
+ export const flatNameExistsValidator = {
365
+ id: FLAT_NAME_EXISTS_ID,
366
+ game: GAME,
367
+ groups: GROUPS,
368
+ modTypes: MOD_TYPES,
369
+ summary: "Every flat assigned on a record exists somewhere on that record's $base chain, unless the record declares its own $type.",
370
+ appliesTo: appliesToYaml,
371
+ appliesToDescription: APPLIES_DESC,
372
+ needs: { project: true, index: true },
373
+ cost: "edit",
374
+ severity: "warn",
375
+ evidence: FLAT_NAME_EXISTS_EVIDENCE,
376
+ async run(ctx) {
377
+ if (ctx.target.kind !== "file")
378
+ return [];
379
+ const file = ctx.target.path;
380
+ const records = await findRecordsInFile(ctx, file);
381
+ if (!records)
382
+ return [];
383
+ const out = [];
384
+ for (const rec of records) {
385
+ if (!rec.base)
386
+ continue;
387
+ const res = await ctx.lookup("chain", { name: rec.base });
388
+ const row = res.rows[0];
389
+ if (res.count === 0 || !row?.flats)
390
+ continue;
391
+ const chainFlatNames = new Set(row.flats.map((f) => f.name));
392
+ for (const flat of rec.flats) {
393
+ if (chainFlatNames.has(flat.name))
394
+ continue;
395
+ const attested = await ctx.lookup("flat", { name: flat.name });
396
+ const attestedElsewhere = attested.rows.some((r) => r.record !== rec.name);
397
+ if (attestedElsewhere)
398
+ continue;
399
+ out.push(finding(FLAT_NAME_EXISTS_ID, "warn", file, `Flat "${flat.name}" (on ${rec.name}) is not declared anywhere on the $base chain of ${rec.base}, nor attested on any other vanilla record. TweakXL creates the flat rather than rejecting the file, so a typo of an existing flat name is silent.`, FLAT_NAME_EXISTS_EVIDENCE, { line: rec.line }));
400
+ }
401
+ }
402
+ return out;
403
+ },
404
+ };
405
+ const RECORD_NAME_EXISTS_ID = "cp2077.tweaks.record-name-exists";
406
+ const RECORD_NAME_EXISTS_EVIDENCE = [
407
+ "cp2077.tweakdb.melee.attack-lookup-by-state-plus-counter",
408
+ "cp2077.loot.player-prereq-tier-gating",
409
+ "cp2077.tweakdb.melee.attack-records-live-in-per-family-packages",
410
+ ];
411
+ const DOTTED_TOKEN_RE = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+$/;
412
+ function collectDottedTokens(value, out) {
413
+ if (typeof value === "string") {
414
+ if (DOTTED_TOKEN_RE.test(value) && !value.startsWith("LocKey#"))
415
+ out.add(value);
416
+ }
417
+ else if (Array.isArray(value)) {
418
+ for (const item of value)
419
+ collectDottedTokens(item, out);
420
+ }
421
+ else if (value && typeof value === "object") {
422
+ for (const item of Object.values(value))
423
+ collectDottedTokens(item, out);
424
+ }
425
+ }
426
+ export const recordNameExistsValidator = {
427
+ id: RECORD_NAME_EXISTS_ID,
428
+ game: GAME,
429
+ groups: GROUPS,
430
+ modTypes: MOD_TYPES,
431
+ summary: "Every dotted token used as a flat value resolves to a record in this project or the vanilla index, when its package is known.",
432
+ appliesTo: appliesToYaml,
433
+ appliesToDescription: APPLIES_DESC,
434
+ needs: { project: true, index: true },
435
+ cost: "edit",
436
+ severity: "warn",
437
+ evidence: RECORD_NAME_EXISTS_EVIDENCE,
438
+ async run(ctx) {
439
+ if (ctx.target.kind !== "file")
440
+ return [];
441
+ const file = ctx.target.path;
442
+ const records = await findRecordsInFile(ctx, file);
443
+ if (!records)
444
+ return [];
445
+ const { namesDefined } = await getProjectTweakData(ctx);
446
+ const out = [];
447
+ for (const rec of records) {
448
+ const tokens = new Set();
449
+ for (const flat of rec.flats)
450
+ collectDottedTokens(flat.value, tokens);
451
+ for (const token of tokens) {
452
+ if (namesDefined.has(token))
453
+ continue;
454
+ const pkg = token.slice(0, token.indexOf("."));
455
+ const pkgKnown = await ctx.lookup("record", { prefix: `${pkg}.`, limit: 1 });
456
+ if (pkgKnown.count === 0)
457
+ continue;
458
+ const resolved = await ctx.lookup("record", { name: token });
459
+ if (resolved.count > 0)
460
+ continue;
461
+ out.push(finding(RECORD_NAME_EXISTS_ID, "warn", file, `"${token}" (used on ${rec.name}) looks like a TweakDB record reference in a known package ("${pkg}") but resolves nowhere, in this project or the vanilla index.`, RECORD_NAME_EXISTS_EVIDENCE, { line: rec.line }));
462
+ }
463
+ }
464
+ return out;
465
+ },
466
+ };
467
+ const DOTTED_PATH_ID = "cp2077.tweaks.dotted-path-same-file";
468
+ const DOTTED_PATH_EVIDENCE = ["cp2077.tweakdb.melee.dotted-path-mod-of-same-file-record-fails"];
469
+ function scanFileForDottedEdits(text) {
470
+ const lineCounter = new YAML.LineCounter();
471
+ const doc = YAML.parseDocument(text, { lineCounter });
472
+ if (doc.errors.length > 0)
473
+ return undefined;
474
+ const definedNames = new Set();
475
+ const dottedEdits = [];
476
+ const root = doc.contents;
477
+ if (!root || !YAML.isMap(root))
478
+ return { definedNames, dottedEdits };
479
+ for (const pair of root.items) {
480
+ if (!YAML.isScalar(pair.key))
481
+ continue;
482
+ const rawName = String(pair.key.value);
483
+ const line = pair.key.range ? lineCounter.linePos(pair.key.range[0]).line : 1;
484
+ if (YAML.isMap(pair.value)) {
485
+ definedNames.add(rawName);
486
+ continue;
487
+ }
488
+ const dot = rawName.lastIndexOf(".");
489
+ if (dot === -1) {
490
+ definedNames.add(rawName);
491
+ continue;
492
+ }
493
+ dottedEdits.push({ owner: rawName.slice(0, dot), rawKey: rawName, line });
494
+ }
495
+ return { definedNames, dottedEdits };
496
+ }
497
+ export const dottedPathSameFileValidator = {
498
+ id: DOTTED_PATH_ID,
499
+ game: GAME,
500
+ groups: GROUPS,
501
+ modTypes: MOD_TYPES,
502
+ summary: "A record modified via a dotted top-level path (Record.flat:) must not also be defined in the same file, and only from a file sorting after the one that defines it.",
503
+ appliesTo(target) {
504
+ return target.kind === "project";
505
+ },
506
+ appliesToDescription: 'every file under a project source of kind "tweaks", reasoned about together once per validation',
507
+ needs: { project: true },
508
+ cost: "edit",
509
+ severity: "block",
510
+ evidence: DOTTED_PATH_EVIDENCE,
511
+ async run(ctx) {
512
+ if (ctx.target.kind !== "project")
513
+ return [];
514
+ const { files } = await getProjectTweakData(ctx);
515
+ const perFile = new Map();
516
+ for (const file of files) {
517
+ let text;
518
+ try {
519
+ text = await ctx.readText(file);
520
+ }
521
+ catch {
522
+ continue;
523
+ }
524
+ const info = scanFileForDottedEdits(text);
525
+ if (info)
526
+ perFile.set(file, info);
527
+ }
528
+ const out = [];
529
+ for (let i = 0; i < files.length; i++) {
530
+ const file = files[i];
531
+ const info = perFile.get(file);
532
+ if (!info)
533
+ continue;
534
+ for (const edit of info.dottedEdits) {
535
+ if (info.definedNames.has(edit.owner)) {
536
+ out.push(finding(DOTTED_PATH_ID, "block", file, `"${edit.rawKey}:" edits ${edit.owner} by dotted path, but ${edit.owner} is also defined as a full record in this same file. TweakXL logs "Ambiguous definition" for exactly this shape; move this edit into a second file that sorts after this one.`, DOTTED_PATH_EVIDENCE, { line: edit.line }));
537
+ continue;
538
+ }
539
+ for (let j = i + 1; j < files.length; j++) {
540
+ const laterFile = files[j];
541
+ if (perFile.get(laterFile)?.definedNames.has(edit.owner)) {
542
+ out.push(finding(DOTTED_PATH_ID, "block", file, `"${edit.rawKey}:" edits ${edit.owner} by dotted path, but ${edit.owner} is defined in ${path.basename(laterFile)}, which sorts AFTER ${path.basename(file)}. TweakXL loads tweak files in name order, so the base record does not exist yet when this edit runs.`, DOTTED_PATH_EVIDENCE, { line: edit.line, related: [laterFile] }));
543
+ break;
544
+ }
545
+ }
546
+ }
547
+ }
548
+ return out;
549
+ },
550
+ };
551
+ const REPLACER_TEMPLATE_ID = "cp2077.tweaks.replacer-template-staged";
552
+ const REPLACER_BASE = "Character.Player_Replacer_Puppet_Base";
553
+ const REPLACER_TEMPLATE_EVIDENCE = [
554
+ "cp2077.player.control.replacers-are-other-player-bodies",
555
+ "cp2077.player.control.android-replacer-is-v-in-the-android-app",
556
+ "cyberpunk2077.tweakxl.missing-resource (triage pattern: `<record>.entityTemplatePath refers to a non-existent resource.`)",
557
+ ];
558
+ async function derivesFromReplacerBase(ctx, start, projectBases) {
559
+ let current = start;
560
+ const seen = new Set();
561
+ for (;;) {
562
+ if (current === REPLACER_BASE)
563
+ return true;
564
+ const next = projectBases.get(current);
565
+ if (next === undefined)
566
+ break;
567
+ if (seen.has(current))
568
+ return false;
569
+ seen.add(current);
570
+ current = next;
571
+ }
572
+ if (!ctx.store)
573
+ return false;
574
+ const res = await ctx.lookup("chain", { name: current });
575
+ const row = res.rows[0];
576
+ return Boolean(row?.chain?.some((c) => c.name === REPLACER_BASE));
577
+ }
578
+ export const replacerTemplateStagedValidator = {
579
+ id: REPLACER_TEMPLATE_ID,
580
+ game: GAME,
581
+ groups: GROUPS,
582
+ modTypes: [...MOD_TYPES, "player-replacer"],
583
+ summary: `A record deriving ${REPLACER_BASE} names an entityTemplatePath that ends in .ent and is staged, declared as a build step's output, or (a vanilla path) in the resource index.`,
584
+ appliesTo: appliesToYaml,
585
+ appliesToDescription: APPLIES_DESC,
586
+ needs: { project: true },
587
+ cost: "edit",
588
+ severity: "block",
589
+ evidence: REPLACER_TEMPLATE_EVIDENCE,
590
+ async run(ctx) {
591
+ if (ctx.target.kind !== "file")
592
+ return [];
593
+ const file = ctx.target.path;
594
+ const records = await findRecordsInFile(ctx, file);
595
+ if (!records)
596
+ return [];
597
+ const { allRecords } = await getProjectTweakData(ctx);
598
+ const projectBases = new Map();
599
+ for (const r of [...allRecords, ...records])
600
+ if (!projectBases.has(r.name) || r.base)
601
+ projectBases.set(r.name, r.base);
602
+ const stagings = ctx.project.sources.filter((s) => s.kind === "archive-staging").map((s) => s.absPath);
603
+ const declared = new Set();
604
+ for (const step of ctx.project.project.build?.steps ?? []) {
605
+ if (typeof step !== "object" || !("expectOutputs" in step) || !Array.isArray(step.expectOutputs))
606
+ continue;
607
+ for (const out of step.expectOutputs)
608
+ declared.add(path.resolve(ctx.project.root, out));
609
+ }
610
+ const out = [];
611
+ for (const rec of records) {
612
+ if (!(await derivesFromReplacerBase(ctx, rec.base ?? rec.name, projectBases)))
613
+ continue;
614
+ const flat = rec.flats.find((f) => f.name === "entityTemplatePath");
615
+ if (!flat || typeof flat.value !== "string" || flat.value === "")
616
+ continue;
617
+ const depotPath = flat.value.replace(/\//g, "\\");
618
+ const lineOpts = { line: rec.line };
619
+ if (!/\.ent$/i.test(depotPath)) {
620
+ out.push(finding(REPLACER_TEMPLATE_ID, "block", file, `${rec.name}.entityTemplatePath "${depotPath}" is not an .ent; a replacer body is an entity template.`, REPLACER_TEMPLATE_EVIDENCE, lineOpts));
621
+ continue;
622
+ }
623
+ if (/^(base|ep1)\\/i.test(depotPath)) {
624
+ if (ctx.store) {
625
+ const res = await ctx.lookup("resource", { path: depotPath });
626
+ if (res.count === 0) {
627
+ out.push(finding(REPLACER_TEMPLATE_ID, "block", file, `${rec.name}.entityTemplatePath "${depotPath}" is not in the vanilla resource index.`, REPLACER_TEMPLATE_EVIDENCE, lineOpts));
628
+ }
629
+ }
630
+ continue;
631
+ }
632
+ if (stagings.length === 0) {
633
+ out.push(finding(REPLACER_TEMPLATE_ID, "block", file, `${rec.name}.entityTemplatePath "${depotPath}" is a mod path, and the project declares no archive-staging source to find it under.`, REPLACER_TEMPLATE_EVIDENCE, {
634
+ ...lineOpts,
635
+ fix: "Declare the staged archive folder as a source of kind archive-staging in modwright.json.",
636
+ }));
637
+ continue;
638
+ }
639
+ const candidates = stagings.map((s) => path.join(s, ...depotPath.split("\\")));
640
+ let found = false;
641
+ for (const c of candidates) {
642
+ if (declared.has(path.resolve(c)) || (await ctx.exists(c))) {
643
+ found = true;
644
+ break;
645
+ }
646
+ }
647
+ if (found)
648
+ continue;
649
+ const abs = candidates[0];
650
+ out.push(finding(REPLACER_TEMPLATE_ID, "block", file, `${rec.name}.entityTemplatePath "${depotPath}" is not staged (expected at ${abs}) and no build step declares it as an output, so TweakXL would log that it refers to a non-existent resource and the body would never spawn.`, REPLACER_TEMPLATE_EVIDENCE, { ...lineOpts, fix: "Fix the path, or stage the entity there (the player-replacer scaffold's deserialize step writes it)." }));
651
+ }
652
+ return out;
653
+ },
654
+ };
655
+ const BASE_ORDER_ID = "cp2077.tweaks.base-defined-before-use";
656
+ const BASE_ORDER_EVIDENCE = ["observed (2026-09-28): TweakXL read a folder in name order and a $base from a later file failed with \"Cannot clone ..., the record doesn't exist\"", "cp2077.tweakxl.yaml-syntax.files-read-in-name-order"];
657
+ function loadKey(file) {
658
+ return path.basename(file);
659
+ }
660
+ export const baseDefinedBeforeUseValidator = {
661
+ id: BASE_ORDER_ID,
662
+ game: GAME,
663
+ groups: GROUPS,
664
+ modTypes: MOD_TYPES,
665
+ summary: "A $base this project defines is defined in a file TweakXL reads earlier (name order), not later.",
666
+ appliesTo: appliesToYaml,
667
+ appliesToDescription: APPLIES_DESC,
668
+ needs: { project: true },
669
+ cost: "edit",
670
+ severity: "block",
671
+ evidence: BASE_ORDER_EVIDENCE,
672
+ async run(ctx) {
673
+ if (ctx.target.kind !== "file")
674
+ return [];
675
+ const file = ctx.target.path;
676
+ const records = await findRecordsInFile(ctx, file);
677
+ if (!records)
678
+ return [];
679
+ const { allRecords } = await getProjectTweakData(ctx);
680
+ const definedIn = new Map();
681
+ for (const r of allRecords) {
682
+ if (!definedIn.has(r.name))
683
+ definedIn.set(r.name, r.file);
684
+ }
685
+ const out = [];
686
+ for (const rec of records) {
687
+ if (!rec.base)
688
+ continue;
689
+ const home = definedIn.get(rec.base);
690
+ if (!home || path.resolve(home) === path.resolve(file))
691
+ continue;
692
+ if (loadKey(home) > loadKey(file)) {
693
+ out.push(finding(BASE_ORDER_ID, "block", file, `$base: ${rec.base} (used by ${rec.name}) is defined in ${loadKey(home)}, which TweakXL reads after ${loadKey(file)} (files are read in name order). TweakXL logs "Cannot clone ${rec.base}, the record doesn't exist" and drops ${rec.name}. Rename one file so the base's file sorts first, or move the record.`, BASE_ORDER_EVIDENCE, { line: rec.line }));
694
+ }
695
+ }
696
+ return out;
697
+ },
698
+ };
699
+ const INLINE_REF_ID = "cp2077.tweaks.inline-reference";
700
+ const INLINE_REF_EVIDENCE = [
701
+ "cp2077.tweakxl.yaml-syntax.inline-record-as-base-is-bad-practice",
702
+ "cp2077.tweakdb.index-inline-names-can-differ-from-the-game",
703
+ "observed (2026-09-28): the index named a vehicle package _inline1 where the game names it _inline0",
704
+ ];
705
+ const INLINE_NAME_RE = /^[A-Za-z_][\w]*\.[\w.]*_inline\d+$/;
706
+ function recordNamesIn(value, out) {
707
+ if (typeof value === "string") {
708
+ if (INLINE_NAME_RE.test(value))
709
+ out.push(value);
710
+ }
711
+ else if (Array.isArray(value)) {
712
+ for (const v of value)
713
+ recordNamesIn(v, out);
714
+ }
715
+ }
716
+ export const inlineReferenceValidator = {
717
+ id: INLINE_REF_ID,
718
+ game: GAME,
719
+ groups: GROUPS,
720
+ modTypes: MOD_TYPES,
721
+ summary: "No $base or value names a vanilla <Record>_inline<N> record (unstable names; the index's may differ from the game's).",
722
+ appliesTo: appliesToYaml,
723
+ appliesToDescription: APPLIES_DESC,
724
+ needs: { project: true },
725
+ cost: "edit",
726
+ severity: "warn",
727
+ evidence: INLINE_REF_EVIDENCE,
728
+ async run(ctx) {
729
+ if (ctx.target.kind !== "file")
730
+ return [];
731
+ const file = ctx.target.path;
732
+ const records = await findRecordsInFile(ctx, file);
733
+ if (!records)
734
+ return [];
735
+ const { namesDefined } = await getProjectTweakData(ctx);
736
+ const out = [];
737
+ for (const rec of records) {
738
+ const refs = [];
739
+ if (rec.base && INLINE_NAME_RE.test(rec.base))
740
+ refs.push({ name: rec.base, where: "$base" });
741
+ for (const flat of rec.flats) {
742
+ const names = [];
743
+ recordNamesIn(flat.value, names);
744
+ for (const n of names)
745
+ refs.push({ name: n, where: flat.name });
746
+ }
747
+ for (const ref of refs) {
748
+ if (namesDefined.has(ref.name))
749
+ continue;
750
+ out.push(finding(INLINE_REF_ID, "warn", file, `${rec.name}: ${ref.where} names the vanilla inline record ${ref.name}. Inline names are positional and can change with a game update, and the index's may not be the game's (observed: the index's _inline1 for a vehicle package was a stat modifier in game). Verify it with a live TweakDB read, or use a named record.`, INLINE_REF_EVIDENCE, { line: rec.line }));
751
+ }
752
+ }
753
+ return out;
754
+ },
755
+ };
756
+ export const TWEAK_VALIDATORS = [
757
+ yamlLintValidator,
758
+ baseExistsValidator,
759
+ typeKnownValidator,
760
+ appendOnListFlatValidator,
761
+ instancesResolveValidator,
762
+ flatNameExistsValidator,
763
+ recordNameExistsValidator,
764
+ dottedPathSameFileValidator,
765
+ replacerTemplateStagedValidator,
766
+ baseDefinedBeforeUseValidator,
767
+ inlineReferenceValidator,
768
+ ];