@volter/editor-game 0.5.65

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 (302) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +23 -0
  3. package/contributions/asset-budget-asset.menu.ts +26 -0
  4. package/contributions/asset-budget.action.ts +18 -0
  5. package/contributions/asset-budget.document.tsx +21 -0
  6. package/contributions/asset-budget.menu.ts +22 -0
  7. package/contributions/audio-unlock.service.ts +17 -0
  8. package/contributions/audio.utility.tsx +15 -0
  9. package/contributions/autoplay.service.ts +48 -0
  10. package/contributions/bridge.command.ts +172 -0
  11. package/contributions/build-profiles.document.tsx +21 -0
  12. package/contributions/build-progress.status.tsx +45 -0
  13. package/contributions/build.action.ts +26 -0
  14. package/contributions/build.command.ts +27 -0
  15. package/contributions/build.header.tsx +37 -0
  16. package/contributions/build.menu.ts +31 -0
  17. package/contributions/build.service.ts +31 -0
  18. package/contributions/connection.status.tsx +43 -0
  19. package/contributions/coverage.service.ts +118 -0
  20. package/contributions/edit-mode-audio.service.ts +23 -0
  21. package/contributions/edit-mode-networking.service.ts +30 -0
  22. package/contributions/game-document.service.ts +25 -0
  23. package/contributions/game-eval.command.ts +210 -0
  24. package/contributions/game.layout.ts +19 -0
  25. package/contributions/gameplay.command.ts +255 -0
  26. package/contributions/generation.service.ts +96 -0
  27. package/contributions/generations.status.tsx +53 -0
  28. package/contributions/ingest.service.ts +80 -0
  29. package/contributions/instances.command.ts +75 -0
  30. package/contributions/navmesh.menu.ts +39 -0
  31. package/contributions/navmesh.service.ts +19 -0
  32. package/contributions/network.utility.tsx +17 -0
  33. package/contributions/play.command.ts +355 -0
  34. package/contributions/profiler.action.ts +16 -0
  35. package/contributions/profiler.menu.ts +22 -0
  36. package/contributions/profiler.utility.tsx +17 -0
  37. package/contributions/react/component-board.service.ts +21 -0
  38. package/contributions/react/design-time-mount.service.ts +60 -0
  39. package/contributions/react/pasteboard.action.ts +41 -0
  40. package/contributions/react/react-inspector.service.ts +85 -0
  41. package/contributions/react/story-documents.service.ts +44 -0
  42. package/contributions/scene-document.service.ts +32 -0
  43. package/contributions/state-watch.action.ts +17 -0
  44. package/contributions/state-watch.menu.ts +23 -0
  45. package/contributions/state-watch.utility.tsx +20 -0
  46. package/contributions/team-playtest.service.ts +124 -0
  47. package/contributions/three/camera-runtime.inspector.tsx +34 -0
  48. package/contributions/three/component-board.service.ts +24 -0
  49. package/contributions/three/component-verbs.command.ts +110 -0
  50. package/contributions/three/component-verbs.service.ts +92 -0
  51. package/contributions/three/constraints.inspector.tsx +34 -0
  52. package/contributions/three/model-asset-sections.service.ts +26 -0
  53. package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
  54. package/contributions/three/story-documents.service.ts +31 -0
  55. package/contributions/three/three-authoring.service.ts +66 -0
  56. package/contributions/transport.header.tsx +19 -0
  57. package/contributions/xstate-behavior.action.ts +42 -0
  58. package/contributions/xstate-behavior.document.tsx +73 -0
  59. package/contributions/xstate-behavior.inspector.tsx +28 -0
  60. package/contributions/xstate-behavior.menu.ts +23 -0
  61. package/package.json +144 -0
  62. package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
  63. package/src/asset-budget/asset-budget-model.ts +799 -0
  64. package/src/asset-budget/basis-encoder.ts +165 -0
  65. package/src/asset-budget/gltf-io.ts +154 -0
  66. package/src/asset-budget/gltf-optimize.ts +384 -0
  67. package/src/asset-budget/image-dims.ts +78 -0
  68. package/src/asset-budget/optimize-apply.ts +221 -0
  69. package/src/audio/AudioDebuggerPanel.tsx +457 -0
  70. package/src/audio/audio-debugger-model.ts +87 -0
  71. package/src/bridge/call.ts +60 -0
  72. package/src/bridge/dispatch.ts +459 -0
  73. package/src/bridge/live-frames.ts +25 -0
  74. package/src/bridge/screenshot.ts +316 -0
  75. package/src/build/BuildProfilesPanel.tsx +476 -0
  76. package/src/build/build-session.ts +247 -0
  77. package/src/build/format-bytes.ts +14 -0
  78. package/src/command-results.ts +36 -0
  79. package/src/coverage/live-authoring-surface.ts +30 -0
  80. package/src/coverage/live-project-verbs.ts +162 -0
  81. package/src/coverage/native-system-coverage.ts +143 -0
  82. package/src/coverage/root-coverage.ts +79 -0
  83. package/src/coverage/session-coverage.ts +193 -0
  84. package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
  85. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
  86. package/src/edit-mode/edit-mode-audio.ts +56 -0
  87. package/src/edit-mode/edit-mode-networking.ts +112 -0
  88. package/src/game-document/DevicePresetPicker.tsx +84 -0
  89. package/src/game-document/GameCaptureFrameButton.tsx +45 -0
  90. package/src/game-document/GameDocument.tsx +215 -0
  91. package/src/game-document/GamePanel.tsx +662 -0
  92. package/src/game-document/InstanceInspectorPicker.tsx +183 -0
  93. package/src/game-document/crowd-debug.ts +183 -0
  94. package/src/game-document/device-preview.ts +336 -0
  95. package/src/game-document/game-view-store.ts +107 -0
  96. package/src/game-document/physics-debug.ts +187 -0
  97. package/src/generation/GenerationActivity.tsx +421 -0
  98. package/src/generation/GenerationGallery.css +231 -0
  99. package/src/generation/generation-documents.tsx +338 -0
  100. package/src/generation/generation-jobs.ts +128 -0
  101. package/src/generation/generation-presentation.ts +257 -0
  102. package/src/host/adapter-reach.ts +445 -0
  103. package/src/host/adapter-runtime-bindings.ts +303 -0
  104. package/src/host/after-paint.ts +112 -0
  105. package/src/host/api/configurations.ts +66 -0
  106. package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
  107. package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
  108. package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
  109. package/src/host/authoring/contract-scenes-stories.ts +204 -0
  110. package/src/host/authoring/creation-site-related.ts +55 -0
  111. package/src/host/authoring/ephemeral-persistence.ts +29 -0
  112. package/src/host/authoring/gesture-persist.ts +84 -0
  113. package/src/host/authoring/ingest-data-writer.ts +232 -0
  114. package/src/host/authoring/ingest-source-persistence.ts +826 -0
  115. package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
  116. package/src/host/authoring/mounted-authoring.ts +41 -0
  117. package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
  118. package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
  119. package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
  120. package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
  121. package/src/host/authoring/pixi-isolation-assets.ts +25 -0
  122. package/src/host/authoring/pixi-live-write-target.ts +979 -0
  123. package/src/host/authoring/pixi-source-identity.ts +141 -0
  124. package/src/host/authoring/pixi-still-presentation.ts +71 -0
  125. package/src/host/authoring/pixi-structure-history.ts +237 -0
  126. package/src/host/authoring/pixi-transform-channels.ts +205 -0
  127. package/src/host/authoring/selection-remount-handoff.ts +23 -0
  128. package/src/host/authoring/source-persistence-backend.ts +373 -0
  129. package/src/host/authoring/source-refresh-revisions.ts +81 -0
  130. package/src/host/authoring/struct-write-pipe.ts +143 -0
  131. package/src/host/auto-frame-window.ts +89 -0
  132. package/src/host/binding-resolver.ts +393 -0
  133. package/src/host/browser-transpile.ts +631 -0
  134. package/src/host/canvas-entry-runtime.ts +95 -0
  135. package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
  136. package/src/host/components/HeaderTelemetry.tsx +341 -0
  137. package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
  138. package/src/host/components/ResolutionPicker.tsx +89 -0
  139. package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
  140. package/src/host/components/frame-debugger-model.ts +579 -0
  141. package/src/host/components/header-telemetry-model.ts +74 -0
  142. package/src/host/components/scene-document.tsx +447 -0
  143. package/src/host/components/utility-view-state.ts +87 -0
  144. package/src/host/components/world-root-stage-binding.tsx +133 -0
  145. package/src/host/components/world-root-stage.ts +1046 -0
  146. package/src/host/coverage/authoring-read-probe.ts +583 -0
  147. package/src/host/coverage/capability-coverage.ts +1587 -0
  148. package/src/host/coverage/coverage-accounting.ts +261 -0
  149. package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
  150. package/src/host/coverage/project-verb-coverage.ts +148 -0
  151. package/src/host/coverage/system-adapter-coverage.ts +373 -0
  152. package/src/host/design-system-stories/StoryLayout.tsx +104 -0
  153. package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
  154. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
  155. package/src/host/document-preview-three.ts +110 -0
  156. package/src/host/entry-adjudication.ts +89 -0
  157. package/src/host/game-location-guard.ts +150 -0
  158. package/src/host/game-module-access.ts +196 -0
  159. package/src/host/game-realm-page.ts +358 -0
  160. package/src/host/game-realm-reclaim.ts +55 -0
  161. package/src/host/game-realm-storage.ts +104 -0
  162. package/src/host/gameplay-export.ts +288 -0
  163. package/src/host/gameplay-recording.ts +717 -0
  164. package/src/host/gated-globals.ts +1511 -0
  165. package/src/host/history/json-history-resource.ts +254 -0
  166. package/src/host/ingest/registry.ts +261 -0
  167. package/src/host/instance-extract-actions.ts +120 -0
  168. package/src/host/instance-fork-actions.ts +120 -0
  169. package/src/host/play-control-hook.ts +26 -0
  170. package/src/host/playwright-shim.ts +479 -0
  171. package/src/host/projection/dom.ts +295 -0
  172. package/src/host/projection/pixi.ts +288 -0
  173. package/src/host/r3f-entry-runtime.ts +77 -0
  174. package/src/host/react-mount-runtime.ts +162 -0
  175. package/src/host/realm-services.ts +148 -0
  176. package/src/host/recording-preview.ts +106 -0
  177. package/src/host/roots/module-root.ts +203 -0
  178. package/src/host/roots/react-root.ts +181 -0
  179. package/src/host/same-realm-loop-gate.ts +544 -0
  180. package/src/host/scene-view-drawability.ts +69 -0
  181. package/src/host/sdk/tools.ts +31 -0
  182. package/src/host/served-bundle-runtime-modules.ts +331 -0
  183. package/src/host/server-log-bridge.ts +60 -0
  184. package/src/host/staged-projects.ts +24 -0
  185. package/src/host/stories/mounted-story-viewport-source.ts +64 -0
  186. package/src/host/stories/story-arg-descriptors.ts +50 -0
  187. package/src/host/stories/story-media-presence.ts +152 -0
  188. package/src/host/surface-content.ts +87 -0
  189. package/src/host/take-named-export.ts +23 -0
  190. package/src/host/three-ingest-runtime.ts +76 -0
  191. package/src/host/types-fastnoise-lite.d.ts +7 -0
  192. package/src/host/types-mikktspace.d.ts +20 -0
  193. package/src/host/types-troika-three-text.d.ts +7 -0
  194. package/src/host/use-active-performance-source.ts +53 -0
  195. package/src/host/viewport-pose-memory.ts +48 -0
  196. package/src/host/viewport-root-presentation.ts +40 -0
  197. package/src/ingest/active-ingest.ts +251 -0
  198. package/src/ingest/active-scene-navigation.ts +43 -0
  199. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
  200. package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
  201. package/src/ingest/capture-wait-report.ts +122 -0
  202. package/src/ingest/deferred-ingest-play.ts +216 -0
  203. package/src/ingest/deferred-ingest-session.ts +23 -0
  204. package/src/ingest/discovery-public-ingest.ts +242 -0
  205. package/src/ingest/dom-stub-mark.ts +7 -0
  206. package/src/ingest/entry-load.ts +56 -0
  207. package/src/ingest/game-contract-realm.ts +34 -0
  208. package/src/ingest/game-pointer-lock.ts +89 -0
  209. package/src/ingest/held-scene-repaint.ts +73 -0
  210. package/src/ingest/host-surface-box.ts +174 -0
  211. package/src/ingest/ingest-boot-viewport.ts +54 -0
  212. package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
  213. package/src/ingest/ingest-canvas-scene.ts +50 -0
  214. package/src/ingest/ingest-evidence-hook.ts +131 -0
  215. package/src/ingest/ingest-frame-snapshot.ts +183 -0
  216. package/src/ingest/ingest-play-commands.ts +134 -0
  217. package/src/ingest/ingest-play-control.ts +299 -0
  218. package/src/ingest/ingest-render-debug.ts +320 -0
  219. package/src/ingest/ingest-siblings.ts +487 -0
  220. package/src/ingest/ingest-status.ts +62 -0
  221. package/src/ingest/live-ingest-facet.ts +46 -0
  222. package/src/ingest/module-mode.ts +229 -0
  223. package/src/ingest/mount-canvas-ingest-root.ts +899 -0
  224. package/src/ingest/mount-coverage.ts +296 -0
  225. package/src/ingest/mount-dom-ingest-root.ts +282 -0
  226. package/src/ingest/mount-ingest-root.ts +744 -0
  227. package/src/ingest/mount-three-ingest-root.ts +366 -0
  228. package/src/ingest/resolve-canvas.ts +47 -0
  229. package/src/ingest/resolve-three.ts +123 -0
  230. package/src/ingest/served-bundle.ts +109 -0
  231. package/src/ingest/served-html-boot.ts +292 -0
  232. package/src/ingest/surface-canvas.ts +84 -0
  233. package/src/ingest/surface-dom.ts +84 -0
  234. package/src/ingest/surface-three.ts +128 -0
  235. package/src/ingest/types.ts +76 -0
  236. package/src/ingest/unmount-ingest-root.ts +224 -0
  237. package/src/navmesh/navmesh-actions.ts +27 -0
  238. package/src/navmesh/navmesh-handler.ts +237 -0
  239. package/src/navmesh/navmesh-workflow-store.ts +78 -0
  240. package/src/network/NetworkInspectorPanel.tsx +644 -0
  241. package/src/network/network-inspector-model.ts +225 -0
  242. package/src/play/play-boot-stall.ts +118 -0
  243. package/src/play/play-log-events.ts +26 -0
  244. package/src/play/play-mode.ts +2553 -0
  245. package/src/play/play-recording.ts +335 -0
  246. package/src/play/react-play-live-authoring.ts +165 -0
  247. package/src/play-bar/PlayBar.tsx +488 -0
  248. package/src/play-bar/PlayerCountPicker.tsx +100 -0
  249. package/src/profiler/FrameDebuggerPanel.tsx +458 -0
  250. package/src/profiler/PerformancePanel.tsx +1068 -0
  251. package/src/profiler/ProfilerPanel.tsx +53 -0
  252. package/src/profiler/frame-debugger-store.ts +97 -0
  253. package/src/profiler/main-thread-busy.ts +90 -0
  254. package/src/react/design-time-react-mount.ts +782 -0
  255. package/src/react/dom-authoring-adapter.ts +764 -0
  256. package/src/react/pasteboard-materialize.ts +154 -0
  257. package/src/react/react-inspector-section.tsx +2496 -0
  258. package/src/react/react-world-authoring-adapter.ts +3795 -0
  259. package/src/react/story-documents/story-args-section.ts +19 -0
  260. package/src/react/story-documents/story-documents.tsx +1380 -0
  261. package/src/react/story-paint-bounds.ts +94 -0
  262. package/src/react/ui-board-document.tsx +101 -0
  263. package/src/react/ui-board-title.ts +9 -0
  264. package/src/react/ui-component-board.ts +72 -0
  265. package/src/services/audio-pose-guard.ts +81 -0
  266. package/src/services/game-audio-unlock.ts +48 -0
  267. package/src/state-watch/StateWatchPanel.tsx +535 -0
  268. package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
  269. package/src/three/authoring/constraint-inspector-section.tsx +189 -0
  270. package/src/three/authoring/design-time-renderer.ts +189 -0
  271. package/src/three/authoring/model-asset-inspector-section.css +41 -0
  272. package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
  273. package/src/three/authoring/oid-source-persistence.ts +557 -0
  274. package/src/three/authoring/r3f-design-session.ts +1174 -0
  275. package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
  276. package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
  277. package/src/three/authoring/spatial-audio-handles.ts +215 -0
  278. package/src/three/authoring/spatial-collider-handles.ts +229 -0
  279. package/src/three/authoring/spatial-joint-handles.ts +114 -0
  280. package/src/three/authoring/spatial-light-handles.ts +178 -0
  281. package/src/three/authoring/spatial-lod-handles.ts +93 -0
  282. package/src/three/authoring/spatial-particle-handles.ts +368 -0
  283. package/src/three/authoring/three-authoring-adapter.ts +1953 -0
  284. package/src/three/authoring/three-scene-identity.ts +19 -0
  285. package/src/three/authoring/three-spatial-handles.ts +161 -0
  286. package/src/three/authoring/typed-three-inspector.ts +528 -0
  287. package/src/three/component-verbs/extract-menu.ts +71 -0
  288. package/src/three/component-verbs/fork-menu.ts +78 -0
  289. package/src/three/component-verbs/internals-menu.ts +91 -0
  290. package/src/three/story-documents/three-story-documents.tsx +607 -0
  291. package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
  292. package/src/three/three-board/board-framing.ts +412 -0
  293. package/src/three/three-board/board-layout.ts +401 -0
  294. package/src/three/three-board/board-scene.ts +901 -0
  295. package/src/three/three-board/three-component-board.ts +62 -0
  296. package/src/xstate/XStateBehaviorSection.tsx +130 -0
  297. package/src/xstate/XStateMachineInspector.tsx +566 -0
  298. package/src/xstate/character-animation-machine.fixture.ts +74 -0
  299. package/src/xstate/live-behaviors.ts +106 -0
  300. package/src/xstate/use-live-actor-state.ts +44 -0
  301. package/src/xstate/xstate-graph.ts +235 -0
  302. package/src/xstate/xstate-layout.ts +76 -0
