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,1104 @@
1
+ -- MODWRIGHT-BRIDGE-DO-NOT-SHIP
2
+ --
3
+ -- ModWrightBridge / probes.lua — the CET dispatch table. One entry per row of
4
+ -- the probe catalogue, keyed by the row's `id`, carrying that row's
5
+ -- EXACT status word, and nothing else: probes are data rather than new Lua,
6
+ -- so a test plan can never ship fresh script into the game.
7
+ --
8
+ -- ============================================================================
9
+ -- THE RULE THAT SHAPES EVERY HANDLER HERE
10
+ -- ============================================================================
11
+ -- The catalogue marks half these rows `unverified`, and an `unverified` row is one where NOTHING ON
12
+ -- DISK attests the call. So a handler whose call is missing returns `error` with a message
13
+ -- naming what was missing and which check settles it. It never returns `pass`, and
14
+ -- it never lets the absence propagate as a Lua error into CET's onUpdate.
15
+ --
16
+ -- Attestation words used below:
17
+ -- attested(engine) a shipped CET mod on disk makes this exact call
18
+ -- attested(doc) a CyberEngineTweaks wiki page names this exact call
19
+ -- unverified no page and no mod on disk makes it; a console check confirms or contradicts it
20
+ -- not designed the catalogue's `cp2077.loot.roll`: answered `unsupported`, always
21
+ -- Each is copied into the probe result's `evidence.attestation` with its citation, so the
22
+ -- provenance of an answer travels with the answer.
23
+ --
24
+ -- ============================================================================
25
+ -- Engine calls used in this file (source quoted above each first use)
26
+ -- ============================================================================
27
+ -- Game.GetPlayer() wiki/Script-tutorial.md §"Regular handles";
28
+ -- shipped CET mods
29
+ -- Game.GetTransactionSystem():GetItemInSlot(...) shipped CET mods
30
+ -- TweakDBID.new(name) shipped CET mods
31
+ -- ItemID.GetTDBID(w:GetItemData():GetID()) shipped CET mods
32
+ -- Game.GetDynamicEntitySystem():GetTaggedIDs(t) shipped CET mods
33
+ -- Game.FindEntityByID(id) shipped CET mods
34
+ -- ent:FindComponentByName(name) shipped CET mods
35
+ -- ent:GetWorldPosition() / GetWorldOrientation() shipped CET mods
36
+ -- Game.AddToInventory(item, qty) wiki/Console.md §"Specificity";
37
+ -- wiki/ConsoleCommands.md:25
38
+ -- GetMod("ModWrightBridge") knowledge fact cp2077.process.cet-mods-sandboxed
39
+ -- (verified, 2.31, observed in game 2026-08-16)
40
+ -- TweakDB:GetRecord / TweakDB:GetFlat observed in the CET console on
41
+ -- 2026-09-08 (see the rows below).
42
+ --
43
+ -- MUTATION. Two rows have effect `mutate`: `cp2077.inventory.add`, and `cp2077.lua.eval`
44
+ -- (added 2026-09-11). They are the two handlers in this
45
+ -- file with `mutates = true`, and `Probes.run` refuses to call a mutating handler unless the
46
+ -- request set `allowMutate: true` AND the kind is one of those two ids. Because the gate keys
47
+ -- off `mutates` and not off the request's own effect word, a request that mislabelled either
48
+ -- kind as `read` is refused just the same. Every other handler reads.
49
+
50
+ local Probes = {}
51
+
52
+ --- Wired by init.lua so this module needs no loader of its own.
53
+ local Protocol = nil
54
+
55
+ function Probes.init(protocol)
56
+ Protocol = protocol
57
+ return Probes
58
+ end
59
+
60
+ -- ---------------------------------------------------------------------------
61
+ -- Shared helpers
62
+ -- ---------------------------------------------------------------------------
63
+
64
+ local function params(probe)
65
+ if type(probe.params) == "table" then return probe.params end
66
+ return {}
67
+ end
68
+
69
+ local function ok(observed, evidence)
70
+ return { status = "pass", observed = observed, evidence = evidence or {} }
71
+ end
72
+
73
+ local function failed(msg, observed, evidence)
74
+ return { status = "fail", observed = observed, evidence = evidence or {}, error = msg }
75
+ end
76
+
77
+ local function errored(msg, evidence)
78
+ return { status = "error", observed = nil, evidence = evidence or {}, error = msg }
79
+ end
80
+
81
+ --- The one message shape every `unverified` row uses when its call is simply not there.
82
+ local function absent(what, confirmStep)
83
+ return errored(what .. " is not available in this CET sandbox, and no source on disk attests it. " ..
84
+ "This row stays `unverified` until " .. confirmStep .. " confirms or contradicts it; it is reported " ..
85
+ "as an error rather than a pass so nothing downstream reads a missing call as a measurement.")
86
+ end
87
+
88
+ --- An attested call whose global is nonetheless not visible from this sandbox. Distinct
89
+ --- from `absent`: the call is known to work (seen from the console, 2026-09-08), so a
90
+ --- missing global here is an environment difference to report, never a reason to call
91
+ --- the row unverified. The old `absent` wording was once read as a
92
+ --- refusal and re-verified by hand; the evidence now names the type seen.
93
+ local function missingAttested(what, call, evidence)
94
+ evidence = evidence or {}
95
+ evidence.globalType = what
96
+ return errored(what .. " is not visible from this mod sandbox, although " .. call ..
97
+ " is attested from the CET console (2026-09-08). Record the type seen here and compare " ..
98
+ "with a direct console call in the same session; this is an environment finding, not an " ..
99
+ "unverified probe.", evidence)
100
+ end
101
+
102
+ --- Is a global present, without calling it?
103
+ -- Globals are read by NAME through a closure, never through `_G`: a CET mod's environment
104
+ -- is a fresh table falling back to a whitelisted globals copy without `_G`
105
+ -- (CyberEngineTweaks/src/scripting/Sandbox.cpp:9, LuaSandbox.cpp whitelist), so `_G[name]`
106
+ -- is always nil in here. Observed in game: the heartbeat, which detects by
107
+ -- name, said GetMod true while this helper said nil. Same fix as protocol.lua's globalIs.
108
+ local GLOBAL_GETTERS = {
109
+ io = function() return io end,
110
+ GetMod = function() return GetMod end,
111
+ Game = function() return Game end,
112
+ TweakDB = function() return TweakDB end,
113
+ }
114
+
115
+ local function have(name)
116
+ local get = GLOBAL_GETTERS[name]
117
+ if not get then return false, "unlisted" end
118
+ local okG, v = pcall(get)
119
+ if not okG then return false, "nil" end
120
+ local t = type(v)
121
+ return (t == "table" or t == "userdata" or t == "function"), t
122
+ end
123
+
124
+ --- Turn an engine value into something `Protocol.encode` can carry. Engine handles are
125
+ --- `userdata`; `tostring` is the only thing attested to work on one (shipped mods
126
+ --- print `tostring(...)` of engine values), so that is what is recorded.
127
+ local function coerce(v)
128
+ local t = type(v)
129
+ if t == "nil" or t == "string" or t == "number" or t == "boolean" then return v end
130
+ if t == "table" then
131
+ local out = {}
132
+ for k, val in pairs(v) do
133
+ local kt = type(k)
134
+ if kt == "string" or kt == "number" then
135
+ local vt = type(val)
136
+ if vt == "string" or vt == "number" or vt == "boolean" then out[k] = val
137
+ else out[k] = tostring(val) end
138
+ end
139
+ end
140
+ return out
141
+ end
142
+ return tostring(v)
143
+ end
144
+
145
+ --- `table.pack` is 5.2+, and this file is kept inside the 5.1-compatible subset the two
146
+ --- shipped CET mods use (README.md "Lua checking"; nothing on disk pins which dialect CET
147
+ --- runs). So pack by hand: `select("#", ...)` counts the values including nils, which
148
+ --- `#{...}` cannot. Returns (count, table).
149
+ local function packResults(...)
150
+ return select("#", ...), { ... }
151
+ end
152
+
153
+ --- Read a Vector4-like handle's x/y/z. Shipped CET mods do exactly this
154
+ --- (`pos.x + dx, pos.y + dy, pos.z + dz`), so the field access is attested.
155
+ local function xyz(v)
156
+ if v == nil then return nil end
157
+ local out
158
+ local okR = pcall(function()
159
+ out = { x = tonumber(v.x), y = tonumber(v.y), z = tonumber(v.z) }
160
+ end)
161
+ if not okR or out == nil or out.x == nil then return nil end
162
+ return out
163
+ end
164
+
165
+ --- Read a quaternion handle's i/j/k/r. Shipped CET mods read `q.i, q.j, q.k, q.r`.
166
+ local function quat(q)
167
+ if q == nil then return nil end
168
+ local out
169
+ local okR = pcall(function()
170
+ out = { i = tonumber(q.i), j = tonumber(q.j), k = tonumber(q.k), r = tonumber(q.r) }
171
+ end)
172
+ if not okR or out == nil or out.i == nil then return nil end
173
+ return out
174
+ end
175
+
176
+ --- `Game.GetPlayer()` — wiki/Script-tutorial.md §"Regular handles" ("we can only get the
177
+ --- handle of the `player` by calling the global function `Game.GetPlayer()`");
178
+ --- shipped CET mods call it the same way (`if not Game.GetPlayer() then return end`).
179
+ local function player()
180
+ local p
181
+ local okP = pcall(function() p = Game.GetPlayer() end)
182
+ if okP and p ~= nil then return p end
183
+ return nil
184
+ end
185
+
186
+ --- `Game.GetDynamicEntitySystem():GetTaggedIDs(tag)` — a shipped CET mod
187
+ --- (`local des = Game.GetDynamicEntitySystem()` … `des:GetTaggedIDs(tag) or {}`).
188
+ local function taggedIds(tag)
189
+ local ids
190
+ local okT, e = pcall(function()
191
+ local des = Game.GetDynamicEntitySystem()
192
+ ids = des:GetTaggedIDs(tag) or {}
193
+ end)
194
+ if not okT then return nil, tostring(e) end
195
+ if type(ids) ~= "table" then return nil, "GetTaggedIDs returned " .. type(ids) end
196
+ return ids, nil
197
+ end
198
+
199
+ --- `Game.FindEntityByID(id)` — shipped CET mods
200
+ --- (`return Game.FindEntityByID(lightID)`).
201
+ local function entityById(id)
202
+ local ent
203
+ local okE, e = pcall(function() ent = Game.FindEntityByID(id) end)
204
+ if not okE then return nil, tostring(e) end
205
+ return ent, nil
206
+ end
207
+
208
+ --- `Game.GetTransactionSystem():GetItemInSlot(player, TweakDBID.new(slot))` —
209
+ --- as shipped CET mods do. Returns (itemObject|nil, err).
210
+ local function itemInSlot(slot)
211
+ local p = player()
212
+ if p == nil then return nil, "Game.GetPlayer() returned nil — load a save first" end
213
+ local item
214
+ local okI, e = pcall(function()
215
+ item = Game.GetTransactionSystem():GetItemInSlot(p, TweakDBID.new(slot))
216
+ end)
217
+ if not okI then return nil, tostring(e) end
218
+ return item, nil
219
+ end
220
+
221
+ --- `ItemID.GetTDBID(w:GetItemData():GetID())` — as shipped CET mods do.
222
+ local function itemTdbid(item)
223
+ local tdbid
224
+ local okT, e = pcall(function() tdbid = ItemID.GetTDBID(item:GetItemData():GetID()) end)
225
+ if not okT then return nil, tostring(e) end
226
+ return tdbid, nil
227
+ end
228
+
229
+ --- Resolve the `entityRef` parameter shared by the entity rows.
230
+ ---
231
+ --- A BARE ENTITY-ID STRING CANNOT BE RESOLVED. `Game.FindEntityByID` is attested
232
+ --- (shipped mods) but only ever with an id handed back by the engine — nothing on disk
233
+ --- attests any call that turns a string back into an EntityID, so asking for one is answered
234
+ --- with an error naming the three forms that DO work rather than a guess:
235
+ --- entityRef = "$player" the player (Game.GetPlayer)
236
+ --- entityRef = "$equipped", params.slot = "…" the item in an equipment slot
237
+ --- params.tag = "…" (+ optional params.index) the nth dynamic entity carrying that tag
238
+ --- Returns (entity, describe, err).
239
+ local function resolveEntity(p)
240
+ local ref = p.entityRef and tostring(p.entityRef) or nil
241
+
242
+ if p.tag ~= nil then
243
+ local tag = tostring(p.tag)
244
+ local ids, e = taggedIds(tag)
245
+ if ids == nil then return nil, nil, "GetTaggedIDs(\"" .. tag .. "\") failed: " .. tostring(e) end
246
+ local index = tonumber(p.index) or 1
247
+ if #ids == 0 then
248
+ return nil, nil, "no dynamic entity carries the tag \"" .. tag .. "\" right now — " ..
249
+ "stream the location in (stand at the marker) and re-run"
250
+ end
251
+ if ids[index] == nil then
252
+ return nil, nil, "params.index " .. tostring(index) .. " is out of range: " .. #ids ..
253
+ " entit" .. ((#ids == 1) and "y" or "ies") .. " carry the tag \"" .. tag .. "\""
254
+ end
255
+ local ent, e2 = entityById(ids[index])
256
+ if ent == nil then
257
+ return nil, nil, "Game.FindEntityByID resolved nothing for tag \"" .. tag .. "\" #" ..
258
+ tostring(index) .. (e2 and (": " .. e2) or " (the entity may still be streaming in)")
259
+ end
260
+ return ent, { via = "tag", tag = tag, index = index, taggedCount = #ids }, nil
261
+ end
262
+
263
+ if ref == "$player" then
264
+ local pl = player()
265
+ if pl == nil then return nil, nil, "Game.GetPlayer() returned nil — load a save first" end
266
+ return pl, { via = "$player" }, nil
267
+ end
268
+
269
+ if ref == "$equipped" then
270
+ local slot = p.slot and tostring(p.slot) or nil
271
+ if slot == nil then return nil, nil, 'entityRef "$equipped" needs params.slot, e.g. "AttachmentSlots.WeaponRight"' end
272
+ local item, e = itemInSlot(slot)
273
+ if item == nil then
274
+ return nil, nil, "nothing is in slot " .. slot .. (e and (": " .. e) or " — equip the item by hand first")
275
+ end
276
+ return item, { via = "$equipped", slot = slot }, nil
277
+ end
278
+
279
+ return nil, nil,
280
+ "cannot resolve entityRef " .. tostring(ref) .. ": nothing on disk attests a call that turns an entity-id " ..
281
+ 'string back into an EntityID. Use params.tag (+ params.index), entityRef "$player", or entityRef ' ..
282
+ '"$equipped" with params.slot.'
283
+ end
284
+
285
+ --- `ent:FindComponentByName(name)` — a shipped CET mod
286
+ --- (`return ent:FindComponentByName("SomeGlow")`). Returns (found, err).
287
+ local function componentPresent(ent, name)
288
+ local comp
289
+ local okC, e = pcall(function() comp = ent:FindComponentByName(name) end)
290
+ if not okC then return nil, tostring(e) end
291
+ return comp ~= nil, nil
292
+ end
293
+
294
+ --- The component sweep shared by `cp2077.entity.components`, `cp2077.interaction.present`
295
+ --- and the appearance differential. Returns (foundArray, mapTable, errArray).
296
+ local function sweepComponents(ent, names)
297
+ local found = Protocol.array({})
298
+ local map = {}
299
+ local errs = Protocol.array({})
300
+ for _, raw in ipairs(names) do
301
+ local name = tostring(raw)
302
+ local present, e = componentPresent(ent, name)
303
+ if present == nil then
304
+ map[name] = "error"
305
+ errs[#errs + 1] = name .. ": " .. tostring(e)
306
+ else
307
+ map[name] = present
308
+ if present then found[#found + 1] = name end
309
+ end
310
+ end
311
+ return found, map, errs
312
+ end
313
+
314
+ -- ---------------------------------------------------------------------------
315
+ -- The dispatch table (in table order)
316
+ -- ---------------------------------------------------------------------------
317
+ -- MODWRIGHT-PROBE-TABLE-BEGIN
318
+ Probes.handlers = {
319
+
320
+ -- Row 1 — **unverified**. "Where can the bridge write?" `io.open(rel, "w")` over the
321
+ -- write-root candidate list; nothing attests CET's `io` for a mod. Confirmed or
322
+ -- contradicted by the discovery result. The discovery itself already ran at onInit; this row
323
+ -- reports it rather than re-running it, so a test plan cannot flap the transport mid-run.
324
+ ["cp2077.bridge.io"] = {
325
+ attestation = "unverified",
326
+ citation = "write-root discovery — no source attests CET's io for a mod",
327
+ effect = "read",
328
+ run = function()
329
+ local present, t = have("io")
330
+ local evidence = {
331
+ writeRoot = Protocol.writeRoot,
332
+ writeRootPrefix = Protocol.writeRootPrefix,
333
+ transport = Protocol.transport,
334
+ candidates = Protocol.array({}),
335
+ failures = Protocol.array({}),
336
+ capabilities = {
337
+ io = Protocol.caps.ioOpen == true,
338
+ json = Protocol.caps.json == true,
339
+ TweakDB = Protocol.caps.TweakDB == true,
340
+ Cron = Protocol.caps.Cron == true,
341
+ Observe = Protocol.caps.Observe == true,
342
+ os = Protocol.caps.osTime == true,
343
+ math = Protocol.caps.math == true,
344
+ GetMod = Protocol.caps.GetMod == true,
345
+ },
346
+ requestDecoder = Protocol.decoderName,
347
+ clockSource = Protocol.clockSource,
348
+ moduleLoader = Protocol.loaderUsed,
349
+ typeOfIo = t,
350
+ }
351
+ for i, prefix in ipairs(Protocol.WRITE_ROOT_CANDIDATES) do
352
+ evidence.candidates[i] = prefix .. Protocol.FILES.heartbeat
353
+ end
354
+ for i, e in ipairs(Protocol.writeRootErrors) do evidence.failures[i] = e end
355
+
356
+ if not present then
357
+ return errored("io is absent from this CET sandbox (type(io) = " .. t .. "): the bridge is in " ..
358
+ "log-only mode and every result is printed as a [MODWRIGHT-RESULT] line. This is " ..
359
+ "recorded as `cp2077.bridge.io` CONTRADICTED.", evidence)
360
+ end
361
+ if Protocol.transport == "file" then
362
+ return ok(Protocol.writeRoot, evidence)
363
+ end
364
+ return failed("io exists but every write-root candidate refused a write; the bridge is in log-only mode",
365
+ Protocol.writeRoot, evidence)
366
+ end,
367
+ },
368
+
369
+ -- Row 2 — attested(engine). "The bridge mod is loaded and console-reachable":
370
+ -- `registerForEvent("onInit", …)` and a returned table for `GetMod("ModWrightBridge")` —
371
+ -- knowledge fact cp2077.process.cet-mods-sandboxed (status `verified`, "2.31",
372
+ -- observed in game 2026-08-16, "console access is via GetMod(\"ModName\"), and each mod's init.lua
373
+ -- must return a table"); shipped CET mods follow the same shape.
374
+ ["cp2077.mod.loaded"] = {
375
+ attestation = "attested(engine)",
376
+ citation = "knowledge cp2077.process.cet-mods-sandboxed (verified 2.31); shipped CET mods register onInit and return a table",
377
+ effect = "read",
378
+ run = function()
379
+ local hb = Protocol.heartbeat()
380
+ local evidence = { heartbeat = hb }
381
+ local present, t = have("GetMod")
382
+ if not present then
383
+ -- Reaching this handler already proves the mod is loaded; only the CONSOLE
384
+ -- half of the row is unanswerable, and that is said rather than assumed.
385
+ evidence.getModAvailable = false
386
+ evidence.note = "this handler ran, so the mod is loaded; GetMod is " .. t ..
387
+ " here, so console reachability could not be checked from inside the sandbox — " ..
388
+ 'run `print(tostring(GetMod("ModWrightBridge")))` in the console to confirm'
389
+ return ok(true, evidence)
390
+ end
391
+ local mod
392
+ local okM, e = pcall(function() mod = GetMod("ModWrightBridge") end)
393
+ evidence.getModAvailable = true
394
+ if not okM then
395
+ evidence.getModError = tostring(e)
396
+ return ok(true, evidence)
397
+ end
398
+ evidence.getModReturned = (mod ~= nil) and type(mod) or "nil"
399
+ evidence.exposes = Protocol.array({})
400
+ if type(mod) == "table" then
401
+ for k in pairs(mod) do evidence.exposes[#evidence.exposes + 1] = tostring(k) end
402
+ table.sort(evidence.exposes)
403
+ end
404
+ if mod == nil then
405
+ return failed('GetMod("ModWrightBridge") returned nil even though this handler is running — ' ..
406
+ "the mod folder name and the GetMod argument must match", false, evidence)
407
+ end
408
+ return ok(true, evidence)
409
+ end,
410
+ },
411
+
412
+ -- Row 3 — attested(engine). "A player exists (i.e. in a save, not the menu)".
413
+ ["cp2077.player.present"] = {
414
+ attestation = "attested(engine)",
415
+ citation = "wiki/Script-tutorial.md §\"Regular handles\"; shipped CET mods",
416
+ effect = "read",
417
+ run = function()
418
+ local present, t = have("Game")
419
+ if not present then
420
+ return absent("the `Game` global (type " .. t .. ")", "a console check")
421
+ end
422
+ local p = player()
423
+ local evidence = { handle = p ~= nil and tostring(p) or nil }
424
+ if p == nil then
425
+ return failed("Game.GetPlayer() returned nil — the game is at the menu, or no save is loaded",
426
+ false, evidence)
427
+ end
428
+ return ok(true, evidence)
429
+ end,
430
+ },
431
+
432
+ -- Row 4 — attested(engine). "Which TweakDB item is in a slot".
433
+ -- PRECONDITION: the item is equipped BY HAND, and the console hazard means
434
+ -- the console must be CLOSED while this samples (the weapon drops to the holstered pose
435
+ -- while you type). Give the probe settleSeconds: 2.0.
436
+ ["cp2077.item.equipped"] = {
437
+ attestation = "attested(engine)",
438
+ citation = "shipped CET mods (GetItemInSlot, ItemID.GetTDBID)",
439
+ effect = "read",
440
+ run = function(probe)
441
+ local p = params(probe)
442
+ if p.slot == nil then
443
+ return errored('params.slot is required, e.g. "AttachmentSlots.WeaponRight"')
444
+ end
445
+ local slot = tostring(p.slot)
446
+ local evidence = {
447
+ slot = slot,
448
+ precondition = "the item must be equipped by hand and THE CONSOLE MUST BE CLOSED while this samples " ..
449
+ "(the console hazard); use settleSeconds: 2.0",
450
+ }
451
+
452
+ local item, e = itemInSlot(slot)
453
+ if item == nil then
454
+ if e ~= nil then return errored("GetItemInSlot failed: " .. e, evidence) end
455
+ return failed("nothing is in slot " .. slot, nil, evidence)
456
+ end
457
+ local tdbid, e2 = itemTdbid(item)
458
+ if tdbid == nil then
459
+ return errored("ItemID.GetTDBID(item:GetItemData():GetID()) failed: " .. tostring(e2), evidence)
460
+ end
461
+ local observed = tostring(tdbid)
462
+ evidence.tdbid = observed
463
+
464
+ -- `expect` handled here, not by the generic comparator: shipped CET mods
465
+ -- compare TDBIDs as `tdbid == TweakDBID.new(name)`, and the string form of a
466
+ -- TweakDBID is not attested to equal the record path it was built from. So an
467
+ -- `eq`/`ne` against a string is answered with the ATTESTED comparison.
468
+ if type(probe.expect) == "table" and type(probe.expect.value) == "string"
469
+ and (probe.expect.op == "eq" or probe.expect.op == "ne") then
470
+ local want = probe.expect.value
471
+ local same
472
+ local okC, e3 = pcall(function() same = (tdbid == TweakDBID.new(want)) end)
473
+ if not okC then
474
+ return errored("TweakDBID.new(\"" .. want .. "\") comparison failed: " .. tostring(e3), evidence)
475
+ end
476
+ evidence.expect = { op = probe.expect.op, value = want, note = "TweakDBID.new equality (the comparison shipped CET mods use)" }
477
+ local pass = (probe.expect.op == "eq") and same or (not same)
478
+ return {
479
+ status = pass and "pass" or "fail",
480
+ observed = observed,
481
+ evidence = evidence,
482
+ expectHandled = true,
483
+ error = (not pass) and ("slot " .. slot .. " holds " .. observed .. ", not " .. want) or nil,
484
+ }
485
+ end
486
+
487
+ return ok(observed, evidence)
488
+ end,
489
+ },
490
+
491
+ -- Row 5 — attested(doc) FOR THE CALL ONLY. "Give an item (**mutate**)".
492
+ -- `Game.AddToInventory(itemString, quantityString)` — wiki/Console.md §"Specificity"
493
+ -- ('if one wants to call `AddToInventory` they would call `Game.AddToInventory("ItemName", 1)`')
494
+ -- and wiki/ConsoleCommands.md:25, whose body is `WIP`: the RETURN VALUE IS UNDOCUMENTED.
495
+ -- The row's rule: the call's return is recorded but never trusted.
496
+ -- Preconditions: `allowMutate: true` and a disposable save.
497
+ ["cp2077.inventory.add"] = {
498
+ attestation = "attested(doc) for the call; success is NOT read from its return",
499
+ citation = "wiki/Console.md §\"Specificity\"; wiki/ConsoleCommands.md:25 (body: WIP)",
500
+ effect = "mutate",
501
+ mutates = true,
502
+ run = function(probe)
503
+ local p = params(probe)
504
+ if p.item == nil then return errored("params.item is required") end
505
+ local item = tostring(p.item)
506
+ -- ConsoleCommands.md:25 names the parameters `itemString, quantityString`, while
507
+ -- Console.md's example passes the number 1. The string form is what the signature
508
+ -- says, so that is what is sent, and both are recorded.
509
+ local qty = tostring(p.qty or p.quantity or 1)
510
+ local evidence = {
511
+ item = item,
512
+ qty = qty,
513
+ signature = "Game.AddToInventory(itemString, quantityString) — ConsoleCommands.md:25",
514
+ returnTrusted = false,
515
+ note = "PASS here means only that the call was invoked without a Lua error. The wiki body for this " ..
516
+ "command is `WIP`, so its return value is undocumented and is recorded verbatim without being " ..
517
+ "interpreted. Pair this row with a following read probe (cp2077.item.equipped, or a TweakDB " ..
518
+ "read) to learn whether anything actually happened.",
519
+ }
520
+ local present = have("Game")
521
+ if not present then return absent("the `Game` global", "a console check") end
522
+ local ret
523
+ local okA, e = pcall(function() ret = Game.AddToInventory(item, qty) end)
524
+ if not okA then
525
+ return errored("Game.AddToInventory(\"" .. item .. "\", \"" .. qty .. "\") raised: " .. tostring(e), evidence)
526
+ end
527
+ evidence.returnValue = coerce(ret)
528
+ evidence.returnType = type(ret)
529
+ return ok(true, evidence)
530
+ end,
531
+ },
532
+
533
+ -- Added 2026-09-11 so that the console lines once pasted by hand
534
+ -- (`Game.AddToInventory(...)`, `TweakDB:GetRecord(...)`, `GetMod(...).where()`) run
535
+ -- through ModWright. Same shape as the BG3 handler: `return <code>` first, the bare chunk
536
+ -- as the fallback, pcall, every return value coerced, the encoded result capped at
537
+ -- `maxBytes` (default 16 KB). Marked `mutates = true` and listed in MUTATING_KINDS: the
538
+ -- bridge cannot know what a chunk does, so it never runs one without allowMutate. The gate
539
+ -- below keys off THIS flag rather than the effect word on the request, so a request that
540
+ -- labelled this kind `read` is skipped exactly like one labelled `mutate`.
541
+ -- Status unverified: nothing here has run in a game, so whether a CET mod's environment
542
+ -- exposes a Lua loader at all is the first thing a launch settles.
543
+ ["cp2077.lua.eval"] = {
544
+ attestation = "unverified",
545
+ citation = "added 2026-09-11 so console lines run through ModWright",
546
+ effect = "mutate",
547
+ mutates = true,
548
+ run = function(probe)
549
+ local p = params(probe)
550
+ if type(p.code) ~= "string" or p.code == "" then
551
+ return errored("params.code (a Lua expression or chunk) is required")
552
+ end
553
+ local code = p.code
554
+ -- The catalogue bounds maxBytes at 1..65536 (registry.ts); a request file is not the
555
+ -- catalogue, so an out-of-range value falls back to the default rather than
556
+ -- truncating the answer to nothing.
557
+ local maxBytes = tonumber(p.maxBytes) or 16384
558
+ if maxBytes < 1 or maxBytes > 65536 then maxBytes = 16384 end
559
+ -- `loadstring` first, and this is the half that matters here: this file stays in the
560
+ -- 5.1 subset, and in 5.1 `load` takes a reader FUNCTION, not a string. 5.2+ drops
561
+ -- `loadstring` and `load` takes the string. The third argument ("t": text chunks
562
+ -- only, never a binary chunk) is honoured by 5.2+'s `load`, ignored by 5.1's
563
+ -- `loadstring`.
564
+ local loader = loadstring or load
565
+ if loader == nil then
566
+ return errored("no Lua loader (loadstring/load) is exposed to this mod", { code = code })
567
+ end
568
+ -- Observed on the first launch of this kind (2026-09-11, BG3):
569
+ -- the extender's loader rejected a chunk name as its second argument
570
+ -- ("bad argument #2 to 'loader' (table expected, got string)"), so the
571
+ -- code is the only argument passed; the loader's own defaults apply.
572
+ local fn, compileErr = loader("return " .. code)
573
+ local form = "expression"
574
+ if fn == nil then
575
+ fn, compileErr = loader(code)
576
+ form = "chunk"
577
+ end
578
+ if fn == nil then
579
+ return errored("does not compile: " .. tostring(compileErr), { code = code })
580
+ end
581
+ local n, results = packResults(pcall(fn))
582
+ if not results[1] then
583
+ return errored("raised: " .. tostring(results[2]), { code = code, form = form })
584
+ end
585
+ -- `coerce` maps nil to nil, so a nil among several returned values drops out of this
586
+ -- array instead of holding its place: `returnCount`, not `#observed`, is the
587
+ -- authority on how many values the chunk returned.
588
+ local values = {}
589
+ for i = 2, n do values[#values + 1] = coerce(results[i]) end
590
+ local returnCount = n - 1
591
+ local observed = values[1]
592
+ if returnCount > 1 then observed = values end
593
+ -- Truncation keeps a RAW PREFIX of the encoded JSON, as a plain Lua string. It is
594
+ -- deliberately not valid JSON on its own: the result writer re-encodes `observed`
595
+ -- with Protocol.encode, which escapes it into one JSON string literal, so the result
596
+ -- stays parseable and the reader sees exactly how far the value got.
597
+ local encoded = Protocol.encode(observed)
598
+ local bytes = type(encoded) == "string" and #encoded or nil
599
+ local truncated = false
600
+ if bytes ~= nil and bytes > maxBytes then
601
+ observed = string.sub(encoded, 1, maxBytes)
602
+ truncated = true
603
+ end
604
+ return ok(observed, { code = code, form = form, returnCount = returnCount, bytes = bytes, truncated = truncated })
605
+ end,
606
+ },
607
+
608
+ -- Row 6 — attested(engine). "Find dynamic entities by tag".
609
+ ["cp2077.entity.tagged"] = {
610
+ attestation = "attested(engine)",
611
+ citation = "shipped CET mods (GetDynamicEntitySystem/GetTaggedIDs, FindEntityByID)",
612
+ effect = "read",
613
+ run = function(probe)
614
+ local p = params(probe)
615
+ if p.tag == nil then return errored("params.tag is required") end
616
+ local tag = tostring(p.tag)
617
+ local ids, e = taggedIds(tag)
618
+ local evidence = { tag = tag, precondition = "loaded save, player near the entity (it must be streamed in)" }
619
+ if ids == nil then
620
+ return errored("Game.GetDynamicEntitySystem():GetTaggedIDs(\"" .. tag .. "\") failed: " .. tostring(e), evidence)
621
+ end
622
+ local list = Protocol.array({})
623
+ local resolved = 0
624
+ for i, id in ipairs(ids) do
625
+ local ent, e2 = entityById(id)
626
+ if ent ~= nil then resolved = resolved + 1 end
627
+ list[i] = { id = tostring(id), resolved = ent ~= nil, error = e2 }
628
+ end
629
+ evidence.ids = list
630
+ evidence.resolvedCount = resolved
631
+ evidence.taggedCount = #ids
632
+ if #ids == 0 then
633
+ return failed("no dynamic entity carries the tag \"" .. tag .. "\" right now", 0, evidence)
634
+ end
635
+ return ok(#ids, evidence)
636
+ end,
637
+ },
638
+
639
+ -- Row 7 — attested(engine) FOR THE CALL; the component NAMES to ask for are per-case
640
+ -- and **unverified**, which is why `params.names` is required and never defaulted.
641
+ ["cp2077.entity.components"] = {
642
+ attestation = "attested(engine) for the call; the component names asked for are unverified (per-case)",
643
+ citation = "shipped CET mods (ent:FindComponentByName)",
644
+ effect = "read",
645
+ run = function(probe)
646
+ local p = params(probe)
647
+ if type(p.names) ~= "table" or #p.names == 0 then
648
+ return errored("params.names must be a non-empty array of component names. There is no default: " ..
649
+ "the names are per-case and unverified, so guessing one would manufacture evidence.")
650
+ end
651
+ local ent, describe, e = resolveEntity(p)
652
+ if ent == nil then return errored(e, { resolve = describe }) end
653
+
654
+ local found, map, errs = sweepComponents(ent, p.names)
655
+ local evidence = {
656
+ resolve = describe,
657
+ asked = Protocol.array(p.names),
658
+ byName = map,
659
+ errors = errs,
660
+ precondition = "the entity must be streamed in",
661
+ }
662
+ if #errs > 0 then
663
+ return errored("FindComponentByName failed for " .. #errs .. " name(s): " .. table.concat(errs, "; "), evidence)
664
+ end
665
+ if #found == 0 then
666
+ return failed("none of the " .. #p.names .. " named components is on this entity", found, evidence)
667
+ end
668
+ return ok(found, evidence)
669
+ end,
670
+ },
671
+
672
+ -- Row 8 — attested(engine) FOR ITEM OBJECTS (shipped mods read these off the drawn
673
+ -- weapon); **unverified** for a sector-placed prop, which a
674
+ -- `GetTaggedIDs`/`GetWorldPosition` console line settles. The attestation reported in the
675
+ -- evidence therefore depends on HOW the entity was resolved.
676
+ ["cp2077.entity.transform"] = {
677
+ attestation = "attested(engine) for item objects; unverified for a sector-placed prop",
678
+ citation = "shipped CET mods (GetWorldPosition/GetWorldOrientation on an item object)",
679
+ effect = "read",
680
+ run = function(probe)
681
+ local p = params(probe)
682
+ local ent, describe, e = resolveEntity(p)
683
+ if ent == nil then return errored(e, { resolve = describe }) end
684
+
685
+ local evidence = { resolve = describe, precondition = "the entity must be streamed in" }
686
+ if describe.via == "tag" then
687
+ evidence.attestationForThisCall = "unverified — this is a sector-placed/dynamic prop, not the item " ..
688
+ "object shipped CET mods read; a console line is the confirmation"
689
+ else
690
+ evidence.attestationForThisCall = "attested(engine) — shipped CET mods read these off an item object"
691
+ end
692
+
693
+ local pos, orient
694
+ local okP, e1 = pcall(function() pos = ent:GetWorldPosition() end)
695
+ if not okP then return errored("ent:GetWorldPosition() raised: " .. tostring(e1), evidence) end
696
+ local okO, e2 = pcall(function() orient = ent:GetWorldOrientation() end)
697
+ if not okO then evidence.orientationError = tostring(e2) end
698
+
699
+ local position = xyz(pos)
700
+ local orientation = quat(orient)
701
+ evidence.position = position
702
+ evidence.orientation = orientation
703
+ evidence.positionRaw = (position == nil) and tostring(pos) or nil
704
+ if position == nil then
705
+ return failed("GetWorldPosition returned something with no readable x/y/z: " .. tostring(pos), nil, evidence)
706
+ end
707
+ return ok(position, evidence)
708
+ end,
709
+ },
710
+
711
+ -- Row 9 — **unverified**. The acceptance-regression differential: tagged -> components
712
+ -- -> transform, compared against a recorded good baseline.
713
+ --
714
+ -- READ THIS BEFORE TRUSTING THIS ROW.
715
+ -- The observed failure is "a wrong/unresolvable name spawns an INVISIBLE entity"; nothing on disk
716
+ -- attests a visibility predicate in CET Lua, so this probe CANNOT answer "is it visible". It
717
+ -- answers "does this entity carry the same components in the same place as the baseline", and
718
+ -- acceptance pairs it with a manual row ("stand at the marker; is the case drawn?").
719
+ --
720
+ -- With no `params.baseline` it is a CAPTURE, and says so with status `skipped` rather than
721
+ -- reporting a pass it did not earn.
722
+ ["cp2077.entity.appearance"] = {
723
+ attestation = "unverified (composite; see the comment above the row)",
724
+ citation = "composed of cp2077.entity.tagged + .components + .transform; see the comment above the row for the failure mode",
725
+ effect = "read",
726
+ run = function(probe)
727
+ local p = params(probe)
728
+ if p.tag == nil then return errored("params.tag is required (the sector node's tag)") end
729
+ if type(p.names) ~= "table" or #p.names == 0 then
730
+ return errored("params.names must be the candidate component-name list to probe; the " ..
731
+ "names are per-case and unverified, so this row never invents one")
732
+ end
733
+ local tag = tostring(p.tag)
734
+
735
+ local ids, e = taggedIds(tag)
736
+ local evidence = {
737
+ tag = tag,
738
+ baselineId = p.baselineId and tostring(p.baselineId) or nil,
739
+ caveat = "this row cannot answer \"is it visible\": no visibility predicate is attested in CET Lua. " ..
740
+ "Pair it with the manual row \"stand at the marker; is the case drawn?\".",
741
+ precondition = "player within streaming range; baseline captured on a known-good build",
742
+ }
743
+ if ids == nil then return errored("GetTaggedIDs(\"" .. tag .. "\") failed: " .. tostring(e), evidence) end
744
+ evidence.taggedCount = #ids
745
+ if #ids == 0 then
746
+ return failed("nothing carries the tag \"" .. tag .. "\" — the entity did not spawn, or is not streamed in",
747
+ nil, evidence)
748
+ end
749
+
750
+ local index = tonumber(p.index) or 1
751
+ local ent, e2 = entityById(ids[index])
752
+ if ent == nil then
753
+ return errored("Game.FindEntityByID resolved nothing for \"" .. tag .. "\" #" .. tostring(index) ..
754
+ (e2 and (": " .. e2) or ""), evidence)
755
+ end
756
+
757
+ local found, map = sweepComponents(ent, p.names)
758
+ local pos
759
+ pcall(function() pos = ent:GetWorldPosition() end)
760
+ local capture = {
761
+ components = found,
762
+ byName = map,
763
+ position = xyz(pos),
764
+ taggedCount = #ids,
765
+ }
766
+ evidence.capture = capture
767
+
768
+ local baseline = p.baseline
769
+ if type(baseline) ~= "table" then
770
+ return {
771
+ status = "skipped",
772
+ observed = capture,
773
+ evidence = evidence,
774
+ error = "no params.baseline was supplied, so no differential was computed. This row is a " ..
775
+ "DIFFERENTIAL: record `evidence.capture` from a known-good build as the baseline and re-run " ..
776
+ "with params.baseline set to it.",
777
+ }
778
+ end
779
+
780
+ local baseComponents = {}
781
+ if type(baseline.components) == "table" then
782
+ for _, n in ipairs(baseline.components) do baseComponents[tostring(n)] = true end
783
+ end
784
+ local missing, extra = Protocol.array({}), Protocol.array({})
785
+ for _, n in ipairs(found) do
786
+ if not baseComponents[n] then extra[#extra + 1] = n end
787
+ end
788
+ for n in pairs(baseComponents) do
789
+ if map[n] ~= true then missing[#missing + 1] = n end
790
+ end
791
+ table.sort(missing)
792
+
793
+ local moved = nil
794
+ local basePos = baseline.position
795
+ if type(basePos) == "table" and capture.position ~= nil then
796
+ local dx = (tonumber(basePos.x) or 0) - capture.position.x
797
+ local dy = (tonumber(basePos.y) or 0) - capture.position.y
798
+ local dz = (tonumber(basePos.z) or 0) - capture.position.z
799
+ moved = { dx = dx, dy = dy, dz = dz }
800
+ local tol = tonumber(p.positionTolerance) or 0.05
801
+ moved.withinTolerance = (dx * dx + dy * dy + dz * dz) <= (tol * tol)
802
+ moved.tolerance = tol
803
+ end
804
+ evidence.diff = { missingComponents = missing, extraComponents = extra, position = moved }
805
+ evidence.baseline = { components = Protocol.array(baseline.components or {}), position = basePos }
806
+
807
+ if #missing > 0 or #extra > 0 then
808
+ return failed("the component list differs from the baseline (" .. #missing .. " missing, " .. #extra ..
809
+ " extra); that is the appearance regression's signature when the position is unchanged",
810
+ capture, evidence)
811
+ end
812
+ if moved ~= nil and moved.withinTolerance == false then
813
+ return failed("components match the baseline but the entity moved beyond the tolerance", capture, evidence)
814
+ end
815
+ return ok(capture, evidence)
816
+ end,
817
+ },
818
+
819
+ -- Row 10. `TweakDB:GetRecord(id)`, not attested by any page or mod on disk, but observed
820
+ -- to work from the CET console (2026-09-08):
821
+ -- print(tostring(TweakDB:GetRecord("Items.Preset_Example")))
822
+ ["cp2077.tweakdb.record"] = {
823
+ attestation = "attested(engine)",
824
+ citation = "observed in the CET console (2026-09-08): TweakDB:GetRecord(id) returned userdata",
825
+ effect = "read",
826
+ run = function(probe)
827
+ local p = params(probe)
828
+ if p.id == nil then return errored("params.id is required, e.g. \"Items.Preset_Example\"") end
829
+ local id = tostring(p.id)
830
+ local evidence = { id = id, call = "TweakDB:GetRecord(id)" }
831
+ local present, t = have("TweakDB")
832
+ if not present then return missingAttested("`TweakDB` (type " .. t .. ")", "TweakDB:GetRecord", evidence) end
833
+ local rec
834
+ local okR, e = pcall(function() rec = TweakDB:GetRecord(id) end)
835
+ if not okR then
836
+ return errored("TweakDB:GetRecord(\"" .. id .. "\") raised: " .. tostring(e) ..
837
+ " — the call itself is attested (observed 2026-09-08); this is an error for this id or this session, not an unverified call", evidence)
838
+ end
839
+ evidence.returnType = type(rec)
840
+ evidence.returnValue = (rec ~= nil) and tostring(rec) or nil
841
+ if rec == nil then
842
+ return failed("no TweakDB record with id \"" .. id .. "\"", false, evidence)
843
+ end
844
+ return ok(true, evidence)
845
+ end,
846
+ },
847
+
848
+ -- Row 11. `TweakDB:GetFlat(id)`, same standing as row 10.
849
+ -- print(tostring(TweakDB:GetFlat("Items.Preset_Example.displayName")))
850
+ ["cp2077.tweakdb.flat"] = {
851
+ attestation = "attested(engine)",
852
+ citation = "observed in the CET console (2026-09-08): TweakDB:GetFlat(id) returned a LocKey",
853
+ effect = "read",
854
+ run = function(probe)
855
+ local p = params(probe)
856
+ if p.id == nil then return errored("params.id is required, e.g. \"Items.Preset_Example.displayName\"") end
857
+ local id = tostring(p.id)
858
+ local evidence = { id = id, call = "TweakDB:GetFlat(id)" }
859
+ local present, t = have("TweakDB")
860
+ if not present then return missingAttested("`TweakDB` (type " .. t .. ")", "TweakDB:GetFlat", evidence) end
861
+ local flat
862
+ local okF, e = pcall(function() flat = TweakDB:GetFlat(id) end)
863
+ if not okF then
864
+ return errored("TweakDB:GetFlat(\"" .. id .. "\") raised: " .. tostring(e) ..
865
+ " — the call itself is attested (observed 2026-09-08); this is an error for this id or this session, not an unverified call", evidence)
866
+ end
867
+ evidence.returnType = type(flat)
868
+ local value = coerce(flat)
869
+ evidence.value = value
870
+ if flat == nil then
871
+ return failed("no TweakDB flat with id \"" .. id .. "\"", nil, evidence)
872
+ end
873
+ return ok(value, evidence)
874
+ end,
875
+ },
876
+
877
+ -- Row 12 — **unverified (name)**. "An interaction component is on the entity" — the
878
+ -- call is `cp2077.entity.components`, but "the *name* is not attested anywhere on disk",
879
+ -- so the name must come from the request and is never guessed at here.
880
+ ["cp2077.interaction.present"] = {
881
+ attestation = "unverified (the interaction component's NAME is attested nowhere on disk)",
882
+ citation = "shipped CET mods for the call; the component name itself is unattested",
883
+ effect = "read",
884
+ run = function(probe)
885
+ local p = params(probe)
886
+ local names = p.names
887
+ if type(names) ~= "table" or #names == 0 then
888
+ if p.name ~= nil then names = { p.name } end
889
+ end
890
+ if type(names) ~= "table" or #names == 0 then
891
+ return errored("params.names (or params.name) is required: NO interaction component name is attested " ..
892
+ "by any source on disk, so this row will not guess one. Supply the candidate name(s) you " ..
893
+ "want probed, and record the answer as the confirmation.")
894
+ end
895
+ local ent, describe, e = resolveEntity(p)
896
+ if ent == nil then return errored(e, { resolve = describe }) end
897
+
898
+ local found, map, errs = sweepComponents(ent, names)
899
+ local evidence = {
900
+ resolve = describe,
901
+ asked = Protocol.array(names),
902
+ byName = map,
903
+ errors = errs,
904
+ caveat = "the component NAME is unverified: an absent result may mean the interaction is missing OR " ..
905
+ "that this is the wrong name for it. Only a name confirmed in the field promotes this row.",
906
+ }
907
+ if #errs > 0 then
908
+ return errored("FindComponentByName failed: " .. table.concat(errs, "; "), evidence)
909
+ end
910
+ if #found == 0 then
911
+ return failed("none of the named interaction components is on this entity", found, evidence)
912
+ end
913
+ return ok(found, evidence)
914
+ end,
915
+ },
916
+
917
+ -- Row 13 — **not designed.** "Loot table produced an item": no attested call exists
918
+ -- on disk. It stays a `manual` row until a confirmation step lands
919
+ -- one, so this kind is implemented as a permanent `unsupported`, which is the status
920
+ -- reserved for a kind the bridge cannot answer.
921
+ ["cp2077.loot.roll"] = {
922
+ attestation = "not designed",
923
+ citation = "the catalogue's last row — no attested call exists on disk",
924
+ effect = "read",
925
+ run = function()
926
+ return {
927
+ status = "unsupported",
928
+ observed = nil,
929
+ evidence = {
930
+ designStatus = "not designed",
931
+ remedy = "keep this as a `manual` test-plan row (\"kill the enemy; did the item drop?\") until a " ..
932
+ "confirmation step lands an attested call. Nothing in CET on disk reads a loot roll.",
933
+ },
934
+ error = "cp2077.loot.roll is not designed: no attested call exists on disk. It stays a manual row.",
935
+ }
936
+ end,
937
+ },
938
+ }
939
+ -- MODWRIGHT-PROBE-TABLE-END
940
+
941
+ -- ---------------------------------------------------------------------------
942
+ -- Dispatch
943
+ -- ---------------------------------------------------------------------------
944
+
945
+ --- Attach the row's status word and citation, apply `expect`, and stamp the result.
946
+ function Probes.finalize(probe, handler, out)
947
+ local status = out.status or "pass"
948
+ local evidence = out.evidence
949
+ if type(evidence) ~= "table" then evidence = {} end
950
+ if handler ~= nil then
951
+ evidence.attestation = handler.attestation
952
+ evidence.citation = handler.citation
953
+ end
954
+
955
+ if not out.expectHandled and (status == "pass" or status == "fail") and type(probe.expect) == "table" then
956
+ local verdict, note = Probes.evaluateExpect(out.observed, probe.expect)
957
+ if verdict ~= nil then
958
+ status = verdict
959
+ evidence.expect = { op = tostring(probe.expect.op), value = probe.expect.value, note = note }
960
+ end
961
+ end
962
+
963
+ local res = {
964
+ id = tostring(probe.id or "?"),
965
+ kind = tostring(probe.kind or "?"),
966
+ status = status,
967
+ observed = out.observed,
968
+ evidence = evidence,
969
+ ranAt = Protocol.isoNow(),
970
+ }
971
+ if out.error ~= nil then res.error = tostring(out.error) end
972
+ return res
973
+ end
974
+
975
+ local function deepEqual(a, b)
976
+ if type(a) ~= "table" or type(b) ~= "table" then return a == b end
977
+ return Protocol.encode(a) == Protocol.encode(b)
978
+ end
979
+
980
+ local function containsValue(observed, value)
981
+ if observed == nil then return false end
982
+ local needle = tostring(value)
983
+ if type(observed) == "string" then return observed:find(needle, 1, true) ~= nil end
984
+ if type(observed) == "table" then
985
+ for _, v in pairs(observed) do
986
+ if type(v) == "table" then
987
+ if containsValue(v, value) then return true end
988
+ elseif tostring(v) == needle then
989
+ return true
990
+ end
991
+ end
992
+ return Protocol.encode(observed):find(needle, 1, true) ~= nil
993
+ end
994
+ return tostring(observed) == needle
995
+ end
996
+
997
+ --- The `expect` block: `{ op: "eq"|"ne"|"gte"|"lte"|"contains"|"absent", value }`.
998
+ --- Returns "pass"|"fail"|"error" plus a short note for `evidence.expect`.
999
+ function Probes.evaluateExpect(observed, expect)
1000
+ if type(expect) ~= "table" or expect.op == nil then return nil, nil end
1001
+ local op = tostring(expect.op)
1002
+ local value = expect.value
1003
+
1004
+ if op == "eq" then
1005
+ return deepEqual(observed, value) and "pass" or "fail", "eq"
1006
+ elseif op == "ne" then
1007
+ return (not deepEqual(observed, value)) and "pass" or "fail", "ne"
1008
+ elseif op == "gte" or op == "lte" then
1009
+ local a, b = tonumber(observed), tonumber(value)
1010
+ if a == nil or b == nil then
1011
+ return "error", op .. " needs two numbers; observed=" .. tostring(observed)
1012
+ end
1013
+ if op == "gte" then return (a >= b) and "pass" or "fail", "gte" end
1014
+ return (a <= b) and "pass" or "fail", "lte"
1015
+ elseif op == "contains" then
1016
+ return containsValue(observed, value) and "pass" or "fail", "contains"
1017
+ elseif op == "absent" then
1018
+ local empty = (observed == nil) or (observed == false) or (observed == "")
1019
+ if not empty and type(observed) == "table" then empty = (next(observed) == nil) end
1020
+ return empty and "pass" or "fail", "absent"
1021
+ end
1022
+ return "error", "unknown expect op: " .. op
1023
+ end
1024
+
1025
+ --- The ids allowed to mutate anything: the inventory row it always had, and
1026
+ --- (since 2026-09-11) the eval row. Named
1027
+ --- as a constant so the gate below reads as the rule it enforces.
1028
+ Probes.MUTATING_KINDS = { ["cp2077.inventory.add"] = true, ["cp2077.lua.eval"] = true }
1029
+
1030
+ --- Run one probe. NEVER throws: one probe failing can never abort the batch, and a handler
1031
+ --- that raises is reported as `error` with the raised message.
1032
+ function Probes.run(probe, request, runCtx)
1033
+ local kind = tostring(probe.kind or "")
1034
+ local handler = Probes.handlers[kind]
1035
+
1036
+ if handler == nil then
1037
+ return Probes.finalize(probe, nil, {
1038
+ status = "unsupported",
1039
+ observed = nil,
1040
+ evidence = { kind = kind, context = "cet" },
1041
+ error = "no handler for probe kind \"" .. kind .. "\" in the CET dispatch table (the catalogue lists every id it knows)",
1042
+ })
1043
+ end
1044
+
1045
+ -- The `observe` effect: a listener is armed and the human performs the action.
1046
+ -- No CET listener API is attested by any source on disk (`Observe` is a CET extra that
1047
+ -- nothing here can vouch for), so an observe probe is `unsupported`, not a silent read.
1048
+ if tostring(probe.effect or "read") == "observe" then
1049
+ return Probes.finalize(probe, handler, {
1050
+ status = "unsupported",
1051
+ observed = nil,
1052
+ evidence = { effect = "observe", ObserveAvailable = Protocol.caps.Observe == true },
1053
+ error = "this bridge cannot observe: no CET listener API is attested by any source on disk. Use a " ..
1054
+ "`read` probe with settleSeconds, or a manual test-plan row.",
1055
+ })
1056
+ end
1057
+
1058
+ -- THE MUTATION GATE (keyed on the capability tiers).
1059
+ -- Two conditions, both required: the kind is one of the ids with effect `mutate`, and
1060
+ -- the request was granted the `session` tier (Protocol.grantsTier, which still honours the
1061
+ -- retired `allowMutate: true` alias for one release).
1062
+ local wantsMutation = (handler.mutates == true) or (tostring(probe.effect or "read") == "mutate")
1063
+ if wantsMutation then
1064
+ if Probes.MUTATING_KINDS[kind] ~= true then
1065
+ return Probes.finalize(probe, handler, {
1066
+ status = "skipped",
1067
+ observed = nil,
1068
+ evidence = { effect = "mutate", kind = kind },
1069
+ error = "the probe declares effect \"mutate\" but " .. kind .. " is a read-only row in the catalogue. " ..
1070
+ "The only kinds this bridge lets mutate anything are cp2077.inventory.add and cp2077.lua.eval.",
1071
+ })
1072
+ end
1073
+ if not Protocol.grantsTier(request, "session") then
1074
+ return Probes.finalize(probe, handler, {
1075
+ status = "skipped",
1076
+ observed = nil,
1077
+ evidence = { effect = "mutate", tier = "session", granted = false },
1078
+ error = "effect is \"mutate\" (session tier) and the request was not granted the \"session\" tier. " ..
1079
+ "Grant it via bridge.policy mode: workbench, grant: [\"session\"], or the retired " ..
1080
+ "allowMutate: true alias.",
1081
+ })
1082
+ end
1083
+ end
1084
+
1085
+ local okRun, out = pcall(handler.run, probe, request, runCtx)
1086
+ if okRun and type(out) == "table" then return Probes.finalize(probe, handler, out) end
1087
+ Protocol.err("probe:" .. kind, out)
1088
+ return Probes.finalize(probe, handler, {
1089
+ status = "error",
1090
+ observed = nil,
1091
+ evidence = { kind = kind },
1092
+ error = tostring(out),
1093
+ })
1094
+ end
1095
+
1096
+ --- Every kind this bridge answers, sorted — printed by `m.help()` and counted in the banner.
1097
+ function Probes.kinds()
1098
+ local out = {}
1099
+ for k in pairs(Probes.handlers) do out[#out + 1] = k end
1100
+ table.sort(out)
1101
+ return out
1102
+ end
1103
+
1104
+ return Probes