@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.
- package/LICENSE +661 -0
- package/NOTICE +23 -0
- package/contributions/asset-budget-asset.menu.ts +26 -0
- package/contributions/asset-budget.action.ts +18 -0
- package/contributions/asset-budget.document.tsx +21 -0
- package/contributions/asset-budget.menu.ts +22 -0
- package/contributions/audio-unlock.service.ts +17 -0
- package/contributions/audio.utility.tsx +15 -0
- package/contributions/autoplay.service.ts +48 -0
- package/contributions/bridge.command.ts +172 -0
- package/contributions/build-profiles.document.tsx +21 -0
- package/contributions/build-progress.status.tsx +45 -0
- package/contributions/build.action.ts +26 -0
- package/contributions/build.command.ts +27 -0
- package/contributions/build.header.tsx +37 -0
- package/contributions/build.menu.ts +31 -0
- package/contributions/build.service.ts +31 -0
- package/contributions/connection.status.tsx +43 -0
- package/contributions/coverage.service.ts +118 -0
- package/contributions/edit-mode-audio.service.ts +23 -0
- package/contributions/edit-mode-networking.service.ts +30 -0
- package/contributions/game-document.service.ts +25 -0
- package/contributions/game-eval.command.ts +210 -0
- package/contributions/game.layout.ts +19 -0
- package/contributions/gameplay.command.ts +255 -0
- package/contributions/generation.service.ts +96 -0
- package/contributions/generations.status.tsx +53 -0
- package/contributions/ingest.service.ts +80 -0
- package/contributions/instances.command.ts +75 -0
- package/contributions/navmesh.menu.ts +39 -0
- package/contributions/navmesh.service.ts +19 -0
- package/contributions/network.utility.tsx +17 -0
- package/contributions/play.command.ts +355 -0
- package/contributions/profiler.action.ts +16 -0
- package/contributions/profiler.menu.ts +22 -0
- package/contributions/profiler.utility.tsx +17 -0
- package/contributions/react/component-board.service.ts +21 -0
- package/contributions/react/design-time-mount.service.ts +60 -0
- package/contributions/react/pasteboard.action.ts +41 -0
- package/contributions/react/react-inspector.service.ts +85 -0
- package/contributions/react/story-documents.service.ts +44 -0
- package/contributions/scene-document.service.ts +32 -0
- package/contributions/state-watch.action.ts +17 -0
- package/contributions/state-watch.menu.ts +23 -0
- package/contributions/state-watch.utility.tsx +20 -0
- package/contributions/team-playtest.service.ts +124 -0
- package/contributions/three/camera-runtime.inspector.tsx +34 -0
- package/contributions/three/component-board.service.ts +24 -0
- package/contributions/three/component-verbs.command.ts +110 -0
- package/contributions/three/component-verbs.service.ts +92 -0
- package/contributions/three/constraints.inspector.tsx +34 -0
- package/contributions/three/model-asset-sections.service.ts +26 -0
- package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
- package/contributions/three/story-documents.service.ts +31 -0
- package/contributions/three/three-authoring.service.ts +66 -0
- package/contributions/transport.header.tsx +19 -0
- package/contributions/xstate-behavior.action.ts +42 -0
- package/contributions/xstate-behavior.document.tsx +73 -0
- package/contributions/xstate-behavior.inspector.tsx +28 -0
- package/contributions/xstate-behavior.menu.ts +23 -0
- package/package.json +144 -0
- package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
- package/src/asset-budget/asset-budget-model.ts +799 -0
- package/src/asset-budget/basis-encoder.ts +165 -0
- package/src/asset-budget/gltf-io.ts +154 -0
- package/src/asset-budget/gltf-optimize.ts +384 -0
- package/src/asset-budget/image-dims.ts +78 -0
- package/src/asset-budget/optimize-apply.ts +221 -0
- package/src/audio/AudioDebuggerPanel.tsx +457 -0
- package/src/audio/audio-debugger-model.ts +87 -0
- package/src/bridge/call.ts +60 -0
- package/src/bridge/dispatch.ts +459 -0
- package/src/bridge/live-frames.ts +25 -0
- package/src/bridge/screenshot.ts +316 -0
- package/src/build/BuildProfilesPanel.tsx +476 -0
- package/src/build/build-session.ts +247 -0
- package/src/build/format-bytes.ts +14 -0
- package/src/command-results.ts +36 -0
- package/src/coverage/live-authoring-surface.ts +30 -0
- package/src/coverage/live-project-verbs.ts +162 -0
- package/src/coverage/native-system-coverage.ts +143 -0
- package/src/coverage/root-coverage.ts +79 -0
- package/src/coverage/session-coverage.ts +193 -0
- package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
- package/src/edit-mode/edit-mode-audio.ts +56 -0
- package/src/edit-mode/edit-mode-networking.ts +112 -0
- package/src/game-document/DevicePresetPicker.tsx +84 -0
- package/src/game-document/GameCaptureFrameButton.tsx +45 -0
- package/src/game-document/GameDocument.tsx +215 -0
- package/src/game-document/GamePanel.tsx +662 -0
- package/src/game-document/InstanceInspectorPicker.tsx +183 -0
- package/src/game-document/crowd-debug.ts +183 -0
- package/src/game-document/device-preview.ts +336 -0
- package/src/game-document/game-view-store.ts +107 -0
- package/src/game-document/physics-debug.ts +187 -0
- package/src/generation/GenerationActivity.tsx +421 -0
- package/src/generation/GenerationGallery.css +231 -0
- package/src/generation/generation-documents.tsx +338 -0
- package/src/generation/generation-jobs.ts +128 -0
- package/src/generation/generation-presentation.ts +257 -0
- package/src/host/adapter-reach.ts +445 -0
- package/src/host/adapter-runtime-bindings.ts +303 -0
- package/src/host/after-paint.ts +112 -0
- package/src/host/api/configurations.ts +66 -0
- package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
- package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
- package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
- package/src/host/authoring/contract-scenes-stories.ts +204 -0
- package/src/host/authoring/creation-site-related.ts +55 -0
- package/src/host/authoring/ephemeral-persistence.ts +29 -0
- package/src/host/authoring/gesture-persist.ts +84 -0
- package/src/host/authoring/ingest-data-writer.ts +232 -0
- package/src/host/authoring/ingest-source-persistence.ts +826 -0
- package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
- package/src/host/authoring/mounted-authoring.ts +41 -0
- package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
- package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
- package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
- package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
- package/src/host/authoring/pixi-isolation-assets.ts +25 -0
- package/src/host/authoring/pixi-live-write-target.ts +979 -0
- package/src/host/authoring/pixi-source-identity.ts +141 -0
- package/src/host/authoring/pixi-still-presentation.ts +71 -0
- package/src/host/authoring/pixi-structure-history.ts +237 -0
- package/src/host/authoring/pixi-transform-channels.ts +205 -0
- package/src/host/authoring/selection-remount-handoff.ts +23 -0
- package/src/host/authoring/source-persistence-backend.ts +373 -0
- package/src/host/authoring/source-refresh-revisions.ts +81 -0
- package/src/host/authoring/struct-write-pipe.ts +143 -0
- package/src/host/auto-frame-window.ts +89 -0
- package/src/host/binding-resolver.ts +393 -0
- package/src/host/browser-transpile.ts +631 -0
- package/src/host/canvas-entry-runtime.ts +95 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
- package/src/host/components/HeaderTelemetry.tsx +341 -0
- package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
- package/src/host/components/ResolutionPicker.tsx +89 -0
- package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
- package/src/host/components/frame-debugger-model.ts +579 -0
- package/src/host/components/header-telemetry-model.ts +74 -0
- package/src/host/components/scene-document.tsx +447 -0
- package/src/host/components/utility-view-state.ts +87 -0
- package/src/host/components/world-root-stage-binding.tsx +133 -0
- package/src/host/components/world-root-stage.ts +1046 -0
- package/src/host/coverage/authoring-read-probe.ts +583 -0
- package/src/host/coverage/capability-coverage.ts +1587 -0
- package/src/host/coverage/coverage-accounting.ts +261 -0
- package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
- package/src/host/coverage/project-verb-coverage.ts +148 -0
- package/src/host/coverage/system-adapter-coverage.ts +373 -0
- package/src/host/design-system-stories/StoryLayout.tsx +104 -0
- package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
- package/src/host/document-preview-three.ts +110 -0
- package/src/host/entry-adjudication.ts +89 -0
- package/src/host/game-location-guard.ts +150 -0
- package/src/host/game-module-access.ts +196 -0
- package/src/host/game-realm-page.ts +358 -0
- package/src/host/game-realm-reclaim.ts +55 -0
- package/src/host/game-realm-storage.ts +104 -0
- package/src/host/gameplay-export.ts +288 -0
- package/src/host/gameplay-recording.ts +717 -0
- package/src/host/gated-globals.ts +1511 -0
- package/src/host/history/json-history-resource.ts +254 -0
- package/src/host/ingest/registry.ts +261 -0
- package/src/host/instance-extract-actions.ts +120 -0
- package/src/host/instance-fork-actions.ts +120 -0
- package/src/host/play-control-hook.ts +26 -0
- package/src/host/playwright-shim.ts +479 -0
- package/src/host/projection/dom.ts +295 -0
- package/src/host/projection/pixi.ts +288 -0
- package/src/host/r3f-entry-runtime.ts +77 -0
- package/src/host/react-mount-runtime.ts +162 -0
- package/src/host/realm-services.ts +148 -0
- package/src/host/recording-preview.ts +106 -0
- package/src/host/roots/module-root.ts +203 -0
- package/src/host/roots/react-root.ts +181 -0
- package/src/host/same-realm-loop-gate.ts +544 -0
- package/src/host/scene-view-drawability.ts +69 -0
- package/src/host/sdk/tools.ts +31 -0
- package/src/host/served-bundle-runtime-modules.ts +331 -0
- package/src/host/server-log-bridge.ts +60 -0
- package/src/host/staged-projects.ts +24 -0
- package/src/host/stories/mounted-story-viewport-source.ts +64 -0
- package/src/host/stories/story-arg-descriptors.ts +50 -0
- package/src/host/stories/story-media-presence.ts +152 -0
- package/src/host/surface-content.ts +87 -0
- package/src/host/take-named-export.ts +23 -0
- package/src/host/three-ingest-runtime.ts +76 -0
- package/src/host/types-fastnoise-lite.d.ts +7 -0
- package/src/host/types-mikktspace.d.ts +20 -0
- package/src/host/types-troika-three-text.d.ts +7 -0
- package/src/host/use-active-performance-source.ts +53 -0
- package/src/host/viewport-pose-memory.ts +48 -0
- package/src/host/viewport-root-presentation.ts +40 -0
- package/src/ingest/active-ingest.ts +251 -0
- package/src/ingest/active-scene-navigation.ts +43 -0
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
- package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
- package/src/ingest/capture-wait-report.ts +122 -0
- package/src/ingest/deferred-ingest-play.ts +216 -0
- package/src/ingest/deferred-ingest-session.ts +23 -0
- package/src/ingest/discovery-public-ingest.ts +242 -0
- package/src/ingest/dom-stub-mark.ts +7 -0
- package/src/ingest/entry-load.ts +56 -0
- package/src/ingest/game-contract-realm.ts +34 -0
- package/src/ingest/game-pointer-lock.ts +89 -0
- package/src/ingest/held-scene-repaint.ts +73 -0
- package/src/ingest/host-surface-box.ts +174 -0
- package/src/ingest/ingest-boot-viewport.ts +54 -0
- package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
- package/src/ingest/ingest-canvas-scene.ts +50 -0
- package/src/ingest/ingest-evidence-hook.ts +131 -0
- package/src/ingest/ingest-frame-snapshot.ts +183 -0
- package/src/ingest/ingest-play-commands.ts +134 -0
- package/src/ingest/ingest-play-control.ts +299 -0
- package/src/ingest/ingest-render-debug.ts +320 -0
- package/src/ingest/ingest-siblings.ts +487 -0
- package/src/ingest/ingest-status.ts +62 -0
- package/src/ingest/live-ingest-facet.ts +46 -0
- package/src/ingest/module-mode.ts +229 -0
- package/src/ingest/mount-canvas-ingest-root.ts +899 -0
- package/src/ingest/mount-coverage.ts +296 -0
- package/src/ingest/mount-dom-ingest-root.ts +282 -0
- package/src/ingest/mount-ingest-root.ts +744 -0
- package/src/ingest/mount-three-ingest-root.ts +366 -0
- package/src/ingest/resolve-canvas.ts +47 -0
- package/src/ingest/resolve-three.ts +123 -0
- package/src/ingest/served-bundle.ts +109 -0
- package/src/ingest/served-html-boot.ts +292 -0
- package/src/ingest/surface-canvas.ts +84 -0
- package/src/ingest/surface-dom.ts +84 -0
- package/src/ingest/surface-three.ts +128 -0
- package/src/ingest/types.ts +76 -0
- package/src/ingest/unmount-ingest-root.ts +224 -0
- package/src/navmesh/navmesh-actions.ts +27 -0
- package/src/navmesh/navmesh-handler.ts +237 -0
- package/src/navmesh/navmesh-workflow-store.ts +78 -0
- package/src/network/NetworkInspectorPanel.tsx +644 -0
- package/src/network/network-inspector-model.ts +225 -0
- package/src/play/play-boot-stall.ts +118 -0
- package/src/play/play-log-events.ts +26 -0
- package/src/play/play-mode.ts +2553 -0
- package/src/play/play-recording.ts +335 -0
- package/src/play/react-play-live-authoring.ts +165 -0
- package/src/play-bar/PlayBar.tsx +488 -0
- package/src/play-bar/PlayerCountPicker.tsx +100 -0
- package/src/profiler/FrameDebuggerPanel.tsx +458 -0
- package/src/profiler/PerformancePanel.tsx +1068 -0
- package/src/profiler/ProfilerPanel.tsx +53 -0
- package/src/profiler/frame-debugger-store.ts +97 -0
- package/src/profiler/main-thread-busy.ts +90 -0
- package/src/react/design-time-react-mount.ts +782 -0
- package/src/react/dom-authoring-adapter.ts +764 -0
- package/src/react/pasteboard-materialize.ts +154 -0
- package/src/react/react-inspector-section.tsx +2496 -0
- package/src/react/react-world-authoring-adapter.ts +3795 -0
- package/src/react/story-documents/story-args-section.ts +19 -0
- package/src/react/story-documents/story-documents.tsx +1380 -0
- package/src/react/story-paint-bounds.ts +94 -0
- package/src/react/ui-board-document.tsx +101 -0
- package/src/react/ui-board-title.ts +9 -0
- package/src/react/ui-component-board.ts +72 -0
- package/src/services/audio-pose-guard.ts +81 -0
- package/src/services/game-audio-unlock.ts +48 -0
- package/src/state-watch/StateWatchPanel.tsx +535 -0
- package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
- package/src/three/authoring/constraint-inspector-section.tsx +189 -0
- package/src/three/authoring/design-time-renderer.ts +189 -0
- package/src/three/authoring/model-asset-inspector-section.css +41 -0
- package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
- package/src/three/authoring/oid-source-persistence.ts +557 -0
- package/src/three/authoring/r3f-design-session.ts +1174 -0
- package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
- package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
- package/src/three/authoring/spatial-audio-handles.ts +215 -0
- package/src/three/authoring/spatial-collider-handles.ts +229 -0
- package/src/three/authoring/spatial-joint-handles.ts +114 -0
- package/src/three/authoring/spatial-light-handles.ts +178 -0
- package/src/three/authoring/spatial-lod-handles.ts +93 -0
- package/src/three/authoring/spatial-particle-handles.ts +368 -0
- package/src/three/authoring/three-authoring-adapter.ts +1953 -0
- package/src/three/authoring/three-scene-identity.ts +19 -0
- package/src/three/authoring/three-spatial-handles.ts +161 -0
- package/src/three/authoring/typed-three-inspector.ts +528 -0
- package/src/three/component-verbs/extract-menu.ts +71 -0
- package/src/three/component-verbs/fork-menu.ts +78 -0
- package/src/three/component-verbs/internals-menu.ts +91 -0
- package/src/three/story-documents/three-story-documents.tsx +607 -0
- package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
- package/src/three/three-board/board-framing.ts +412 -0
- package/src/three/three-board/board-layout.ts +401 -0
- package/src/three/three-board/board-scene.ts +901 -0
- package/src/three/three-board/three-component-board.ts +62 -0
- package/src/xstate/XStateBehaviorSection.tsx +130 -0
- package/src/xstate/XStateMachineInspector.tsx +566 -0
- package/src/xstate/character-animation-machine.fixture.ts +74 -0
- package/src/xstate/live-behaviors.ts +106 -0
- package/src/xstate/use-live-actor-state.ts +44 -0
- package/src/xstate/xstate-graph.ts +235 -0
- 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
|
+
}
|