@@ -0,0 +1,1587 @@
1
+ /**
2
+ * CAPABILITY COVERAGE — one derivation over whichever protocol families a
3
+ * mounted subject can honestly measure.
4
+ *
5
+ * ## The defect this closes
6
+ *
7
+ * An ingested game that declares no integration seams does not fail. It
8
+ * mounts, it draws, and the editor simply … has less: ▶ does not control its
9
+ * frames, an edit cannot be written anywhere. Every one of those is measured somewhere in this
10
+ * codebase already — `adapter-reach.ts` measures what the editor reaches,
11
+ * `ingest/same-realm-loop-gate.ts`
12
+ * probes the loop, `game-contract.ts` reads the declared endpoints (its
13
+ * `systems` surface included, which `adapter/ingest/contract-debug-adapter.ts`
14
+ * projects onto `game.commands()`/`game.state()`),
15
+ * `authoring/ingest-source-persistence.ts` asks the server who owns the bytes —
16
+ * and none of it was ever ASSEMBLED into one answer a reader could act on. The
17
+ * absence stayed silent, so the user's model of "what this editor can do"
18
+ * quietly diverged from the truth per game.
19
+ *
20
+ * This began as the shelf-badge derivation (ARCHITECTURE-CORE §Foreign games: a
21
+ * capability list "DERIVED, never hand-written, from all five seam families")
22
+ * built early and pointed at the LIVE mount instead of a gallery card. It now
23
+ * receives authoring reach from each mounted root, observation from the game
24
+ * and system registry, and verbs from the project. A family the subject does
25
+ * not own is omitted and produces no placeholder rows.
26
+ *
27
+ * ## The rules it is written under
28
+ *
29
+ * - **Every row is a measurement.** Nothing here consults the route, the
30
+ * manifest's promises, or the game's id. There is no per-game text of any
31
+ * kind — a row's words come from the seam vocabulary plus values the probes
32
+ * actually returned. A game this file has never heard of gets the same
33
+ * report as one it has.
34
+ * - **Unmeasured is its own answer.** A probe that has not run yet reports
35
+ * `info`, never `ok` and never `gap` — a verdict is a measurement with a
36
+ * timestamp (`ingest/same-realm-loop-gate.ts`), and the honest report of a missing
37
+ * measurement is "not measured", with what was looked at.
38
+ * - **A fix names a mechanism, not an aspiration.** Where the only mechanism
39
+ * is one that does not exist yet, the row says so in those words rather
40
+ * than implying the user has a move they do not have.
41
+ *
42
+ * Pure and injectable — every input arrives as data, so the whole derivation
43
+ * is unit-testable with no browser (`packages/editor/test/capability-coverage.test.ts`).
44
+ * `ingest/mount-coverage.ts` assembles the facts from the live singletons.
45
+ */
46
+
47
+ import { commandLine } from '@volter/editor-core/product-command';
48
+ import type {
49
+ AuthoringProviderKey,
50
+ SeamEvidenceVerdict,
51
+ WriteAnchorKind,
52
+ } from '@volter/editor-project/adapter';
53
+ import { WRITE_ANCHOR_KINDS } from '@volter/editor-project/adapter';
54
+ import type { AdapterSurface } from '@volter/editor-project/adapter/adapter-surface';
55
+ import type { VgaiGameContract } from '@volter/editor-project/adapter/ingest/game-contract';
56
+ import type { SystemAdapters } from '@volter/editor-project/adapter/system-adapter';
57
+ import { SYSTEM_ADAPTERS_SHAPE } from '@volter/editor-project/adapter/system-seam-contract';
58
+ import {
59
+ type AdapterReach,
60
+ AUTHORING_PROVIDER_KEYS,
61
+ CAPTURE_GAP,
62
+ HIERARCHY_RECIPROCITY_GAP,
63
+ notApplicableReason,
64
+ PROVIDER_GAP,
65
+ } from '../adapter-reach';
66
+ import {
67
+ type GameContractEvidence,
68
+ inspectGameContractSeams,
69
+ } from './game-contract-seam-evidence';
70
+ import type { MeasuredLoop } from '../same-realm-loop-gate';
71
+ import {
72
+ assertCoverageReconciles,
73
+ deriveCoverageAccounting,
74
+ formatCoverageGapHeadline,
75
+ } from './coverage-accounting';
76
+
77
+ export type { CoverageAccounting, CoverageAccountingRow } from './coverage-accounting';
78
+ export {
79
+ assertCoverageReconciles,
80
+ coverageSeamFamily,
81
+ deriveCoverageAccounting,
82
+ formatCoverageGapHeadline,
83
+ } from './coverage-accounting';
84
+
85
+ /**
86
+ * The seams a report can speak about.
87
+ *
88
+ * The `editor.*` family is GENERATED — it is the `AuthoringAdapter` provider
89
+ * vocabulary (`@volter/editor-project/adapter/authoring`'s `AUTHORING_PROVIDER_KEYS`, which
90
+ * the compiler pins to the interface) plus the one non-provider fact,
91
+ * `editor.capture`. A capability nobody remembered to enumerate therefore still
92
+ * gets a row.
93
+ *
94
+ * The rest are contract facts about an ingested game — declarations only a
95
+ * `window.vgaiGame` can make — and they grow with the contract; nothing
96
+ * switches exhaustively on this type, so no consumer breaks when it does.
97
+ */
98
+ export type CapabilityCoverageSeam =
99
+ | 'editor.capture'
100
+ | `editor.${AuthoringProviderKey}`
101
+ | 'loop'
102
+ | 'contract.root'
103
+ | 'contract.lifecycle.start'
104
+ | 'contract.lifecycle.pause'
105
+ | 'persistence'
106
+ | 'systems'
107
+ | `system.${SystemAdapterSlot}`
108
+ | `project.${ProjectVerbSlot}`
109
+ | 'authoring.scenes'
110
+ | 'authoring.pieces';
111
+
112
+ /** The `SystemAdapters` slots. The type is `keyof SystemAdapters`, so a slot
113
+ * added to the contract cannot be forgotten here. */
114
+ export type SystemAdapterSlot = keyof SystemAdapters;
115
+
116
+ /**
117
+ * THE NATIVE VERBS THAT LIVE OUTSIDE BOTH DERIVED FAMILIES.
118
+ *
119
+ * `editor.*` is generated from the `AuthoringAdapter` contract and `system.*`
120
+ * from `SystemAdapters`, so between them they exhaust what an ADAPTER can be
121
+ * asked. This capability is not an adapter's to answer at all — it is a fact
122
+ * about the PROJECT, and a project can be green in both other families while
123
+ * lacking it:
124
+ *
125
+ * - `export` — whether this project can produce the standalone build that is
126
+ * the ONLY way it runs outside the editor.
127
+ *
128
+ * There is no interface to pin this list to, so it is a literal — which is why
129
+ * it is a `Record` (a slot dropped from the vocabulary fails to compile against
130
+ * the union) and why the pure rules that fill it live in one file with one
131
+ * test (`coverage/project-verb-coverage.ts`).
132
+ */
133
+ export type ProjectVerbSlot = 'export';
134
+
135
+ const PROJECT_VERB_PRESENCE: Readonly<Record<ProjectVerbSlot, true>> = {
136
+ export: true,
137
+ };
138
+
139
+ export const PROJECT_VERB_SLOTS: readonly ProjectVerbSlot[] = Object.keys(
140
+ PROJECT_VERB_PRESENCE,
141
+ ) as ProjectVerbSlot[];
142
+
143
+ /**
144
+ * Report ORDER for those slots — a property of the report (two reports of the
145
+ * same mount stay diffable line for line), and pinned to the type by the
146
+ * `Record` below, so the order list cannot silently omit a slot the way a bare
147
+ * array literal could.
148
+ */
149
+ export const SYSTEM_ADAPTER_SLOTS = Object.keys(SYSTEM_ADAPTERS_SHAPE) as SystemAdapterSlot[];
150
+
151
+ /**
152
+ * One seam's verdict.
153
+ *
154
+ * - `ok` — the seam is there and the editor functionality it unlocks works.
155
+ * - `gap` — MEASURED absent; `missing` says what the editor therefore cannot
156
+ * do and `fix` names the mechanism that would close it.
157
+ * - `na` — absent BY DESIGN for this root's surface kind, with the reason in
158
+ * `detail`. Never a way to be quiet about a real absence: the N-A
159
+ * set is a small per-surface table (`adapter-reach.ts`'s
160
+ * `SURFACE_NOT_APPLICABLE`), and anything not in it is a `gap`.
161
+ * - `info` — measured, but not a pass/fail: a fact the reader needs (what was
162
+ * registered, what has not been probed yet, what could not be
163
+ * looked at).
164
+ */
165
+ export interface CapabilityCoverageRow {
166
+ readonly seam: CapabilityCoverageSeam;
167
+ readonly status: 'ok' | 'gap' | 'na' | 'info';
168
+ /** What was measured, in the measurement's own terms. Always present —
169
+ * a row with no evidence behind it has no business existing. */
170
+ readonly detail: string;
171
+ /** The editor functionality this gap disables, in plain language. */
172
+ readonly missing?: string;
173
+ /** The named mechanism that fills it. */
174
+ readonly fix?: string;
175
+ /**
176
+ * WHO made an `empty` row's statement — the game's own declaration, or the
177
+ * host's inference. Structured (not only flattened into `detail`) so
178
+ * consumers can COUNT host inferences: a header reading "0 unanswered"
179
+ * above rows the game never answered was the measured misleading case.
180
+ */
181
+ readonly attestedBy?: 'game' | 'host';
182
+ }
183
+
184
+ export interface CapabilityCoverageSummary {
185
+ readonly worldId: string;
186
+ readonly rows: number;
187
+ readonly gaps: number;
188
+ readonly ok: number;
189
+ readonly na: number;
190
+ readonly info: number;
191
+ }
192
+
193
+ export interface CapabilityCoverageReport {
194
+ readonly summary: CapabilityCoverageSummary;
195
+ readonly rows: readonly CapabilityCoverageRow[];
196
+ }
197
+
198
+ // ─────────────────────────────────────────────────────────── the measurements
199
+
200
+ /** The NAMES a declared `systems` surface exposes, in declaration order. Names
201
+ * rather than a count because the systems row prints them, and they are the
202
+ * same strings `game.commands()`/`game.providers()` list — still presence:
203
+ * nothing here ever calls a command's `run` or a provider's `read`. */
204
+ export interface SystemsPresence {
205
+ readonly commands: readonly string[];
206
+ readonly state: readonly string[];
207
+ }
208
+
209
+ /**
210
+ * Which game→host contract endpoints this mount's realm actually declared.
211
+ * Presence only — the contract's own doctrine is "capabilities by PRESENCE",
212
+ * and this file never calls any of them.
213
+ */
214
+ export interface ContractPresence {
215
+ /** `readGameContract()` answered at all (a v1 `window.vgaiGame`). */
216
+ readonly declared: boolean;
217
+ readonly root: boolean;
218
+ readonly start: boolean;
219
+ readonly pause: boolean;
220
+ readonly resume: boolean;
221
+ /** The declared `systems` surface, or `null` when the contract names none. */
222
+ readonly systems: SystemsPresence | null;
223
+ readonly proof?: GameContractEvidence | null;
224
+ }
225
+
226
+ /** Fold a read contract (or its absence) into presence flags. */
227
+ export function measureGameContract(contract: VgaiGameContract | null): ContractPresence {
228
+ const systems = contract?.systems;
229
+ return {
230
+ declared: contract !== null,
231
+ root: contract?.root !== undefined,
232
+ start: contract?.lifecycle?.start !== undefined,
233
+ pause: contract?.lifecycle?.pause !== undefined,
234
+ resume: contract?.lifecycle?.resume !== undefined,
235
+ systems: systems
236
+ ? {
237
+ commands: (systems.commands ?? []).map((c) => c.name),
238
+ state: (systems.state ?? []).map((p) => p.name),
239
+ }
240
+ : null,
241
+ proof: contract ? inspectGameContractSeams({ contract }) : null,
242
+ };
243
+ }
244
+
245
+ /**
246
+ * The server's answer about who owns this session's base, as
247
+ * `authoring/ingest-source-persistence.ts` caches it. `null` means the
248
+ * question has not been answered yet — and unknown is NOT writable there, so
249
+ * it is not writable here either; it is `info`.
250
+ */
251
+ export interface OwnershipFacts {
252
+ readonly writable: boolean;
253
+ /** The server's own words for why not. */
254
+ readonly reason: string | null;
255
+ /** The server's own words for who records the diff a write produces. `null`
256
+ * when it did not say — which is only ever the not-writable case. */
257
+ readonly recorder?: string | null;
258
+ }
259
+
260
+ /**
261
+ * What the mount's own authoring adapter MEASURED about its world's reach into
262
+ * source: how many projected nodes have a real write address, out of how many
263
+ * exist, and where a write would land when one does.
264
+ */
265
+ export interface WriteReachFacts {
266
+ readonly addressable: number;
267
+ readonly total: number;
268
+ /** The persistence backend's own `destination()` phrase, shown verbatim. */
269
+ readonly destination: string;
270
+ /**
271
+ * The same nodes, counted by the LANE their write would take
272
+ * (`WriteAnchorKind`). Derived from the same planning call `addressable` is,
273
+ * so the two cannot disagree.
274
+ *
275
+ * Why a bare count is not enough: "12 of 37 resolve to a source address"
276
+ * reads healthy while every one of the 12 belongs to one lane and a whole
277
+ * other lane — a world's physics-placed cargo, or its level-data records —
278
+ * goes unwritten and unexercised. A per-kind tally is what makes an
279
+ * exhaustive walk possible; `vgai doctor`'s edit-write phase is its reader.
280
+ *
281
+ * Optional, because callers older than the vocabulary supply none and a
282
+ * fabricated zero for every kind would read as a measurement.
283
+ */
284
+ readonly byKind?: Readonly<Record<WriteAnchorKind, number>>;
285
+ }
286
+
287
+ /**
288
+ * A declared data writer's terminal state — the OTHER half of "can an edit be
289
+ * written back here", for a game that holds part of its truth outside source.
290
+ *
291
+ * `dataFile` is present only when the writer is `ready`, because until the
292
+ * module loads nobody knows which file it names; a surface that printed one
293
+ * beforehand would be printing the manifest's promise as a fact.
294
+ */
295
+ export interface DataWriterFacts {
296
+ readonly state: 'loading' | 'ready' | 'failed';
297
+ readonly dataFile?: string;
298
+ /** The loader's own words when it failed. */
299
+ readonly reason?: string;
300
+ }
301
+
302
+ /**
303
+ * One `SystemAdapters` slot's terminal state, MEASURED two ways and never
304
+ * inferred from the route or the game's id:
305
+ *
306
+ * - `bound` — the live editor-side registry (`authoring/active-systems.ts`)
307
+ * actually holds an adapter for this slot. That is the strongest available
308
+ * statement: it is the same object the editor's panels will call.
309
+ * - `empty` — the game POSITIVELY answered "I have no X" through
310
+ * `window.vgaiGame.systems.systemAdapters` (`evidence` is its own words).
311
+ * - `malformed` — a declaration that could not be honoured; `evidence` is the
312
+ * projection's reason. Reported so a broken shim is loud, never silently
313
+ * read as either of the two terminal states.
314
+ * - `unanswered` — nothing bound and nothing declared. The only state that is
315
+ * a work order.
316
+ */
317
+ export interface SystemAdapterMeasurement {
318
+ readonly slot: SystemAdapterSlot;
319
+ readonly state: 'bound' | 'empty' | 'malformed' | 'unanswered';
320
+ /** The game's absence evidence, or the projection's rejection reason. */
321
+ readonly evidence?: string;
322
+ /** Bound-object availability is not operational proof. */
323
+ readonly proof?: SeamEvidenceVerdict | undefined;
324
+ /**
325
+ * The mechanism that would close this slot, in the MEASURER's own words —
326
+ * supplied when the lane that produced the measurement knows a fix the
327
+ * shared default would state wrongly.
328
+ *
329
+ * This is what keeps the derivation provenance-NEUTRAL. The default fix text
330
+ * below names `window.vgaiGame.systems…`, which is true of a game that
331
+ * declares a contract and false of a first-party one; rather than have the
332
+ * row builder ask WHO produced the measurement, the measurement carries the
333
+ * answer as a fact (`coverage/system-adapter-coverage.ts` supplies it for a
334
+ * native mount; the ingested lane supplies none and gets the contract text).
335
+ */
336
+ readonly fix?: string;
337
+ /**
338
+ * WHO answered an `empty`: the GAME (an `absent(reason)` declaration, or an
339
+ * ingested game's `{ present: false, evidence }` record) or the HOST (the
340
+ * editor's own inference from an unfilled slot on a live mount).
341
+ *
342
+ * Both are legitimate and both are terminal, but they are not the same claim,
343
+ * and the row must not print one as the other — a host inference wearing the
344
+ * game's voice attributes to a game a statement it never made. Absent means
345
+ * `'game'`: the ingest lane's `empty` IS the game's declaration, and that was
346
+ * this field's only producer before the native lane could derive one.
347
+ */
348
+ readonly attestedBy?: 'game' | 'host';
349
+ }
350
+
351
+ /**
352
+ * One `project.*` verb's terminal state — THREE states, and the split between
353
+ * the last two is the whole point:
354
+ *
355
+ * - `present` — the project declares the thing the verb reads, and `evidence`
356
+ * quotes it.
357
+ * - `absent` — MEASURED absent: the file was read and the declaration is not
358
+ * in it. A standing work order.
359
+ * - `unanswered` — nobody looked yet (no server to ask, the fetch has not
360
+ * landed). Never a gap and never an `ok`: `evidence` says what could not be
361
+ * read, so an unread fact can never be mistaken for a clean bill of health.
362
+ *
363
+ * Same shape and same reason as {@link SystemAdapterMeasurement}: the measurer
364
+ * carries its own words, so the row builder below never asks who produced it.
365
+ */
366
+ export interface ProjectVerbMeasurement {
367
+ readonly slot: ProjectVerbSlot;
368
+ readonly state: 'present' | 'absent' | 'unanswered';
369
+ /** What was measured, in the measurement's own terms. Always present — a row
370
+ * with no evidence behind it has no business existing. */
371
+ readonly evidence: string;
372
+ /** Result of executing the real project verb, when Doctor or the verb itself
373
+ * has produced one. A package.json key by itself is not proof. */
374
+ readonly proof?: SeamEvidenceVerdict | undefined;
375
+ /** What the project therefore cannot do. Absent states only. */
376
+ readonly missing?: string;
377
+ /** The named mechanism that fills it. Absent states only. */
378
+ readonly fix?: string;
379
+ }
380
+
381
+ /**
382
+ * The facts one capability subject can honestly answer. Families are optional:
383
+ * a mounted root supplies authoring reach, the game supplies runtime facts, the
384
+ * system registry supplies adapters, and the project supplies verbs. Omission
385
+ * means "this is not my question" and produces no row; an explicit `null`
386
+ * means the family measured the question and has no answer yet.
387
+ */
388
+ export interface CapabilityCoverageFacts {
389
+ readonly worldId: string;
390
+ /** What this mount's authoring surface actually reached, per editor
391
+ * capability (`adapter-reach.ts`) — `null` before anything has measured it.
392
+ * There is nothing to compare it against: the bar is "everything", so a
393
+ * false here is a gap whatever route produced it. */
394
+ readonly reach?: AdapterReach | null;
395
+ /**
396
+ * The root's SURFACE kind, which is what decides whether an absent provider is
397
+ * a gap or absent-by-design (`adapter-reach.ts`'s `SURFACE_NOT_APPLICABLE`).
398
+ * `null` ⇒ the caller could not say, and NOTHING is excused — an unknown
399
+ * surface reports every absence as a gap rather than inventing an exemption.
400
+ */
401
+ readonly surface?: AdapterSurface | null;
402
+ /** How this mount reached (or failed to reach) the game's runtime — the
403
+ * capture route in the mount's own words. Diagnosis a reader needs to act
404
+ * on a fix, never a verdict about how much is acceptable. */
405
+ readonly reachMechanism?: string | null;
406
+ /**
407
+ * The MEASURED loop verdict AND the evidence behind it
408
+ * (`ingest/same-realm-loop-gate.ts`'s `MeasuredLoop`); `null` before the probe
409
+ * has ever run — or on a route that installs no gate, where
410
+ * {@link loopReason} says so.
411
+ *
412
+ * A pair rather than the bare word because `'gated'`/`'self-driven'` is also
413
+ * the manifest's DECLARED intent vocabulary, and this report may only ever
414
+ * speak the measured one: see that type's doc comment.
415
+ */
416
+ readonly loop?: MeasuredLoop | null;
417
+ /** The probe's own words — or, when `loop` is `null`, why there is no verdict
418
+ * to report. `null` means the probe simply has not run yet. */
419
+ readonly loopReason?: string | null;
420
+ readonly contract?: ContractPresence;
421
+ readonly ownership?: OwnershipFacts | null;
422
+ /**
423
+ * How much of the MOUNTED WORLD an edit can be written back to — `null` when
424
+ * nothing measured it (a route with no three authoring surface).
425
+ *
426
+ * Separate from {@link ownership} because they answer different questions and
427
+ * the row needs both: ownership is about the FOLDER (may these bytes be
428
+ * written, and who records the diff), reach is about the OBJECTS (does any of
429
+ * them have a source address at all). A writable folder whose every object is
430
+ * unaddressable is precisely the state the persistence row used to report as
431
+ * `ok` — measured on a react-three-fiber game, where every object is
432
+ * constructed inside `node_modules` and no creation site in the game's own
433
+ * source names one.
434
+ */
435
+ readonly writeReach?: WriteReachFacts | null;
436
+ /** The state of this game's DECLARED data writer, or `null` when it declares
437
+ * none — which is the ordinary case and not a gap: most games hold nothing
438
+ * authorable outside their source. */
439
+ readonly dataWriter?: DataWriterFacts | null;
440
+ /** A completed write→source→cold-remount→revert proof. Ownership and source
441
+ * addressability alone are only preconditions. */
442
+ readonly persistenceProof?: SeamEvidenceVerdict | undefined;
443
+ /** One entry per `SystemAdapters` slot. An EMPTY array means the caller had
444
+ * no live mount to ask, and the rows say so rather than reporting six gaps
445
+ * against a mount that does not exist. */
446
+ readonly systemAdapters?: readonly SystemAdapterMeasurement[];
447
+ /** One entry per native verb (`coverage/project-verb-coverage.ts`). EMPTY
448
+ * means this caller's subject is not a project — the same convention
449
+ * {@link CapabilityCoverageFacts.systemAdapters} uses, and for the same reason:
450
+ * three fabricated gaps are worse than no rows. */
451
+ readonly projectVerbs?: readonly ProjectVerbMeasurement[];
452
+ /** The visible Edit tabs and Content pieces derived from the project's
453
+ * resolved scene table. `null` means the table is not measurable yet. */
454
+ readonly authoringSurface?: AuthoringSurfaceFacts | null;
455
+ }
456
+
457
+ export interface AuthoringSurfaceFacts {
458
+ readonly sceneDocuments: number;
459
+ readonly isolationDocuments: number;
460
+ readonly openIsolationDocuments: number;
461
+ readonly availableIsolationDocuments: number;
462
+ readonly pieces: number;
463
+ readonly sceneEntries: number;
464
+ }
465
+
466
+ /** Required only inside the row rules. Public callers never manufacture this
467
+ * shape; {@link deriveCapabilityCoverage} normalizes the families they did
468
+ * supply and invokes only those families. */
469
+ type ResolvedCoverageFacts = Required<CapabilityCoverageFacts>;
470
+
471
+ // ───────────────────────────────────────────────────────── the fix vocabulary
472
+
473
+ /**
474
+ * The route for a game whose bytes may not be touched — which is EVERY
475
+ * repo-vendored ingest, by doctrine. "Declare `window.vgaiGame.root`" is the
476
+ * mechanism, but a game nobody may edit cannot declare anything, so a fix that
477
+ * stopped there would be telling the reader to do something they are not
478
+ * allowed to do. Both host-side carriers now exist, so this names them and
479
+ * where to put one.
480
+ */
481
+ const UNMODIFIABLE_ROUTE =
482
+ 'a game whose bytes must stay unmodified declares it from a host-side carrier ' +
483
+ 'instead — a contract shim beside the game (`ingest.contractShim` in its ' +
484
+ 'vgai.project.json, injected ahead of the game`s entry module; see ' +
485
+ 'public/ingest/simcity/vgai.shim.js) or a recorded patch';
486
+
487
+ const CONTRACT_FIX = (endpoint: string): string =>
488
+ `declare ${endpoint} in the game's own entry; ${UNMODIFIABLE_ROUTE}`;
489
+
490
+ // ────────────────────────────────────────────────────────────── the row logic
491
+
492
+ /**
493
+ * THE GENERATED FAMILY — one row per member of the authoring contract, plus the
494
+ * capture fact they all stand on.
495
+ *
496
+ * The bar is a native root's own capability, so there is nothing to compare
497
+ * against and no summary verdict to compute: a capability this root did not
498
+ * reach is a `gap` that keeps being reported for as long as it is missing, one
499
+ * it did reach is silent-adjacent (`ok`), and one its surface cannot have is
500
+ * `na` WITH THE REASON. The route that produced the mount appears only inside
501
+ * `detail`, as the mechanism a reader has to act on.
502
+ *
503
+ * Nothing in here names a provider: the loop is over
504
+ * `AUTHORING_PROVIDER_KEYS`, so this function does not change when the contract
505
+ * grows, and a provider that reaches the contract without reaching this report
506
+ * is impossible by construction.
507
+ */
508
+ function editorSurfaceRows(facts: ResolvedCoverageFacts): readonly CapabilityCoverageRow[] {
509
+ const because = facts.reachMechanism ? ` — ${facts.reachMechanism}` : '';
510
+ if (facts.reach === null) {
511
+ return [
512
+ {
513
+ seam: 'editor.capture',
514
+ status: 'info',
515
+ detail: 'no mount has measured what the editor reaches on this root yet',
516
+ },
517
+ ...AUTHORING_PROVIDER_KEYS.map(
518
+ (key): CapabilityCoverageRow => ({
519
+ seam: `editor.${key}`,
520
+ status: 'info',
521
+ detail: `no mount has measured whether this root exposes ${key} yet`,
522
+ }),
523
+ ),
524
+ ];
525
+ }
526
+ const reach = facts.reach;
527
+ const captureRow: CapabilityCoverageRow = reach.captured
528
+ ? reach.captureEvidence?.state === 'verified'
529
+ ? {
530
+ seam: 'editor.capture',
531
+ status: 'ok',
532
+ // The VERDICT'S OWN sentence, not a restatement of it. The walk
533
+ // reports how many nodes it visited and whether the budget bounded
534
+ // it, and a reader checking a "0 gaps" report needs that number —
535
+ // a generic "exercised its hierarchy" reads identically over a
536
+ // twelve-node scaffold and a truncated 24,000-node translation.
537
+ detail: `the editor reached this root's own runtime and exercised its hierarchy — ${reach.captureEvidence.detail}${because}`,
538
+ }
539
+ : reach.captureEvidence?.state === 'failed'
540
+ ? {
541
+ seam: 'editor.capture',
542
+ status: 'gap',
543
+ detail: `an authoring object was captured, but its hierarchy seam failed — ${reach.captureEvidence.detail}${because}`,
544
+ // NOT `CAPTURE_GAP`: the editor DID reach this root. Spending the
545
+ // never-reached words here told a reader with a working hierarchy
546
+ // panel that "not one object in it can be listed" and pointed them
547
+ // at a bare `import 'three'` their game already has — a row that
548
+ // reads as a false alarm and teaches readers to skip the report.
549
+ missing: HIERARCHY_RECIPROCITY_GAP.missing,
550
+ fix: HIERARCHY_RECIPROCITY_GAP.fix,
551
+ }
552
+ : {
553
+ seam: 'editor.capture',
554
+ status: 'info',
555
+ // Reachable only when NO hierarchy operation was recorded at all
556
+ // — an injected fixture, or a reach measured without the read
557
+ // probe. Every live mount runs the probe, so a `--template game`
558
+ // scaffold reading this line is the bug, not the honest answer;
559
+ // it was the permanent answer until `captureEvidence` stopped
560
+ // reading the hierarchy CARRIER grade (`adapter-reach.ts`).
561
+ detail: `an authoring object was captured, but no current-epoch hierarchy operation proves it projects this root's runtime${
562
+ reach.captureEvidence ? ` — ${reach.captureEvidence.detail}` : ''
563
+ }${because}`,
564
+ }
565
+ : {
566
+ seam: 'editor.capture',
567
+ status: 'gap',
568
+ detail: `the editor never reached this root's runtime${because}`,
569
+ missing: CAPTURE_GAP.missing,
570
+ fix: CAPTURE_GAP.fix,
571
+ };
572
+ const providerRows = AUTHORING_PROVIDER_KEYS.map((key): CapabilityCoverageRow => {
573
+ const seam = `editor.${key}` as const;
574
+ const evidence = reach.providerEvidence?.[key];
575
+ const naReason = notApplicableReason(facts.surface, key);
576
+ if (!reach.providers[key] && naReason !== null) {
577
+ return {
578
+ seam,
579
+ status: 'na',
580
+ detail: `not applicable to a ${facts.surface} root — ${naReason}`,
581
+ };
582
+ }
583
+ if (evidence?.state === 'verified') {
584
+ return { seam, status: 'ok', detail: evidence.detail };
585
+ }
586
+ if (evidence?.state === 'failed') {
587
+ return {
588
+ seam,
589
+ status: 'gap',
590
+ detail: `${key} is exposed but malformed or failed — ${evidence.detail}${because}`,
591
+ missing: PROVIDER_GAP[key].missing,
592
+ fix: PROVIDER_GAP[key].fix,
593
+ };
594
+ }
595
+ if (reach.providers[key]) {
596
+ return {
597
+ seam,
598
+ status: 'info',
599
+ detail:
600
+ evidence?.detail ?? `this root exposes ${key}, but no operational proof was recorded`,
601
+ };
602
+ }
603
+ return {
604
+ seam,
605
+ status: 'gap',
606
+ detail: `this root exposes no ${key} provider${because}`,
607
+ missing: PROVIDER_GAP[key].missing,
608
+ fix: PROVIDER_GAP[key].fix,
609
+ };
610
+ });
611
+ return [captureRow, ...providerRows];
612
+ }
613
+
614
+ /**
615
+ * Does anything in this editor control the game's frames?
616
+ *
617
+ * TWO mechanisms can answer it, and the row must read the one the ingest
618
+ * adapter ACTUALLY bound: the game's own declared `lifecycle.pause`/`resume`
619
+ * pair, otherwise the host's outside-in gate. So a
620
+ * game that declares both HAS answered this capability — the gate's verdict is
621
+ * then a fact about a fallback nobody is using, and reporting it as a gap makes
622
+ * this report contradict itself two rows down (the shape a live mount of a
623
+ * pause-declaring game produced: `contract.lifecycle.pause ✓` beside
624
+ * `loop ✗ declare lifecycle.pause`).
625
+ *
626
+ * The gate verdict is the answer only for a game with no declared pause, and
627
+ * `null` there is genuinely unmeasured — never a gap.
628
+ */
629
+ function loopRow(facts: ResolvedCoverageFacts): CapabilityCoverageRow {
630
+ const because = facts.loopReason ? ` — ${facts.loopReason}` : '';
631
+ const { pause, resume } = facts.contract;
632
+ if (
633
+ pause &&
634
+ resume &&
635
+ facts.contract.proof?.pause.state === 'verified' &&
636
+ facts.contract.proof.resume.state === 'verified'
637
+ ) {
638
+ return {
639
+ seam: 'loop',
640
+ status: 'ok',
641
+ detail:
642
+ "the game's own declared lifecycle.pause + resume control its frames, and that is what " +
643
+ `▶/⏸ call${facts.loopReason ? ` (the host's fallback gate, unused here, reports: ${facts.loopReason})` : ''}`,
644
+ };
645
+ }
646
+ if (facts.loop?.verdict === 'gated') {
647
+ return { seam: 'loop', status: 'ok', detail: `gated${because}` };
648
+ }
649
+ if (facts.loop?.verdict === 'self-driven') {
650
+ return {
651
+ seam: 'loop',
652
+ status: 'gap',
653
+ detail: `self-driven${because}`,
654
+ missing:
655
+ "nothing in this editor controls the game's frames: ▶/⏸ and Step do not stop it, and " +
656
+ 'Edit mode cannot be quiet — it keeps simulating while you author',
657
+ fix: CONTRACT_FIX('window.vgaiGame.lifecycle.pause + resume'),
658
+ };
659
+ }
660
+ // No verdict, and that is its own answer — never a gap. The caller supplies
661
+ // the truth for its own route when there is one (a route with no gate at all
662
+ // says so); the pending sentence is the fallback for a route that really does
663
+ // have a gate waiting to be probed.
664
+ return {
665
+ seam: 'loop',
666
+ status: 'info',
667
+ detail:
668
+ facts.loopReason ??
669
+ 'no loop verdict has been measured yet — the probe runs at the first pause',
670
+ };
671
+ }
672
+
673
+ function contractRootRow(facts: ResolvedCoverageFacts): CapabilityCoverageRow {
674
+ if (facts.contract.root) {
675
+ if (facts.contract.proof?.root.state === 'failed') {
676
+ return {
677
+ seam: 'contract.root',
678
+ status: 'gap',
679
+ detail: `the game declares a root, but the seam failed — ${facts.contract.proof.root.detail}`,
680
+ missing:
681
+ 'the host cannot trust that the declared element owns the game DOM rather than an unrelated or detached subtree',
682
+ fix: CONTRACT_FIX('window.vgaiGame.root'),
683
+ };
684
+ }
685
+ return {
686
+ seam: 'contract.root',
687
+ status: facts.contract.proof?.root.state === 'verified' ? 'ok' : 'info',
688
+ detail:
689
+ facts.contract.proof?.root.state === 'verified'
690
+ ? facts.contract.proof.root.detail
691
+ : 'the game declares a root element, but no consumer has proved it owns the mounted game DOM',
692
+ };
693
+ }
694
+ return {
695
+ seam: 'contract.root',
696
+ status: 'gap',
697
+ detail: facts.contract.declared
698
+ ? 'a contract is declared, but it names no root element'
699
+ : 'no game→host contract is declared at all',
700
+ missing:
701
+ 'the host adopts the bare canvas, so any DOM this game owns beside it (HUD, overlays, ' +
702
+ 'portals) is stranded at page level over the editor chrome instead of living in the ' +
703
+ 'game pane',
704
+ fix: CONTRACT_FIX('window.vgaiGame.root'),
705
+ };
706
+ }
707
+
708
+ function contractStartRow(facts: ResolvedCoverageFacts): CapabilityCoverageRow {
709
+ if (facts.contract.start) {
710
+ if (facts.contract.proof?.start.state === 'failed') {
711
+ return {
712
+ seam: 'contract.lifecycle.start',
713
+ status: 'gap',
714
+ detail: `lifecycle.start is declared but failed — ${facts.contract.proof.start.detail}`,
715
+ missing:
716
+ 'the mount cannot prove it starts cold and enters a session only when Play requests one',
717
+ fix: CONTRACT_FIX('window.vgaiGame.lifecycle.start'),
718
+ };
719
+ }
720
+ return {
721
+ seam: 'contract.lifecycle.start',
722
+ status: facts.contract.proof?.start.state === 'verified' ? 'ok' : 'info',
723
+ detail:
724
+ facts.contract.proof?.start.state === 'verified'
725
+ ? facts.contract.proof.start.detail
726
+ : 'the game declares lifecycle.start, but no current-epoch effect proves that ▶ starts the session',
727
+ };
728
+ }
729
+ return {
730
+ seam: 'contract.lifecycle.start',
731
+ status: 'gap',
732
+ detail: facts.contract.declared
733
+ ? 'a contract is declared, but it names no lifecycle.start'
734
+ : 'no game→host contract is declared at all',
735
+ missing:
736
+ 'the mount cannot be COLD: this game runs its session side-effects (backend ' +
737
+ 'connections, audio, narrative) as it loads, so opening it in the editor starts ' +
738
+ 'playing it, and ▶ is a wire event the game may ignore',
739
+ fix: CONTRACT_FIX('window.vgaiGame.lifecycle.start'),
740
+ };
741
+ }
742
+
743
+ /**
744
+ * What falling back to the host's loop gate actually costs — DERIVED from the
745
+ * measured loop verdict, not asserted.
746
+ *
747
+ * This row used to state flatly that the host gate "cannot reach a raw
748
+ * requestAnimationFrame loop at all", which was true before the gate existed and
749
+ * is now the opposite of what the probe measures: the same report would print
750
+ * `loop ✓ gated` two lines above this row's claim that ⏸ does not work. Two rows
751
+ * of one report contradicting each other about one mechanism is exactly the
752
+ * dishonesty the coverage derivation exists to end, so the fallback's cost is
753
+ * read off the same measurement the loop row reads.
754
+ */
755
+ function pauseFallbackCost(facts: ResolvedCoverageFacts): string {
756
+ if (facts.loop?.verdict === 'gated') {
757
+ return (
758
+ "pausing this game is the host's outside-in loop gate only. That gate is MEASURED to hold " +
759
+ 'the work this game schedules, so ⏸ does stop it — but a host reaching in is not the game ' +
760
+ 'quieting itself: anything it drives from outside its own scheduling (a worker, a socket, ' +
761
+ 'a media callback) keeps running, and it gets no chance to stop its audio, drop its ' +
762
+ 'connections, or checkpoint before the pause'
763
+ );
764
+ }
765
+ if (facts.loop?.verdict === 'self-driven') {
766
+ return (
767
+ "pausing this game is the host's outside-in loop gate only, and that gate is MEASURED not " +
768
+ 'to control this loop — so ⏸ may freeze the picture while the game keeps running ' +
769
+ 'underneath it'
770
+ );
771
+ }
772
+ return (
773
+ "pausing this game is the host's outside-in loop gate only, and no probe has measured " +
774
+ 'whether that gate reaches this loop yet — so ⏸ may freeze the picture while the game keeps ' +
775
+ 'running underneath it'
776
+ );
777
+ }
778
+
779
+ function contractPauseRow(facts: ResolvedCoverageFacts): CapabilityCoverageRow {
780
+ const { pause, resume, declared } = facts.contract;
781
+ if (pause && resume) {
782
+ const pauseVerified = facts.contract.proof?.pause.state === 'verified';
783
+ const resumeVerified = facts.contract.proof?.resume.state === 'verified';
784
+ const failed = [facts.contract.proof?.pause, facts.contract.proof?.resume].find(
785
+ (proof) => proof?.state === 'failed',
786
+ );
787
+ if (failed) {
788
+ return {
789
+ seam: 'contract.lifecycle.pause',
790
+ status: 'gap',
791
+ detail: `pause/resume is declared but failed — ${failed.detail}`,
792
+ missing: pauseFallbackCost(facts),
793
+ fix: CONTRACT_FIX('window.vgaiGame.lifecycle.pause + resume'),
794
+ };
795
+ }
796
+ return {
797
+ seam: 'contract.lifecycle.pause',
798
+ status: pauseVerified && resumeVerified ? 'ok' : 'info',
799
+ detail:
800
+ pauseVerified && resumeVerified
801
+ ? 'the mounted lifecycle exercised both declared pause + resume effects'
802
+ : 'the game declares pause + resume, but declaration alone does not prove either changes its frames',
803
+ };
804
+ }
805
+ const half = missingPauseHalf(pause, resume);
806
+ return {
807
+ seam: 'contract.lifecycle.pause',
808
+ status: 'gap',
809
+ detail: half
810
+ ? `the contract declares ${half} — the host needs both to hand control back`
811
+ : declared
812
+ ? 'a contract is declared, but it names no lifecycle.pause/resume'
813
+ : 'no game→host contract is declared at all',
814
+ missing: pauseFallbackCost(facts),
815
+ fix: CONTRACT_FIX('window.vgaiGame.lifecycle.pause + resume'),
816
+ };
817
+ }
818
+
819
+ function missingPauseHalf(pause: boolean, resume: boolean): string | null {
820
+ if (pause) return 'lifecycle.pause without a resume';
821
+ if (resume) return 'lifecycle.resume without a pause';
822
+ return null;
823
+ }
824
+
825
+ /**
826
+ * THE MEASUREMENT BEATS THE PERMISSION.
827
+ *
828
+ * A writable base is a fact about the FOLDER; it says nothing about whether any
829
+ * object in the world that is actually mounted has a source address. On a
830
+ * react-three-fiber game every object is constructed inside `node_modules`, so
831
+ * the creation-site index knows none of them — and the persistence row read
832
+ * `ok` over a world where not one edit could ever leave the session. Reported
833
+ * reach of ZERO is a gap however writable the folder is, and the mechanism is
834
+ * named so the reader can act on it.
835
+ */
836
+ /**
837
+ * The per-lane breakdown, in vocabulary order — `` when the measurement carries
838
+ * none, and only the kinds this world actually has, because a row that printed
839
+ * `data-record 0` for every game would bury the two counts that matter.
840
+ */
841
+ function byKindLeg(reach: WriteReachFacts | null): string {
842
+ const byKind = reach?.byKind;
843
+ if (!byKind) return '';
844
+ const named = WRITE_ANCHOR_KINDS.filter((kind: WriteAnchorKind) => (byKind[kind] ?? 0) > 0).map(
845
+ (kind: WriteAnchorKind) => `${kind} ${byKind[kind]}`,
846
+ );
847
+ // "lane", not "anchor": a node can belong to a lane whose ADDRESS this tier
848
+ // has not resolved (a stamped object the client index has not answered for
849
+ // yet still writes through the server), so the two counts are not the same
850
+ // question and the row must not read as though they were.
851
+ return named.length > 0 ? ` (by write lane: ${named.join(', ')})` : '';
852
+ }
853
+
854
+ function unreachableWorldRow(reach: WriteReachFacts): CapabilityCoverageRow {
855
+ return {
856
+ seam: 'persistence',
857
+ status: 'gap',
858
+ detail:
859
+ `this game's base is writable, but NONE of the ${reach.total} object(s) the editor ` +
860
+ `projects from the mounted world has a source address — every edit stays ${reach.destination}` +
861
+ byKindLeg(reach),
862
+ missing:
863
+ 'nothing you change here survives the session: the base may be written, but no line of ' +
864
+ "this game's own source is known to address any object in it",
865
+ fix:
866
+ "serve this game's own source through the editor's authoring transform so its elements " +
867
+ 'carry source stamps (a JSX world), or give it construction sites the creation-site ' +
868
+ "index can see (a `new` expression in the game's own served module)",
869
+ };
870
+ }
871
+
872
+ /** The writable-base row, where the two destinations a game can have are
873
+ * spelled out. Split out of {@link persistenceRow} for readability only — the
874
+ * conditions are unchanged. */
875
+ function writableBaseRow(
876
+ ownership: OwnershipFacts,
877
+ reach: WriteReachFacts | null,
878
+ data: DataWriterFacts | null,
879
+ proof: SeamEvidenceVerdict | undefined,
880
+ ): CapabilityCoverageRow {
881
+ const recorder = ownership.recorder ?? 'whatever records this folder';
882
+ const reachLeg = reach
883
+ ? `; ${reach.addressable} of ${reach.total} projected object(s) resolve to one, and the rest are runtime or library parts with no source of their own${byKindLeg(reach)}`
884
+ : '';
885
+ // TWO destinations, and a game can have both. Source write-back reaches
886
+ // whatever a `new` expression or a JSX literal spells; a declared data writer
887
+ // reaches what the game placed from its own level file and no literal
888
+ // mentions. Naming only the first would report a game as less writable than
889
+ // it is — and naming the second where none is declared would be the opposite
890
+ // lie, which is why this reads the writer's real state rather than assuming
891
+ // one.
892
+ const dataStalled = data !== null && data.state !== 'ready';
893
+ const dataLeg = writableDataLeg(data);
894
+ if (proof?.state === 'failed') {
895
+ return {
896
+ seam: 'persistence',
897
+ status: 'gap',
898
+ detail: `the write path is addressable, but its round trip failed — ${proof.detail}`,
899
+ missing:
900
+ 'the editor can issue a write but cannot trust that the game source and a cold remount reflect it',
901
+ fix: 'fix the failing write receipt named above; provider presence and a write acknowledgement are not persistence',
902
+ };
903
+ }
904
+ return {
905
+ seam: 'persistence',
906
+ status: dataStalled ? 'gap' : proof?.state === 'verified' ? 'ok' : 'info',
907
+ detail:
908
+ "this game's base is writable, so an edit can be written back to the line of its own " +
909
+ `source that built the object${reachLeg}${dataLeg}; the diff is recorded in ${recorder}` +
910
+ (proof?.state === 'verified'
911
+ ? `; ${proof.detail}`
912
+ : '; no current-epoch cold-remount round trip has verified that path'),
913
+ ...(dataStalled
914
+ ? {
915
+ missing:
916
+ 'every object this game places from its own level data — its whole placed cargo — ' +
917
+ 'is unwritable while that writer is unreachable',
918
+ fix: `make \`ingest.dataWriter\` loadable: ${data.reason ?? 'it has not resolved yet'}`,
919
+ }
920
+ : {}),
921
+ };
922
+ }
923
+
924
+ function writableDataLeg(data: DataWriterFacts | null): string {
925
+ if (data === null) return '';
926
+ if (data.state === 'ready') {
927
+ return `, and an object the game anchored to a record in ${data.dataFile} is written back into that file`;
928
+ }
929
+ const reason = data.reason ? ` (${data.reason})` : '';
930
+ return `, but its declared data writer is ${data.state}${reason}, so an object anchored to a level-data record stays live-only`;
931
+ }
932
+
933
+ function persistenceRow(facts: ResolvedCoverageFacts): CapabilityCoverageRow {
934
+ const { ownership } = facts;
935
+ if (ownership === null) {
936
+ return {
937
+ seam: 'persistence',
938
+ status: 'info',
939
+ detail:
940
+ "the editor has not asked who owns this game's source yet — until it answers, edits " +
941
+ 'stay live-only',
942
+ };
943
+ }
944
+ const reach = facts.writeReach;
945
+ if (ownership.writable && reach !== null && reach.addressable === 0) {
946
+ return unreachableWorldRow(reach);
947
+ }
948
+ if (ownership.writable)
949
+ return writableBaseRow(ownership, reach, facts.dataWriter, facts.persistenceProof);
950
+ return {
951
+ seam: 'persistence',
952
+ status: 'gap',
953
+ detail: `edits are live-only — ${ownership.reason ?? "this game's source is not writable"}`,
954
+ missing:
955
+ 'nothing you change survives the session: no source write-back, and by doctrine no ' +
956
+ 'sidecar will ever be invented to fake one',
957
+ fix:
958
+ 'open this game from a folder the editor can write — creation-site persistence needs a ' +
959
+ 'writable base (the server decides, not the editor)',
960
+ };
961
+ }
962
+
963
+ /** `${n} thing(s)`, plus the names when there are any. */
964
+ function counted(noun: string, names: readonly string[]): string {
965
+ return `${names.length} ${noun}(s)${names.length > 0 ? `: ${names.join(', ')}` : ''}`;
966
+ }
967
+
968
+ /**
969
+ * The AGENT door. A declared `systems` surface is what
970
+ * `contract-debug-adapter.ts` projects onto the host's `DebugAdapter`, so a
971
+ * game that declares one can be enumerated, driven and read through the same
972
+ * `game.commands()`/`game.state()` an agent already uses on first-party
973
+ * content — and a game that declares none is not merely quiet there, it is
974
+ * undrivable. That is a gap, measured the same way as the other contract rows:
975
+ * an EMPTY surface counts as none, because the projection declines to build an
976
+ * adapter over zero verbs and zero reads rather than serve an empty one.
977
+ */
978
+ function systemsRow(facts: ResolvedCoverageFacts): CapabilityCoverageRow {
979
+ const { systems, declared } = facts.contract;
980
+ const commands = systems?.commands ?? [];
981
+ const state = systems?.state ?? [];
982
+ if (commands.length > 0 || state.length > 0) {
983
+ if (facts.contract.proof?.systems.state === 'failed') {
984
+ return {
985
+ seam: 'systems',
986
+ status: 'gap',
987
+ detail: `the systems carrier is declared but failed — ${facts.contract.proof.systems.detail}`,
988
+ missing:
989
+ '`game.commands()` / `game.providers()` cannot safely enumerate or invoke this carrier',
990
+ fix: CONTRACT_FIX('window.vgaiGame.systems (commands + state)'),
991
+ };
992
+ }
993
+ return {
994
+ seam: 'systems',
995
+ status: facts.contract.proof?.systems.state === 'verified' ? 'ok' : 'info',
996
+ detail:
997
+ facts.contract.proof?.systems.state === 'verified'
998
+ ? facts.contract.proof.systems.detail
999
+ : `the game declares ${counted('command', commands)}; ${counted('state provider', state)}, but the agent door has not operationally enumerated/read them in this epoch`,
1000
+ };
1001
+ }
1002
+ return {
1003
+ seam: 'systems',
1004
+ status: 'gap',
1005
+ detail: systems
1006
+ ? 'the contract declares a systems surface that names no commands and no state providers'
1007
+ : declared
1008
+ ? 'a contract is declared, but it names no systems'
1009
+ : 'no game→host contract is declared at all',
1010
+ missing:
1011
+ 'this game exposes no verbs and no state to drive or read it with: `game.commands()` and ' +
1012
+ '`game.providers()` enumerate nothing, so there is no `game.command(...)` to play it from ' +
1013
+ `${commandLine('eval')} and no \`game.state(...)\` for ${commandLine('status')} to see anything of what it is doing`,
1014
+ fix: CONTRACT_FIX('window.vgaiGame.systems (commands + state)'),
1015
+ };
1016
+ }
1017
+
1018
+ /**
1019
+ * What each slot unlocks, in the editor's own terms — used ONLY to say what a
1020
+ * work-order row costs. Never a per-game sentence: the same words describe the
1021
+ * same missing slot on every mount, because the slot is what is missing.
1022
+ */
1023
+ const SYSTEM_SLOT_COSTS: Record<SystemAdapterSlot, string> = {
1024
+ physics:
1025
+ 'the editor cannot freeze a simulated body while the gizmo edits it, so a physics-driven ' +
1026
+ "object's pose is overwritten the next frame",
1027
+ networking:
1028
+ 'the Network inspector shows nothing: no connection state, no room, no peers, and no ' +
1029
+ 'authority answer to keep a server-owned object inspect-only',
1030
+ navigation: 'the Navigation panel has no navmesh to report, query a path through, or draw',
1031
+ audio:
1032
+ "⏸ cannot silence this world's audio and the Audio debugger has no graph, transport, meters " +
1033
+ 'or events to show',
1034
+ camera:
1035
+ 'the selected-camera Inspector cannot report which native camera/controller is active or whether a blend is in progress',
1036
+ // A getter: the product's command is known only once the page has asked
1037
+ // for it, well after this module loaded.
1038
+ get debug() {
1039
+ return (
1040
+ '`game.commands()` and `game.providers()` enumerate nothing, so there is no verb to drive ' +
1041
+ `this game with and no state read for ${commandLine('status')} to see`
1042
+ );
1043
+ },
1044
+ renderDebug:
1045
+ 'the Frame debugger cannot capture a frame and the Profiler has no render-memory snapshot',
1046
+ };
1047
+
1048
+ /**
1049
+ * The `debug` slot's fix is the contract's commands/state (the `systems` row
1050
+ * above owns that story in detail); every other game-declarable slot's fix is
1051
+ * the `systemAdapters` carrier. `renderDebug` is neither — it is engine-owned,
1052
+ * derived from a captured renderer, so a game cannot supply it and telling a
1053
+ * reader to declare one would be false.
1054
+ */
1055
+ function systemSlotFix(slot: SystemAdapterSlot): string {
1056
+ if (slot === 'renderDebug') {
1057
+ return (
1058
+ 'nothing a game declares reaches this slot — the host derives it from the captured ' +
1059
+ "renderer's own WebGL2 context (`ingest/ingest-render-debug.ts`), so a mount whose " +
1060
+ 'renderer has no such context (a stub context) can never have it'
1061
+ );
1062
+ }
1063
+ if (slot === 'debug') {
1064
+ return CONTRACT_FIX('window.vgaiGame.systems (commands + state)');
1065
+ }
1066
+ return CONTRACT_FIX(`window.vgaiGame.systems.systemAdapters.${slot}`);
1067
+ }
1068
+
1069
+ /**
1070
+ * ONE row per `SystemAdapters` slot — the ingestion bar's §F ("the full
1071
+ * surface, both modes") made checkable.
1072
+ *
1073
+ * The bar admits exactly two terminal states per slot, so exactly two states
1074
+ * here are `ok`: an adapter the live registry holds, and an absence the game
1075
+ * positively answered with evidence. Everything else is a work order.
1076
+ */
1077
+ function systemAdapterRow(m: SystemAdapterMeasurement): CapabilityCoverageRow {
1078
+ const seam = `system.${m.slot}` as const;
1079
+ if (m.state === 'bound') {
1080
+ if (m.proof?.state === 'failed') {
1081
+ return {
1082
+ seam,
1083
+ status: 'gap',
1084
+ detail: `a ${m.slot} adapter is bound, but its contract failed — ${m.proof.detail}`,
1085
+ missing: SYSTEM_SLOT_COSTS[m.slot],
1086
+ fix: `fix the bound ${m.slot} adapter; registration is not evidence that its operations work`,
1087
+ };
1088
+ }
1089
+ if (m.proof?.state !== 'verified') {
1090
+ return {
1091
+ seam,
1092
+ status: 'info',
1093
+ detail:
1094
+ m.proof?.detail ??
1095
+ `a ${m.slot} adapter is installed, but no current-epoch operational proof was recorded`,
1096
+ };
1097
+ }
1098
+ return {
1099
+ seam,
1100
+ status: 'ok',
1101
+ detail: `a ${m.slot} adapter is installed in the editor's live system registry`,
1102
+ };
1103
+ }
1104
+ if (m.state === 'empty') {
1105
+ // WHO ANSWERED is part of the answer. Both provenances land in `empty`, and
1106
+ // for a long time both printed "the game answers that it has no X" — which
1107
+ // was a fabricated attestation whenever the HOST had derived it: measured
1108
+ // live on third-person, whose `systems` table declares only audio and
1109
+ // physics, the navigation/camera/networking rows all claimed the game had
1110
+ // answered. Attributing a statement to a game that never made it is the
1111
+ // anti-shim rule's own failure mode, in the product's own voice.
1112
+ if (m.attestedBy === 'host') {
1113
+ return {
1114
+ seam,
1115
+ status: 'info',
1116
+ attestedBy: 'host',
1117
+ detail:
1118
+ `derived-empty (the HOST's observation, not the game's answer) — nothing filled ` +
1119
+ `${m.slot} on a live mount: ${m.evidence ?? ''}. The game has not itself declared ` +
1120
+ `this absence; \`absent(reason)\` in its \`systems\` table is what turns the host's ` +
1121
+ `inference into the game's own statement.`,
1122
+ };
1123
+ }
1124
+ return {
1125
+ seam,
1126
+ status: 'info',
1127
+ attestedBy: 'game',
1128
+ detail: `declared-empty (an attestation, not a mechanically verified absence) — the game answers that it has no ${m.slot}: ${
1129
+ m.evidence ?? ''
1130
+ }`,
1131
+ };
1132
+ }
1133
+ if (m.state === 'malformed') {
1134
+ return {
1135
+ seam,
1136
+ status: 'gap',
1137
+ detail: `the game declared a ${m.slot} slot that could not be honoured — ${m.evidence ?? ''}`,
1138
+ missing: SYSTEM_SLOT_COSTS[m.slot],
1139
+ fix:
1140
+ `fix the declaration to engage with the row's evidence — the detail above names the ` +
1141
+ `specific conflict (a declared absence must name a contradicting shipped dependency; a ` +
1142
+ `declared adapter must be a real adapter or a { present: false, evidence } record)`,
1143
+ };
1144
+ }
1145
+ return {
1146
+ seam,
1147
+ status: 'gap',
1148
+ detail: `no ${m.slot} adapter is installed and the game declares no answer for the slot`,
1149
+ missing: SYSTEM_SLOT_COSTS[m.slot],
1150
+ fix: m.fix ?? systemSlotFix(m.slot),
1151
+ };
1152
+ }
1153
+
1154
+ /**
1155
+ * ONE row per native verb — the family that is neither an `AuthoringAdapter`
1156
+ * provider nor a `SystemAdapters` slot, and so was reported by nothing.
1157
+ *
1158
+ * The measurer supplies every word, because each verb's evidence is a different
1159
+ * KIND of fact (a package.json script, the server's ownership answer, the
1160
+ * export door's own precondition) and a shared sentence here would be wrong for
1161
+ * at least two of them. All this decides is which of the three states is a
1162
+ * standing work order.
1163
+ */
1164
+ function projectVerbRow(m: ProjectVerbMeasurement): CapabilityCoverageRow {
1165
+ const seam = `project.${m.slot}` as const;
1166
+ if (m.state === 'present') return presentProjectVerbRow(m, seam);
1167
+ // Unmeasured is its own answer (see this file's header): a fact nobody could
1168
+ // read is `info` with the reason, never a gap and never a pass.
1169
+ return {
1170
+ seam,
1171
+ status: m.state === 'absent' ? 'gap' : 'info',
1172
+ detail: m.evidence,
1173
+ ...(m.missing ? { missing: m.missing } : {}),
1174
+ ...(m.fix ? { fix: m.fix } : {}),
1175
+ };
1176
+ }
1177
+
1178
+ function presentProjectVerbRow(
1179
+ measurement: ProjectVerbMeasurement,
1180
+ seam: `project.${ProjectVerbSlot}`,
1181
+ ): CapabilityCoverageRow {
1182
+ if (measurement.proof?.state === 'failed') {
1183
+ return {
1184
+ seam,
1185
+ status: 'gap',
1186
+ detail: `${measurement.evidence}; the real verb failed — ${measurement.proof.detail}`,
1187
+ ...(measurement.missing ? { missing: measurement.missing } : {}),
1188
+ ...(measurement.fix ? { fix: measurement.fix } : {}),
1189
+ };
1190
+ }
1191
+ const verified = measurement.proof?.state === 'verified';
1192
+ return {
1193
+ seam,
1194
+ status: verified ? 'ok' : 'info',
1195
+ detail: verified
1196
+ ? `${measurement.evidence}; ${measurement.proof?.detail}`
1197
+ : `${measurement.evidence}; declaration is only a precondition — this verb has not completed successfully in the current evidence run`,
1198
+ };
1199
+ }
1200
+
1201
+ function authoringSurfaceRows(facts: ResolvedCoverageFacts): readonly CapabilityCoverageRow[] {
1202
+ const surface = facts.authoringSurface;
1203
+ if (!surface) return [];
1204
+ const scenes: CapabilityCoverageRow =
1205
+ surface.sceneDocuments === 0
1206
+ ? {
1207
+ seam: 'authoring.scenes',
1208
+ status: 'gap',
1209
+ detail: 'the adapter scene table names no authorable scene with a document to open',
1210
+ missing:
1211
+ 'Edit has no scene tab — a captured runtime can still be healthy while the authoring surface is empty',
1212
+ fix: 'declare authorable scene-table entries with a mountable source or root region so planSceneDocument produces a document',
1213
+ }
1214
+ : surface.availableIsolationDocuments < surface.isolationDocuments
1215
+ ? {
1216
+ seam: 'authoring.scenes',
1217
+ status: 'gap',
1218
+ detail: `the table plans ${surface.isolationDocuments} isolation document(s) but only ${surface.availableIsolationDocuments} are available to open`,
1219
+ missing: 'declared scenes are absent from the Content catalog',
1220
+ fix: 'register every planned isolation document in the available-document catalog',
1221
+ }
1222
+ : {
1223
+ seam: 'authoring.scenes',
1224
+ status: 'ok',
1225
+ detail:
1226
+ `${surface.sceneDocuments} scene document(s) planned` +
1227
+ (surface.isolationDocuments > 0
1228
+ ? `, ${surface.openIsolationDocuments} of ${surface.isolationDocuments} isolation tab(s) open, ${surface.availableIsolationDocuments} available from Content`
1229
+ : ''),
1230
+ };
1231
+ const pieces: CapabilityCoverageRow =
1232
+ surface.pieces === 0 && surface.sceneEntries === 0
1233
+ ? {
1234
+ seam: 'authoring.pieces',
1235
+ status: 'na',
1236
+ detail:
1237
+ 'the scene table declares no scenes, so there is no surface to place prefabs on ' +
1238
+ '(a models or website project)',
1239
+ }
1240
+ : surface.pieces === 0
1241
+ ? {
1242
+ seam: 'authoring.pieces',
1243
+ status: 'gap',
1244
+ detail: 'the resolved scene table contains no prefab entries',
1245
+ missing: 'Content has no project pieces and the component board has nothing to show',
1246
+ fix: 'add portable CSF stories inside the adapter region include globs so prefabsFromStories resolves entries',
1247
+ }
1248
+ : {
1249
+ seam: 'authoring.pieces',
1250
+ status: 'ok',
1251
+ detail: `${surface.pieces} prefab(s) resolved from the scene table`,
1252
+ };
1253
+ return [scenes, pieces];
1254
+ }
1255
+
1256
+ /**
1257
+ * THE derivation. Rows are produced in a fixed order — cheapest to reason
1258
+ * about first (what the editor reached), then what the game declared, then
1259
+ * what it owns — so two reports of the same mount are diffable line for line.
1260
+ */
1261
+ function resolveCoverageFacts(facts: CapabilityCoverageFacts): ResolvedCoverageFacts {
1262
+ return {
1263
+ worldId: facts.worldId,
1264
+ reach: facts.reach ?? null,
1265
+ surface: facts.surface ?? null,
1266
+ reachMechanism: facts.reachMechanism ?? null,
1267
+ loop: facts.loop ?? null,
1268
+ loopReason: facts.loopReason ?? null,
1269
+ contract: facts.contract ?? {
1270
+ declared: false,
1271
+ root: false,
1272
+ start: false,
1273
+ pause: false,
1274
+ resume: false,
1275
+ systems: null,
1276
+ },
1277
+ ownership: facts.ownership ?? null,
1278
+ writeReach: facts.writeReach ?? null,
1279
+ dataWriter: facts.dataWriter ?? null,
1280
+ persistenceProof: facts.persistenceProof,
1281
+ systemAdapters: facts.systemAdapters ?? [],
1282
+ projectVerbs: facts.projectVerbs ?? [],
1283
+ authoringSurface: facts.authoringSurface ?? null,
1284
+ };
1285
+ }
1286
+
1287
+ function capabilityRows(
1288
+ facts: CapabilityCoverageFacts,
1289
+ resolved: ResolvedCoverageFacts,
1290
+ ): readonly CapabilityCoverageRow[] {
1291
+ const hasAuthoringFacts = 'reach' in facts || 'surface' in facts || 'reachMechanism' in facts;
1292
+ const hasLoopFacts = 'loop' in facts || 'loopReason' in facts || 'contract' in facts;
1293
+ const hasContractFacts = 'contract' in facts;
1294
+ const hasPersistenceFacts =
1295
+ 'ownership' in facts ||
1296
+ 'writeReach' in facts ||
1297
+ 'dataWriter' in facts ||
1298
+ 'persistenceProof' in facts;
1299
+ return [
1300
+ ...(hasAuthoringFacts ? editorSurfaceRows(resolved) : []),
1301
+ ...(hasLoopFacts ? [loopRow(resolved)] : []),
1302
+ ...(hasContractFacts
1303
+ ? [contractRootRow(resolved), contractStartRow(resolved), contractPauseRow(resolved)]
1304
+ : []),
1305
+ ...(hasPersistenceFacts ? [persistenceRow(resolved)] : []),
1306
+ ...(hasContractFacts ? [systemsRow(resolved)] : []),
1307
+ // One row per SystemAdapters slot, in `SYSTEM_ADAPTER_SLOTS` order. An
1308
+ // empty measurement list contributes no rows: with no live mount to ask,
1309
+ // six fabricated gaps would be worse than silence.
1310
+ ...resolved.systemAdapters.map(systemAdapterRow),
1311
+ // One row per native verb, same empty-list convention.
1312
+ ...resolved.projectVerbs.map(projectVerbRow),
1313
+ ...('authoringSurface' in facts ? authoringSurfaceRows(resolved) : []),
1314
+ ];
1315
+ }
1316
+
1317
+ export function deriveCapabilityCoverage(facts: CapabilityCoverageFacts): CapabilityCoverageReport {
1318
+ const rows = capabilityRows(facts, resolveCoverageFacts(facts));
1319
+ return {
1320
+ rows,
1321
+ summary: {
1322
+ worldId: facts.worldId,
1323
+ rows: rows.length,
1324
+ gaps: rows.filter((r) => r.status === 'gap').length,
1325
+ ok: rows.filter((r) => r.status === 'ok').length,
1326
+ na: rows.filter((r) => r.status === 'na').length,
1327
+ info: rows.filter((r) => r.status === 'info').length,
1328
+ },
1329
+ };
1330
+ }
1331
+
1332
+ // ──────────────────────────────────────────────────── the canvas-overlay probe
1333
+
1334
+ /** The shape this probe needs from a DOM element. Structural on purpose: a real
1335
+ * `Element` satisfies it, and so does a plain object in a headless test. */
1336
+ export interface OverlayRect {
1337
+ readonly left: number;
1338
+ readonly top: number;
1339
+ readonly right: number;
1340
+ readonly bottom: number;
1341
+ }
1342
+
1343
+ export interface OverlayElement {
1344
+ readonly tagName: string;
1345
+ readonly id: string;
1346
+ readonly children: ArrayLike<OverlayElement>;
1347
+ getBoundingClientRect(): OverlayRect;
1348
+ }
1349
+
1350
+ function subtreeHasCanvas(el: OverlayElement, canvas: OverlayElement): boolean {
1351
+ if (el === canvas) return true;
1352
+ for (let i = 0; i < el.children.length; i++) {
1353
+ const child = el.children[i];
1354
+ if (child && subtreeHasCanvas(child, canvas)) return true;
1355
+ }
1356
+ return false;
1357
+ }
1358
+
1359
+ interface CanvasCandidate {
1360
+ element: OverlayElement;
1361
+ depth: number;
1362
+ area: number;
1363
+ }
1364
+
1365
+ /** Pick the visible canvas most likely to be the game's presentation surface:
1366
+ * largest painted area first, then the shallowest DOM placement. Hidden
1367
+ * loading canvases therefore stop winning once the real game canvas appears. */
1368
+ export function findPrimaryCanvas(root: OverlayElement): OverlayElement | null {
1369
+ const candidates: CanvasCandidate[] = [];
1370
+ const visit = (element: OverlayElement, depth: number): void => {
1371
+ if (element.tagName.toLowerCase() === 'canvas') {
1372
+ const rect = element.getBoundingClientRect();
1373
+ candidates.push({
1374
+ element,
1375
+ depth,
1376
+ area: Math.max(0, rect.right - rect.left) * Math.max(0, rect.bottom - rect.top),
1377
+ });
1378
+ }
1379
+ for (let i = 0; i < element.children.length; i++) {
1380
+ const child = element.children[i];
1381
+ if (child) visit(child, depth + 1);
1382
+ }
1383
+ };
1384
+ visit(root, 0);
1385
+ const visible = candidates.filter((candidate) => candidate.area > 0);
1386
+ const ranked = visible.length > 0 ? visible : candidates;
1387
+ ranked.sort((a, b) => b.area - a.area || a.depth - b.depth);
1388
+ return ranked[0]?.element ?? null;
1389
+ }
1390
+
1391
+ const NON_UI_TAGS = new Set(['script', 'style', 'link', 'meta', 'base', 'title', 'noscript']);
1392
+
1393
+ /** Outermost non-inert branches outside the primary canvas ancestry. Unlike
1394
+ * the coverage probe this deliberately includes hidden UI: title screens and
1395
+ * pause menus remain authorable documents when their current state hides them. */
1396
+ export function findCanvasUiElements(
1397
+ root: OverlayElement,
1398
+ canvas: OverlayElement,
1399
+ ): OverlayElement[] {
1400
+ const found: OverlayElement[] = [];
1401
+ const visit = (element: OverlayElement): void => {
1402
+ for (let i = 0; i < element.children.length; i++) {
1403
+ const child = element.children[i];
1404
+ if (!child) continue;
1405
+ if (subtreeHasCanvas(child, canvas)) {
1406
+ visit(child);
1407
+ } else if (!NON_UI_TAGS.has(child.tagName.toLowerCase())) found.push(child);
1408
+ }
1409
+ };
1410
+ visit(root);
1411
+ return found;
1412
+ }
1413
+
1414
+ // ──────────────────────────────────────────────────────────── the console door
1415
+
1416
+ /** One block as the editor console takes it: a level and the whole grouped text. */
1417
+ export interface CapabilityCoverageConsoleBlock {
1418
+ readonly level: 'info' | 'warn';
1419
+ readonly message: string;
1420
+ }
1421
+
1422
+ const STATUS_MARK: Record<CapabilityCoverageRow['status'], string> = {
1423
+ gap: '✗',
1424
+ ok: '✓',
1425
+ na: '–',
1426
+ info: '·',
1427
+ };
1428
+
1429
+ function renderRow(row: CapabilityCoverageRow): string {
1430
+ const lines = [` ${STATUS_MARK[row.status]} ${row.seam} — ${row.detail}`];
1431
+ if (row.missing) lines.push(` missing: ${row.missing}`);
1432
+ if (row.fix) lines.push(` fix: ${row.fix}`);
1433
+ return lines.join('\n');
1434
+ }
1435
+
1436
+ // ───────────────────────────────────────────────────── the headline, once
1437
+ //
1438
+ // THE COUNTING RULE, AND WHY IT HAS ONE OWNER.
1439
+ //
1440
+ // The headline used to read "N of 21 seams are MISSING" wherever it was
1441
+ // printed, and 21 is the `editor.*` family ALONE — the `AuthoringAdapter`
1442
+ // provider vocabulary plus `editor.capture`. Every other family
1443
+ // (`system.*`, the contract rows, and now `project.*`) is derived in its own
1444
+ // report, so a session could print a clean "0 of 21" while a whole family of
1445
+ // verbs was missing and nothing anywhere summed them. A headline that counts
1446
+ // one family while calling its total "seams" is the clean-answer failure this
1447
+ // derivation exists to end.
1448
+ //
1449
+ // The remaining failure is quieter: the same sentence said "editor 11 of 42"
1450
+ // in the morning and "editor 7 of 21" in the afternoon, both while still
1451
+ // grading two roots. 42 is root-instances; 21 is distinct capabilities. The
1452
+ // producer now states both units (`coverage-accounting.ts`) so a reader can
1453
+ // tell them apart without opening this file.
1454
+
1455
+ export interface CoverageFamilyCount {
1456
+ readonly family: string;
1457
+ readonly rows: number;
1458
+ readonly gaps: number;
1459
+ }
1460
+
1461
+ /**
1462
+ * THE sentence. `rows` is whatever the caller is reporting on — one report or
1463
+ * the union of several. Capabilities are unique applicable seams; a gap on
1464
+ * any root keeps the capability missing (gap-wins). Root-instances ride in
1465
+ * the same sentence so 21 capabilities cannot be mistaken for 42 rows.
1466
+ */
1467
+ export function coverageGapHeadline(head: string, rows: readonly CapabilityCoverageRow[]): string {
1468
+ const headline = formatCoverageGapHeadline(head, deriveCoverageAccounting(rows));
1469
+ assertCoverageReconciles(rows, headline);
1470
+ return headline;
1471
+ }
1472
+
1473
+ /** One contributing report, and the label that says whose it is. A `label` is
1474
+ * needed only when several parts can carry the SAME seam (one `editor.*` row
1475
+ * set per mounted root); the game-scoped families pass `null`. */
1476
+ export interface CoveragePart {
1477
+ readonly label: string | null;
1478
+ readonly report: CapabilityCoverageReport;
1479
+ }
1480
+
1481
+ /**
1482
+ * Fold several family reports into ONE, so a caller that derives its families
1483
+ * separately still prints a single union-counting headline.
1484
+ *
1485
+ * A labelled part's rows carry the label in their `detail`, because the same
1486
+ * seam legitimately appears once per mounted root and a union that dropped the
1487
+ * attribution would show two verdicts for one capability with no way to tell
1488
+ * them apart.
1489
+ */
1490
+ export function unionCoverageReport(
1491
+ worldId: string,
1492
+ parts: readonly CoveragePart[],
1493
+ ): CapabilityCoverageReport {
1494
+ const rows = parts.flatMap((part) =>
1495
+ part.report.rows.map((row) =>
1496
+ part.label ? { ...row, detail: `[${part.label}] ${row.detail}` } : row,
1497
+ ),
1498
+ );
1499
+ return {
1500
+ rows,
1501
+ summary: {
1502
+ worldId,
1503
+ rows: rows.length,
1504
+ gaps: rows.filter((r) => r.status === 'gap').length,
1505
+ ok: rows.filter((r) => r.status === 'ok').length,
1506
+ na: rows.filter((r) => r.status === 'na').length,
1507
+ info: rows.filter((r) => r.status === 'info').length,
1508
+ },
1509
+ };
1510
+ }
1511
+
1512
+ /**
1513
+ * Render the report as at most two grouped blocks — gaps as ONE warning,
1514
+ * everything else as ONE info — matching how the mount paths already report
1515
+ * (a single multi-line entry per thing that happened, never a line per row:
1516
+ * the console collapses consecutive identical messages, and a fan-out of
1517
+ * eight entries would bury the mount's own log lines).
1518
+ */
1519
+ export function formatCapabilityCoverageBlocks(
1520
+ report: CapabilityCoverageReport,
1521
+ /** What this report is ABOUT, when the caller's subject is not one root — a
1522
+ * union across every family is a session's answer, not a root's, and a head
1523
+ * saying otherwise would misname where the gaps live. */
1524
+ headOverride?: string,
1525
+ ): readonly CapabilityCoverageConsoleBlock[] {
1526
+ const { summary } = report;
1527
+ // "root", not "ingest": the same derivation now reports NATIVE roots too
1528
+ // (`@volter/editor-game/coverage/root-coverage.ts`), and a headline naming the ingest lane over a
1529
+ // first-party root's gaps would tell the reader something false about where
1530
+ // the gap lives.
1531
+ const head = headOverride ?? `root coverage "${summary.worldId}"`;
1532
+ const blocks: CapabilityCoverageConsoleBlock[] = [];
1533
+ const gaps = report.rows.filter((r) => r.status === 'gap');
1534
+ const rest = report.rows.filter((r) => r.status !== 'gap');
1535
+ if (gaps.length > 0) {
1536
+ blocks.push({
1537
+ level: 'warn',
1538
+ message: [
1539
+ `${coverageGapHeadline(head, report.rows)} ` +
1540
+ 'This game mounts, but the editor cannot do the following with it:',
1541
+ ...gaps.map(renderRow),
1542
+ ].join('\n'),
1543
+ });
1544
+ }
1545
+ if (rest.length > 0) {
1546
+ blocks.push({
1547
+ level: 'info',
1548
+ message: [
1549
+ `${head} — ${summary.ok} seam(s) present, ${summary.na} not applicable to this surface, ` +
1550
+ `${summary.info} measured-only:`,
1551
+ ...rest.map(renderRow),
1552
+ ].join('\n'),
1553
+ });
1554
+ }
1555
+ return blocks;
1556
+ }
1557
+
1558
+ /**
1559
+ * The console door, ONCE PER MOUNT.
1560
+ *
1561
+ * The report is derived on demand (the status facet re-derives it on every
1562
+ * read, so `vgai status` never serves a verdict older than the question), which
1563
+ * makes "print it" a thing that could happen many times — per status poll, per
1564
+ * re-render, in the limit per frame. The guard is a mount token: the same
1565
+ * mount's report is emitted once and then never again, and a NEW mount emits
1566
+ * again because it is a different game's answer.
1567
+ *
1568
+ * A factory rather than module-level state so the guard is directly testable
1569
+ * and so a test never has to reset a global.
1570
+ */
1571
+ export function createCapabilityCoverageConsole(
1572
+ emit: (block: CapabilityCoverageConsoleBlock) => void,
1573
+ ): {
1574
+ report(mountToken: string, report: CapabilityCoverageReport): boolean;
1575
+ } {
1576
+ let lastToken: string | null = null;
1577
+ return {
1578
+ /** Emits and returns true the first time it sees `mountToken`; a no-op
1579
+ * returning false for every later call with that same token. */
1580
+ report(mountToken: string, report: CapabilityCoverageReport): boolean {
1581
+ if (lastToken === mountToken) return false;
1582
+ lastToken = mountToken;
1583
+ for (const block of formatCapabilityCoverageBlocks(report)) emit(block);
1584
+ return true;
1585
+ },
1586
+ };
1587
+ }