diff --git a/.fallowrc.json b/.fallowrc.json index 8e71253c0..891800919 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -147,6 +147,36 @@ "comment": "Published type surface: re-exported by src/sdk/limrun.ts to agent-device/limrun, where test/integration/limrun-public-types.test.ts pins that consumers can import it. Its only importer compiles in a temp checkout, so no static in-repo edge reaches it — the same reason src/sdk/*.ts is exempt above.", "file": "packages/provider-limrun/src/index.ts", "exports": ["LimrunIosCommandExecution"] + }, + { + "comment": "Converting the contracts façades from `export *` to explicit named re-exports (the pin-table retirement) made these individually visible to --production analysis for the first time; a bare star previously hid them from this exact check. GESTURE_INITIAL_ANGLE_DEGREES/GESTURE_SAMPLE_INTERVAL_MS/INTERACTION_GUARANTEES/INTERACTION_PATH_IDS/parseAndroidInputMethodPackage/isRecordingExportQuality have same-package test-tree consumers (invisible to --production, mirroring the packages/{ad-script,maestro} entry above); the rest currently have none. Kept rather than narrowed here so the façade's re-export surface stays byte-identical to the symbol set the retired pin table asserted — narrowing the surface is a follow-up with its own review, not a side effect of this mechanical conversion. The last five (COORDINATE_GESTURE_KINDS, normalizePublicGesture, DEFAULT_DRAG_*) are #1567's drag vocabulary, surfaced the same way by the same conversion and kept for the same reason.", + "file": "packages/contracts/src/facades/{client,command,divergence,interaction,platform,recording}.ts", + "exports": [ + "isRecord", + "IOS_SAFARI_BUNDLE_ID", + "REPLAY_DIVERGENCE_DEFAULT_REF_LIMIT", + "REPLAY_DIVERGENCE_DIGEST_REF_LIMIT", + "REPLAY_DIVERGENCE_LEVEL_BYTE_LIMITS", + "applyReplayDivergenceLevelCaps", + "measureReplayDivergenceBytes", + "sanitizeReplayDivergenceField", + "truncateUtf8Field", + "GESTURE_DURATION_MAX_MS", + "GESTURE_INITIAL_ANGLE_DEGREES", + "GESTURE_SAMPLE_INTERVAL_MS", + "INTERACTION_GUARANTEES", + "INTERACTION_PATH_IDS", + "buildSwipePresetGesturePlan", + "clampGestureCoordinate", + "gestureDirectionDelta", + "parseAndroidInputMethodPackage", + "isRecordingExportQuality", + "COORDINATE_GESTURE_KINDS", + "normalizePublicGesture", + "DEFAULT_DRAG_DESTINATION_HOLD_MS", + "DEFAULT_DRAG_MOVE_MS", + "DEFAULT_DRAG_SOURCE_HOLD_MS" + ] } ], "usedClassMembers": ["name", "listActiveLeases", "delete", "values", "elapsedMs", "isExpired"], @@ -154,6 +184,15 @@ "unused-types": "error", "duplicate-exports": "off" }, + "overrides": [ + { + "comment": "unused-types counts importers, not signature reachability. A façade names every type its re-exported functions/values put in a public signature, and callers use those types structurally without ever importing the name — the same false-positive shape already fixed for other workspace-package façades. unused-exports (a VALUE check) stays on for this glob; that is what would catch a real leak.", + "files": ["packages/contracts/src/facades/*.ts"], + "rules": { + "unused-types": "off" + } + } + ], "production": { "dupes": true }, diff --git a/packages/contracts/src/facades/capture.ts b/packages/contracts/src/facades/capture.ts index 7cf30e8e3..2f3a800db 100644 --- a/packages/contracts/src/facades/capture.ts +++ b/packages/contracts/src/facades/capture.ts @@ -1,6 +1,49 @@ -export * from '../diff.ts'; -export * from '../screenshot.ts'; -export * from '../snapshot-capture-annotations.ts'; -export * from '../snapshot-diagnostics.ts'; -export * from '../snapshot-types.ts'; -export * from '../viewport.ts'; +export type { DiffSnapshotCommandResult, SnapshotDiffLine, SnapshotDiffSummary } from '../diff.ts'; +export { + SCREENSHOT_ACTION_FLAG_KEYS, + SCREENSHOT_COMMAND_FLAG_KEYS, + SCREENSHOT_SPECIFIC_FLAG_DEFINITIONS, + appendScreenshotScriptFlags, + readScreenshotScriptFlag, + screenshotFlagsFromOptions, + screenshotOptionsFromFlags, +} from '../screenshot.ts'; +export type { + ScreenshotDispatchFlags, + ScreenshotPublicOptions, + ScreenshotRequestFlags, + ScreenshotRuntimeFlags, + ScreenshotRuntimeOptions, +} from '../screenshot.ts'; +export { + publicSnapshotCaptureAnnotations, + readSerializedSnapshotCaptureAnnotations, + snapshotCaptureAnnotationsFrom, +} from '../snapshot-capture-annotations.ts'; +export type { + PublicSnapshotCaptureAnnotations, + SnapshotCaptureAnalysis, + SnapshotCaptureAnnotations, + SnapshotCaptureFreshness, +} from '../snapshot-capture-annotations.ts'; +export { + mergeSnapshotDiagnostics, + readSnapshotDiagnosticsSummary, + recordSnapshotTiming, + summarizeSnapshotDiagnostics, + summarizeSnapshotTimingSamples, +} from '../snapshot-diagnostics.ts'; +export type { + SnapshotDiagnosticsState, + SnapshotDiagnosticsSummary, + SnapshotTimingSample, + SnapshotTimingStats, +} from '../snapshot-diagnostics.ts'; +export type { + AndroidSnapshotBackendMetadata, + BackendSnapshotOptions, + BackendSnapshotResult, + FindLocator, + ScreenshotResultData, +} from '../snapshot-types.ts'; +export type { ViewportCommandResult } from '../viewport.ts'; diff --git a/packages/contracts/src/facades/client.ts b/packages/contracts/src/facades/client.ts index 332625a75..ba8b99a46 100644 --- a/packages/contracts/src/facades/client.ts +++ b/packages/contracts/src/facades/client.ts @@ -1,15 +1,125 @@ -export * from '../client-app.ts'; -export * from '../client-capture.ts'; -export * from '../client-connection.ts'; -export * from '../client-device-view.ts'; -export * from '../client-gesture.ts'; -export * from '../client-lease.ts'; -export * from '../client-observability.ts'; -export * from '../client-replay.ts'; -export * from '../client-request.ts'; -export * from '../client-selector-read.ts'; -export * from '../client-session.ts'; -export * from '../client-settings.ts'; -export * from '../client-system.ts'; -export * from '../client-target.ts'; -export * from '../json.ts'; +export type { + AppCloseOptions, + AppCloseResult, + AppDeployOptions, + AppDeployResult, + AppInstallFromSourceOptions, + AppInstallFromSourceResult, + AppInstallOptions, + AppListOptions, + AppOpenOptions, + AppOpenResult, + AppPushOptions, + AppTriggerEventOptions, + MaterializationReleaseOptions, + MaterializationReleaseResult, +} from '../client-app.ts'; +export type { + CaptureDiffOptions, + CaptureScreenshotOptions, + CaptureScreenshotResult, + CaptureSnapshotOptions, + CaptureSnapshotResult, + FindSnapshotCommandOptions, + SelectorSnapshotCommandOptions, +} from '../client-capture.ts'; +export type { + AgentDeviceClientConfig, + AgentDeviceDaemonTransport, + AgentDeviceDaemonTransportContext, + AgentDeviceIdentifiers, + AgentDeviceRequestOverrides, + AgentDeviceSelectionOptions, + DeviceCommandBaseOptions, +} from '../client-connection.ts'; +export type { + AgentDeviceCapabilitiesResult, + AgentDeviceDevice, + AgentDeviceSession, + AgentDeviceSessionDevice, + DeviceBootOptions, + DeviceShutdownOptions, + StartupPerfSample, +} from '../client-device-view.ts'; +export type { + ClickOptions, + DragOptions, + FillOptions, + FlingOptions, + FocusOptions, + LongPressOptions, + PanOptions, + PinchOptions, + PressOptions, + RepeatedPressOptions, + RotateGestureOptions, + ScrollOptions, + SettleCommandOptions, + SwipeGestureOptions, + SwipeOptions, + TransformGestureOptions, + TypeTextOptions, +} from '../client-gesture.ts'; +export type { + CloudArtifactsOptions, + Lease, + LeaseAllocateOptions, + LeaseOptions, + LeaseScopedOptions, +} from '../client-lease.ts'; +export type { + AudioOptions, + EventsOptions, + LogsOptions, + NetworkOptions, + PerfOptions, + RecordOptions, + TraceOptions, +} from '../client-observability.ts'; +export type { + BatchRunOptions, + BatchStep, + ReplayRunOptions, + ReplayTestOptions, +} from '../client-replay.ts'; +export type { + CommandExecutionOptions, + CommandRequestResult, + InternalRequestOptions, +} from '../client-request.ts'; +export type { + FindBaseOptions, + FindOptions, + GetOptions, + IsOptions, + IsStatePredicateOptions, + IsTextPredicateOptions, + RecordControlOptions, +} from '../client-selector-read.ts'; +export type { + SessionCloseResult, + SessionSaveScriptOptions, + SessionSaveScriptResult, +} from '../client-session.ts'; +export type { PermissionTarget, SettingsUpdateOptions } from '../client-settings.ts'; +export type { + AlertCommandOptions, + AppStateCommandOptions, + ClipboardCommandOptions, + DoctorCommandOptions, + KeyboardCommandOptions, + PrepareCommandOptions, + ReactNativeCommandOptions, + ViewportCommandOptions, + WaitCommandOptions, + WaitCommandTarget, +} from '../client-system.ts'; +export type { + ElementTarget, + InteractionTarget, + PointTarget, + RefTarget, + SelectorTarget, +} from '../client-target.ts'; +export { isRecord } from '../json.ts'; +export type { JsonObject, JsonPrimitive, JsonValue } from '../json.ts'; diff --git a/packages/contracts/src/facades/command.ts b/packages/contracts/src/facades/command.ts index 5404c2a2b..2e650e666 100644 --- a/packages/contracts/src/facades/command.ts +++ b/packages/contracts/src/facades/command.ts @@ -1,9 +1,26 @@ -export * from '../batch-contract.ts'; -export * from '../batch-step.ts'; -export * from '../cli-flags.ts'; -export * from '../command-flags.ts'; -export * from '../dispatched-command.ts'; -export * from '../input-validation.ts'; -export * from '../open-target.ts'; -export * from '../prepare.ts'; -export * from '../push.ts'; +export { + DEFAULT_BATCH_MAX_STEPS, + assertBatchStepCount, + isValidBatchMaxSteps, + parseBatchStepRuntime, + readBatchStepInputObject, + readBatchStepRecord, +} from '../batch-contract.ts'; +export type { DaemonBatchStep } from '../batch-step.ts'; +export type { CliFlags, DaemonExcludedCliFlag } from '../cli-flags.ts'; +export type { CommandFlags, MaestroRuntimeFlags } from '../command-flags.ts'; +export type { DispatchedCommand } from '../dispatched-command.ts'; +export { readOptionalInteger } from '../input-validation.ts'; +export { + IOS_SAFARI_BUNDLE_ID, + isDeepLinkTarget, + isWebUrl, + resolveIosDeviceDeepLinkBundleId, +} from '../open-target.ts'; +export type { + PrepareCommandResult, + PrepareIosRunnerArtifactState, + PrepareIosRunnerCacheKind, + PrepareIosRunnerTiming, +} from '../prepare.ts'; +export type { PushCommandResult } from '../push.ts'; diff --git a/packages/contracts/src/facades/device.ts b/packages/contracts/src/facades/device.ts index f0d029901..44143e18c 100644 --- a/packages/contracts/src/facades/device.ts +++ b/packages/contracts/src/facades/device.ts @@ -1,9 +1,42 @@ -export * from '../app-events.ts'; -export * from '../app-inventory.ts'; -export * from '../app-state.ts'; -export * from '../device-inventory.ts'; -export * from '../device-provider.ts'; -export * from '../device-rotation.ts'; -export * from '../device.ts'; -export * from '../provider-device-runtime.ts'; -export * from '../target-shutdown-contract.ts'; +export type { TriggerAppEventCommandResult } from '../app-events.ts'; +export { + DEFAULT_APPS_FILTER, + assertResolvedAppsFilter, + resolveAppsFilter, +} from '../app-inventory.ts'; +export type { AppsFilter } from '../app-inventory.ts'; +export type { AppStateCommandResult } from '../app-state.ts'; +export { + LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, + WEB_DESKTOP_DEVICE, + countDeviceInventoryByGroup, + shouldUseHostMacFastPath, +} from '../device-inventory.ts'; +export type { + DeviceInventoryGroup, + DeviceInventoryGroupCounts, + DeviceInventoryRequest, +} from '../device-inventory.ts'; +export type { + DeviceInventoryProvider, + DeviceLease, + LeaseLifecycleContext, + LeaseLifecycleProvider, +} from '../device-provider.ts'; +export { + DEVICE_ROTATIONS, + DEVICE_ROTATION_SURFACE_INDEX, + deviceRotationOrientation, + deviceRotationSurfaceDegrees, + parseDeviceRotation, +} from '../device-rotation.ts'; +export type { DeviceRotation } from '../device-rotation.ts'; +export type { BootCommandResult, ShutdownCommandResult } from '../device.ts'; +export type { + ProviderDeviceInstallOptions, + ProviderDeviceInstallResult, + ProviderDeviceRuntime, + ProviderExpiredLeaseRecovery, + ProviderPortReverseOptions, +} from '../provider-device-runtime.ts'; +export type { TargetShutdownResult } from '../target-shutdown-contract.ts'; diff --git a/packages/contracts/src/facades/divergence.ts b/packages/contracts/src/facades/divergence.ts index 86f161b8f..058247488 100644 --- a/packages/contracts/src/facades/divergence.ts +++ b/packages/contracts/src/facades/divergence.ts @@ -1 +1,33 @@ -export * from '../replay-divergence.ts'; +export { + REPLAY_DIVERGENCE_DEFAULT_REF_LIMIT, + REPLAY_DIVERGENCE_DIGEST_REF_LIMIT, + REPLAY_DIVERGENCE_LEVEL_BYTE_LIMITS, + REPLAY_DIVERGENCE_SUGGESTION_LIMIT, + applyReplayDivergenceLevelCaps, + boundReplayDivergence, + createReplayDivergenceSanitizer, + formatReplayDivergenceReport, + measureReplayDivergenceBytes, + sanitizeReplayDivergenceField, + scrubReplayVarValues, + truncateUtf8Field, +} from '../replay-divergence.ts'; +export type { + ReplayDivergence, + ReplayDivergenceCause, + ReplayDivergenceKind, + ReplayDivergenceOverflow, + ReplayDivergenceResume, + ReplayDivergenceScreen, + ReplayDivergenceScreenRef, + ReplayDivergenceStep, + ReplayDivergenceStepSource, + ReplayDivergenceSuggestion, + ReplayDivergenceSuggestionBasis, + ReplayDivergenceTargetBinding, + ReplayDivergenceTargetBindingKind, + ReplayDivergenceTargetCandidate, + ReplayDivergenceTargetIdentity, + ReplayRepairHint, + ReplayVarScrubEntry, +} from '../replay-divergence.ts'; diff --git a/packages/contracts/src/facades/interaction.ts b/packages/contracts/src/facades/interaction.ts index f5a33c3ac..336f87fe9 100644 --- a/packages/contracts/src/facades/interaction.ts +++ b/packages/contracts/src/facades/interaction.ts @@ -1,17 +1,184 @@ -export * from '../alert-contract.ts'; -export * from '../back-mode.ts'; -export * from '../click-button.ts'; -export * from '../clipboard.ts'; -export * from '../gesture-input.ts'; -export * from '../gesture-normalization.ts'; -export * from '../gesture-plan.ts'; -export * from '../gesture-plan-types.ts'; -export * from '../interaction.ts'; -export * from '../interaction-guarantees.ts'; -export * from '../interactor-types.ts'; -export * from '../keyboard.ts'; -export * from '../navigation.ts'; -export * from '../scroll-command.ts'; -export * from '../scroll-gesture.ts'; -export * from '../tv-remote.ts'; -export * from '../wait.ts'; +export { + ALERT_ACTIONS, + ALERT_ACTION_RETRY_MS, + ALERT_POLL_INTERVAL_MS, + DEFAULT_ALERT_TIMEOUT_MS, +} from '../alert-contract.ts'; +export type { AlertAction, AlertInfo, AlertPlatform, AlertSource } from '../alert-contract.ts'; +export { BACK_MODES } from '../back-mode.ts'; +export type { BackMode } from '../back-mode.ts'; +export { + CLICK_BUTTONS, + buttonTag, + getClickButtonValidationError, + resolveClickButton, +} from '../click-button.ts'; +export type { ClickButton } from '../click-button.ts'; +export type { ClipboardCommandResult } from '../clipboard.ts'; +export { COORDINATE_GESTURE_KINDS, GESTURE_KINDS, readGesturePayload } from '../gesture-input.ts'; +export type { + FlingGesturePayload, + GesturePayload, + PanGesturePayload, + PinchGesturePayload, + RotateGesturePayload, + SwipeGesturePayload, + TransformGesturePayload, +} from '../gesture-input.ts'; +export { + assertNoRemovedSwipeInput, + describeReplayGestureArityError, + dragGesturePayloadFromPositionals, + gesturePayloadFromPositionals, + gesturePayloadToPositionals, + normalizeGestureCommandInput, + normalizePublicGesture, + normalizePublicSwipeMotion, + swipePayloadFromPositionals, +} from '../gesture-normalization.ts'; +export type { NormalizedPublicGesture, SwipePayload } from '../gesture-normalization.ts'; +export { + DEFAULT_DRAG_DESTINATION_HOLD_MS, + DEFAULT_DRAG_MOVE_MS, + DEFAULT_DRAG_SOURCE_HOLD_MS, + GESTURE_DURATION_MAX_MS, + GESTURE_DURATION_MIN_MS, +} from '../gesture-plan-types.ts'; +export type { + GestureExecutionProfile, + GestureIntent, + GesturePlan, + GesturePointerCount, + GestureSemanticInput, + MultiTouchGesturePlan, + PointerTrajectory, + PointerTrajectorySample, + SinglePointerGesturePlan, + SinglePointerTrajectory, +} from '../gesture-plan-types.ts'; +export { + GESTURE_FLING_DURATION_MS, + GESTURE_INITIAL_ANGLE_DEGREES, + GESTURE_SAMPLE_INTERVAL_MS, + buildDragGesturePlan, + buildGesturePlan, + interpolateGesturePoint, + sampleGestureOffsets, + singlePointerPlanEndpoints, +} from '../gesture-plan.ts'; +export type { GestureSamplingProfile } from '../gesture-plan.ts'; +export { + INTERACTION_DISPATCH_PATHS, + INTERACTION_GUARANTEES, + INTERACTION_PATH_IDS, +} from '../interaction-guarantees.ts'; +export type { + GuaranteeEnforcement, + InteractionGuarantee, + InteractionPathContract, + InteractionPathId, +} from '../interaction-guarantees.ts'; +export type { + ClickCommandResponseData, + DisambiguationTiebreak, + ElementTarget, + FillCommandResponseData, + FillCommandResult, + FindCommandResponseData, + InteractionEvidence, + InteractionTarget, + LongPressCommandResponseData, + LongPressCommandResult, + PointTarget, + PressCommandResponseData, + PressCommandResult, + RecordingTargetOverride, + RefTarget, + ResolutionDiagnosticEntry, + ResolutionDisclosure, + ResolvedInteractionTarget, + ResolvedTarget, + SelectorTarget, + SettleDiffLine, + SettleObservation, + SettleParams, + SettleTailEntry, +} from '../interaction.ts'; +export { MAESTRO_NON_HITTABLE_FALLBACK_MESSAGE, TEXT_ENTRY_ROUTES } from '../interactor-types.ts'; +export type { + ElementSelectorKey, + ElementSelectorTapOptions, + Interactor, + RunnerCallOptions, + RunnerContext, + ScreenshotOptions, + SnapshotOptions, + SnapshotResult, + TextEntryRoute, + TypeTextBackendResult, +} from '../interactor-types.ts'; +export type { KeyboardCommandResult } from '../keyboard.ts'; +export type { + AppSwitcherCommandResult, + BackCommandResult, + HomeCommandResult, + OrientationCommandResult, + RotateCommandResult, + TvRemoteCommandResult, +} from '../navigation.ts'; +export { + SCROLL_DURATION_MAX_MS, + assertExclusiveScrollDistanceInputs, + honoredScrollDurationMs, + normalizeScrollDurationMs, +} from '../scroll-command.ts'; +export type { + ScrollCommandOptions, + ScrollDistanceOptions, + ScrollTimingOptions, +} from '../scroll-command.ts'; +export { + SCROLL_DIRECTIONS, + SCROLL_INPUT_DIRECTIONS, + SWIPE_PATTERNS, + SWIPE_PAUSE_MAX_MS, + SWIPE_PRESETS, + SWIPE_REPETITION_MAX, + SWIPE_SERIES_MAX_SCHEDULED_DURATION_MS, + assertScrollGestureInput, + buildInPageSwipeGesturePlan, + buildScrollGesturePlan, + buildSwipePresetGesturePlan, + clampGestureCoordinate, + gestureDirectionDelta, + inferGestureReferenceFrame, + parseScrollDirection, +} from '../scroll-gesture.ts'; +export type { + GestureReferenceFrame, + InPageSwipeGesturePlan, + ScrollDirection, + ScrollGestureOptions, + ScrollGesturePlan, + ScrollInputDirection, + SwipePattern, + SwipePreset, + SwipePresetGesturePlan, + TransformGestureParams, +} from '../scroll-gesture.ts'; +export { + TV_REMOTE_BUTTONS, + TV_REMOTE_BUTTON_USAGE, + parseTvRemoteButton, + toAndroidTvRemoteKeyevent, + toAppleTvRemoteButton, + toVegaTvRemoteKey, + tvRemoteDurationMode, +} from '../tv-remote.ts'; +export type { AppleTvRemoteButton, TvRemoteButton, VegaTvRemoteKey } from '../tv-remote.ts'; +export { WAIT_REASONS } from '../wait.ts'; +export type { WaitCommandResult, WaitReason } from '../wait.ts'; +export type { CoordinateGesturePayload } from '../gesture-input.ts'; +export type { DragGesturePayload } from '../gesture-input.ts'; +export type { DragGestureInput } from '../gesture-plan-types.ts'; +export type { GestureCommandInput } from '../gesture-plan-types.ts'; diff --git a/packages/contracts/src/facades/observability.ts b/packages/contracts/src/facades/observability.ts index fc3f6a2f4..e2e2617e7 100644 --- a/packages/contracts/src/facades/observability.ts +++ b/packages/contracts/src/facades/observability.ts @@ -1,7 +1,51 @@ -export * from '../cloud-artifacts.ts'; -export * from '../debug-symbols.ts'; -export * from '../doctor.ts'; -export * from '../launch-console.ts'; -export * from '../logs.ts'; -export * from '../network-log.ts'; -export * from '../perf.ts'; +export type { + AgentArtifactsResult, + CloudArtifact, + CloudArtifactAvailability, + CloudArtifactKind, + CloudArtifactProvider, + CloudArtifactsQuery, + CloudArtifactsResult, + CloudArtifactsStatus, + CloudProviderSessionResult, + DaemonArtifactInventoryEntry, + DaemonArtifactsResult, +} from '../cloud-artifacts.ts'; +export type { + DebugSymbolsCrashFrame, + DebugSymbolsCrashSummary, + DebugSymbolsImage, + DebugSymbolsOptions, + DebugSymbolsResult, +} from '../debug-symbols.ts'; +export type { DoctorCheck, DoctorCommandResult, DoctorKind, DoctorStatus } from '../doctor.ts'; +export { + LAUNCH_CONSOLE_DIRECT_APP_ONLY_MESSAGE, + LAUNCH_CONSOLE_IOS_SIMULATOR_ONLY_MESSAGE, +} from '../launch-console.ts'; +export { LOG_ACTION_VALUES } from '../logs.ts'; +export type { LogAction, LogBackend } from '../logs.ts'; +export type { NetworkEntry } from '../network-log.ts'; +export { + PERF_ACTION_ERROR_MESSAGE, + PERF_ACTION_VALUES, + PERF_AREA_ERROR_MESSAGE, + PERF_AREA_VALUES, + PERF_KIND_ERROR_MESSAGE, + PERF_KIND_VALUES, + PERF_MEMORY_KIND_ERROR_MESSAGE, + PERF_SUBJECT_ERROR_MESSAGE, + PERF_SUBJECT_VALUES, + isPerfAction, + isPerfArea, + isPerfKind, + isPerfMemoryKind, + isPerfSubject, +} from '../perf.ts'; +export type { + PerfAction, + PerfArea, + PerfKind, + PerfMetricsSamplerTag, + PerfSubject, +} from '../perf.ts'; diff --git a/packages/contracts/src/facades/platform.ts b/packages/contracts/src/facades/platform.ts index fc22f780f..0a109a974 100644 --- a/packages/contracts/src/facades/platform.ts +++ b/packages/contracts/src/facades/platform.ts @@ -1,8 +1,37 @@ -export * from '../android-input-ownership.ts'; -export * from '../android-system-chrome.ts'; -export * from '../apple-multitouch-support.ts'; -export * from '../audio-probe-result.ts'; -export * from '../audio-probe-support.ts'; -export * from '../platform-plugin.ts'; -export * from '../platform-providers.ts'; -export * from '../runner-lease-context.ts'; +export { + classifyAndroidInputOwner, + classifyAndroidInputOwnership, + isAndroidInputMethodOwnedNode, + isFallbackAndroidInputMethodPackage, + isFallbackAndroidInputMethodResource, + parseAndroidInputMethodPackage, + readAndroidActiveInputMethodPackage, +} from '../android-input-ownership.ts'; +export type { + AndroidInputOwner, + AndroidInputOwnership, + AndroidInputOwnershipSource, +} from '../android-input-ownership.ts'; +export { + ANDROID_SYSTEM_CHROME_PACKAGE, + hasAndroidSystemChromeProvenance, + isAndroidSystemChromeWindowResourceId, + stripAndroidSystemChromeProvenance, + stripAndroidSystemChromeProvenanceFromNode, +} from '../android-system-chrome.ts'; +export type { AndroidSystemChromeProvenance } from '../android-system-chrome.ts'; +export { assertAppleMultiTouchSupported } from '../apple-multitouch-support.ts'; +export { emptyAudioProbeResult, normalizeAudioProbeRecord } from '../audio-probe-result.ts'; +export type { + AudioProbeResult, + AudioProbeSource, + EmptyAudioProbeResultOptions, + NormalizeAudioProbeRecordOptions, +} from '../audio-probe-result.ts'; +export { + isAudioProbeSupportedDevice, + isHostSystemAudioProbeDevice, +} from '../audio-probe-support.ts'; +export type { PlatformPlugin } from '../platform-plugin.ts'; +export type { PlatformGatedProviderResolverKey } from '../platform-providers.ts'; +export type { RunnerLogicalLeaseContext } from '../runner-lease-context.ts'; diff --git a/packages/contracts/src/facades/progress.ts b/packages/contracts/src/facades/progress.ts index b2575ad43..513b71e5b 100644 --- a/packages/contracts/src/facades/progress.ts +++ b/packages/contracts/src/facades/progress.ts @@ -1 +1,7 @@ -export * from '../request-progress.ts'; +export type { + CommandProgressEvent, + ReplayTestProgressEvent, + ReplayTestSuiteProgressEvent, + RequestProgressEvent, + RequestProgressSink, +} from '../request-progress.ts'; diff --git a/packages/contracts/src/facades/recording.ts b/packages/contracts/src/facades/recording.ts index 39aeac1ea..795b13b7f 100644 --- a/packages/contracts/src/facades/recording.ts +++ b/packages/contracts/src/facades/recording.ts @@ -1,3 +1,17 @@ -export * from '../recording.ts'; -export * from '../recording-export-quality.ts'; -export * from '../recording-scope.ts'; +export { + DEFAULT_RECORDING_EXPORT_QUALITY, + RECORDING_EXPORT_QUALITIES, + isRecordingExportQuality, + recordingQualityInputToExportQuality, +} from '../recording-export-quality.ts'; +export type { RecordingExportQuality } from '../recording-export-quality.ts'; +export { RECORDING_SCOPE_VALUES, isWholeScreenRecordingScope } from '../recording-scope.ts'; +export type { RecordingScope } from '../recording-scope.ts'; +export type { + RecordingAppIdentity, + RecordingBackendTag, + RecordingCommandResult, + RecordingStartCommandResult, + RecordingStopCommandResult, + TraceCommandResult, +} from '../recording.ts'; diff --git a/packages/contracts/src/facades/remote.ts b/packages/contracts/src/facades/remote.ts index 72cc611e4..852c8272c 100644 --- a/packages/contracts/src/facades/remote.ts +++ b/packages/contracts/src/facades/remote.ts @@ -1,4 +1,23 @@ -export * from '../companion-tunnel-scope.ts'; -export * from '../metro.ts'; -export * from '../remote-config-fields.ts'; -export * from '../provider-connection.ts'; +export type { CompanionTunnelScope, MetroBridgeScope } from '../companion-tunnel-scope.ts'; +export type { + MetroBridgeResult, + MetroPrepareKind, + MetroPrepareOptions, + MetroPrepareResult, + MetroReloadOptions, + MetroReloadResult, + PrepareMetroRuntimeResult, + ReloadMetroResult, + ResolvedMetroKind, +} from '../metro.ts'; +export type { + ProviderConnectionResource, + ProviderConnectionVerification, +} from '../provider-connection.ts'; +export { PROVIDER_DEVICE_ORIENTATIONS } from '../remote-config-fields.ts'; +export type { + CloudProviderProfileFields, + ProviderDeviceOrientation, + RemoteConfigMetroOptions, + RemoteConnectionProfileFields, +} from '../remote-config-fields.ts'; diff --git a/packages/contracts/src/facades/replay.ts b/packages/contracts/src/facades/replay.ts index ccef87576..6360740dc 100644 --- a/packages/contracts/src/facades/replay.ts +++ b/packages/contracts/src/facades/replay.ts @@ -1,3 +1,24 @@ -export * from '../ref-frame-effect.ts'; -export * from '../replay.ts'; -export * from '../target-annotation.ts'; +export type { RefFrameEffect } from '../ref-frame-effect.ts'; +export { REPLAY_TARGET_GUARD_MISMATCH_REASON, WAIT_LANDMARK_MISMATCH_REASON } from '../replay.ts'; +export type { + ReplayCommandResult, + ReplaySuiteAttemptFailure, + ReplaySuiteResult, + ReplaySuiteTestFailed, + ReplaySuiteTestPassed, + ReplaySuiteTestResult, + ReplaySuiteTestSkipReason, + ReplaySuiteTestSkipped, + ReplayTargetGuardDenotation, + WaitLandmarkMismatchEvidence, +} from '../replay.ts'; +export type { + LocalIdentity, + MultiTargetAnnotationV1, + NodeStructuralDenotation, + TargetAncestryEntry, + TargetAnnotationV1, + TargetRect, + TargetScrollRegion, + TargetVerification, +} from '../target-annotation.ts'; diff --git a/packages/contracts/src/facades/session.ts b/packages/contracts/src/facades/session.ts index d10bb8d71..08b4d6229 100644 --- a/packages/contracts/src/facades/session.ts +++ b/packages/contracts/src/facades/session.ts @@ -1,2 +1,3 @@ -export * from '../session-action.ts'; -export * from '../session-surface.ts'; +export type { SessionAction } from '../session-action.ts'; +export { SESSION_SURFACES, parseSessionSurface } from '../session-surface.ts'; +export type { SessionSurface } from '../session-surface.ts'; diff --git a/scripts/layering/facade-exports.test.ts b/scripts/layering/facade-exports.test.ts index cc5b2a78b..7cf04e7ef 100644 --- a/scripts/layering/facade-exports.test.ts +++ b/scripts/layering/facade-exports.test.ts @@ -1,18 +1,10 @@ -// Façade export enumeration, tested directly: what `readNamedExports` and -// `readFacadeExports` report for each export FORM, independently of the R11 -// boundary rules that consume them. -// -// The `readFacadeExports` cases write throwaway modules under a real package -// rather than using committed fixtures — a fixture would pin the walker -// against a file shape this repo never actually ships. +// Named-export enumeration, tested directly: what `readNamedExports` reports +// for each export FORM, independently of the R11 boundary rules that consume +// it. import assert from 'node:assert/strict'; -import fs from 'node:fs'; -import path from 'node:path'; import { test } from 'node:test'; -import { readFacadeExports, readNamedExports } from './facade-exports.ts'; - -const repoRoot = path.resolve(import.meta.dirname, '../..'); +import { readNamedExports } from './facade-exports.ts'; test('readNamedExports collects re-export and direct-declaration forms, resolving aliases', () => { const source = [ @@ -87,221 +79,3 @@ test('readNamedExports collects every declarator of a multi-declarator export', // only exercises a single declarator, so the second name went unpinned. assert.deepEqual(readNamedExports('export const a = 1, b = 2;'), ['a', 'b']); }); - -// `readFacadeExports` is the same enumeration widened from one source string -// to the re-export CHAIN behind a file — the form every `contracts` façade -// is built from. These use the real tree's own barrels rather than fixtures: -// a fixture would pin the walker against a file this repo never ships. -test('readFacadeExports resolves a bare `export *` chain the source-only reader refuses', () => { - const barrel = path.join(repoRoot, 'packages/contracts/src/facades/session.ts'); - // Source-only: unknowable, so it throws (the merged contract, unchanged). - assert.throws(() => readNamedExports(fs.readFileSync(barrel, 'utf8')), /export \* from/); - // Given the FILE, the same barrel is fully enumerable. - assert.deepEqual(readFacadeExports(barrel), [ - 'SESSION_SURFACES', - 'SessionAction', - 'SessionSurface', - 'parseSessionSurface', - ]); -}); - -test('readFacadeExports refuses a bare `export *` across a package specifier', () => { - // A relative star names a module this gate can read; a package star means - // resolving node_modules into another package's exports map — unbounded - // widening, the exact thing the gate refuses. - const scratch = path.join(repoRoot, 'packages/contracts/src/facades/.export-star-probe.ts'); - fs.writeFileSync(scratch, "export * from '@agent-device/kernel/errors';\n"); - try { - assert.throws(() => readFacadeExports(scratch), /only a relative re-export/); - } finally { - fs.rmSync(scratch); - } -}); - -/** Write throwaway modules next to a real façade; always clean them up. */ -function withProbeModules(files: Record, run: (dir: string) => void): void { - const dir = path.join(repoRoot, 'packages/contracts/src/facades'); - const written = Object.entries(files).map(([name, source]) => { - const file = path.join(dir, name); - fs.writeFileSync(file, source); - return file; - }); - try { - run(dir); - } finally { - for (const file of written) fs.rmSync(file, { force: true }); - } -} - -test('readFacadeExports excludes a default that a star export cannot reach', () => { - // #1574 review P1: `export *` skips the child's default per - // GetExportedNames, so a private default in a leaf is NOT part of the - // barrel's surface. Counterfactual: the named sibling still comes through, - // proving the leaf is genuinely being read and the default specifically — - // not the whole module — is what got dropped. - withProbeModules( - { - '.leaf-probe.ts': 'export default function hidden() {}\nexport const reachable = 1;\n', - '.barrel-probe.ts': "export * from './.leaf-probe.ts';\n", - }, - (dir) => { - assert.deepEqual(readFacadeExports(path.join(dir, '.barrel-probe.ts')), ['reachable']); - }, - ); -}); - -test('readFacadeExports still rejects a default on the façade entry itself', () => { - // The other side of the same rule: the entry's own default IS a default - // export of the façade, and a façade pinned to a named list must not carry - // one. Same source text as the leaf above — only its position changed. - withProbeModules({ '.entry-default-probe.ts': 'export default function leak() {}\n' }, (dir) => { - assert.throws( - () => readFacadeExports(path.join(dir, '.entry-default-probe.ts')), - /export default/, - ); - }); -}); - -test('readFacadeExports rejects a name two star sources resolve differently', () => { - // ESM resolves this to `ambiguous`, so `clash` is not importable at all; - // unioning would pin a symbol no consumer can reach. - withProbeModules( - { - '.clash-a-probe.ts': 'export const clash = 1;\nexport const onlyA = 1;\n', - '.clash-b-probe.ts': 'export const clash = 2;\n', - '.clash-barrel-probe.ts': - "export * from './.clash-a-probe.ts';\nexport * from './.clash-b-probe.ts';\n", - }, - (dir) => { - assert.throws(() => readFacadeExports(path.join(dir, '.clash-barrel-probe.ts')), /ambiguous/); - }, - ); -}); - -test('readFacadeExports resolves a diamond and lets an explicit export shadow a star', () => { - // The two counterfactuals to the ambiguity rule, both of which a naive - // "two paths reached this name" check would wrongly reject. One shared - // declaration reached by two barrels is ONE binding, not a clash; and an - // explicit re-export of a name a star also provides is the spec's own - // precedence, not ambiguity. - withProbeModules( - { - '.shared-probe.ts': 'export const shared = 1;\n', - '.mid-one-probe.ts': "export * from './.shared-probe.ts';\n", - '.mid-two-probe.ts': "export * from './.shared-probe.ts';\n", - '.diamond-probe.ts': - "export * from './.mid-one-probe.ts';\nexport * from './.mid-two-probe.ts';\n", - '.shadow-src-probe.ts': 'export const shadowed = 1;\nexport const other = 2;\n', - '.shadow-probe.ts': - "export * from './.shadow-src-probe.ts';\nexport { shadowed } from './.shared-probe.ts';\n", - }, - (dir) => { - assert.deepEqual(readFacadeExports(path.join(dir, '.diamond-probe.ts')), ['shared']); - assert.deepEqual(readFacadeExports(path.join(dir, '.shadow-probe.ts')), [ - 'other', - 'shadowed', - ]); - }, - ); -}); - -test('readFacadeExports follows a named re-export chain to its ultimate binding', () => { - // #1574 review P1: `a` re-exports `x` from `b`, `c` re-exports `x` from - // `a`, and the façade stars both. ESM resolves ONE binding (`b`'s `x`), so - // this is a diamond, not a clash. Identifying a re-export by its immediate - // source would see `b#x` vs `a#x` and falsely reject the façade — the - // counterfactual that fails without transitive origin resolution. - withProbeModules( - { - '.chain-b-probe.ts': 'export const x = 1;\n', - '.chain-a-probe.ts': "export { x } from './.chain-b-probe.ts';\n", - '.chain-c-probe.ts': "export { x } from './.chain-a-probe.ts';\n", - '.chain-facade-probe.ts': - "export * from './.chain-a-probe.ts';\nexport * from './.chain-c-probe.ts';\n", - }, - (dir) => { - assert.deepEqual(readFacadeExports(path.join(dir, '.chain-facade-probe.ts')), ['x']); - }, - ); -}); - -test('readFacadeExports keeps rejecting two genuinely distinct bindings behind a chain', () => { - // The guard against over-correcting the above: resolving through chains - // must not collapse two REAL declarations into one. Same chain depth as the - // diamond, but the two branches bottom out in different modules. - withProbeModules( - { - '.split-one-probe.ts': 'export const y = 1;\n', - '.split-two-probe.ts': 'export const y = 2;\n', - '.split-a-probe.ts': "export { y } from './.split-one-probe.ts';\n", - '.split-c-probe.ts': "export { y } from './.split-two-probe.ts';\n", - '.split-facade-probe.ts': - "export * from './.split-a-probe.ts';\nexport * from './.split-c-probe.ts';\n", - }, - (dir) => { - assert.throws(() => readFacadeExports(path.join(dir, '.split-facade-probe.ts')), /ambiguous/); - }, - ); -}); - -// #1574 review, third round. `export { default } from './x.ts'` is reported -// by oxc as kind `Name` with the name `default` — the same fact as -// `export default …` wearing a different parse shape. It has to stay in a -// module's map so a later `export { default as x }` can resolve its binding, -// while never being reachable through a star. These three pin that split. -test('a star export does not re-export a name called `default`', () => { - // Per GetExportedNames a star skips `default` — oxc names the star's own - // import `AllButDefault`. Counterfactual: the ordinary sibling name in the - // same module still comes through, so this is `default` being filtered and - // not the whole module being dropped. - withProbeModules( - { - '.dstar-leaf-probe.ts': 'export default function hidden() {}\nexport const kept = 1;\n', - '.dstar-mid-probe.ts': - "export { default } from './.dstar-leaf-probe.ts';\n" + - "export { kept } from './.dstar-leaf-probe.ts';\n", - '.dstar-facade-probe.ts': "export * from './.dstar-mid-probe.ts';\n", - }, - (dir) => { - assert.deepEqual(readFacadeExports(path.join(dir, '.dstar-facade-probe.ts')), ['kept']); - }, - ); -}); - -test('a façade re-exporting a default under the name `default` is rejected', () => { - // The entry carries a default export either way; only the parse shape - // differs from the `export default …` case above. - withProbeModules( - { - '.dentry-leaf-probe.ts': 'export default function leak() {}\n', - '.dentry-facade-probe.ts': "export { default } from './.dentry-leaf-probe.ts';\n", - }, - (dir) => { - assert.throws( - () => readFacadeExports(path.join(dir, '.dentry-facade-probe.ts')), - /must not carry one/, - ); - }, - ); -}); - -test('two paths to one default binding resolve to a single name, not ambiguity', () => { - // `leaf` declares a default; `a` re-exports it; `b` names a's default `x` - // while `c` names leaf's default `x`; a façade stars both. ESM resolves ONE - // `leaf#default` binding, so `x` is exported rather than ambiguous — the - // intermediate `export { default } from` link has to carry identity through - // for the two paths to agree. - withProbeModules( - { - '.dchain-leaf-probe.ts': 'export default function shared() {}\n', - '.dchain-a-probe.ts': "export { default } from './.dchain-leaf-probe.ts';\n", - '.dchain-b-probe.ts': "export { default as x } from './.dchain-a-probe.ts';\n", - '.dchain-c-probe.ts': "export { default as x } from './.dchain-leaf-probe.ts';\n", - '.dchain-facade-probe.ts': - "export * from './.dchain-b-probe.ts';\nexport * from './.dchain-c-probe.ts';\n", - }, - (dir) => { - assert.deepEqual(readFacadeExports(path.join(dir, '.dchain-facade-probe.ts')), ['x']); - }, - ); -}); diff --git a/scripts/layering/facade-exports.ts b/scripts/layering/facade-exports.ts index 5f696682b..957666390 100644 --- a/scripts/layering/facade-exports.ts +++ b/scripts/layering/facade-exports.ts @@ -1,13 +1,21 @@ -// Façade export enumeration: what names a module actually exposes to a -// consumer, for the R11 exact-symbol pins in `package-boundaries.test.ts`. +// Named-export enumeration for a façade module's own source text — the R11 +// structural gate in `package-boundaries.test.ts` uses this to prove a +// façade names its exports explicitly instead of a bare `export *`. // // Split out of `package-boundaries.ts` (#1574 review): the boundary rules // answer "may this file import that one?", while this module answers "what // does this façade name?" — different questions, so per AGENTS.md they are // different bounded reads. +// +// This module used to also carry `readFacadeExports`, a ~200-line star-chain +// resolver (`GetExportedNames`/`ResolveExport` re-implemented over relative +// specifiers) that existed only to enumerate what a barrel's `export *` +// hides, feeding a hand-maintained 816-symbol pin table +// (`facade-symbols.ts`, #1574). Every workspace-package façade now names its +// exports explicitly (`export { a, b } from './x.ts'`), so there is nothing +// left for a star-chain walker to resolve — the façade file itself is the +// pin, and `readNamedExports` below is enough to prove it stays that way. -import fs from 'node:fs'; -import path from 'node:path'; import { parseSync } from 'oxc-parser'; /** @@ -66,177 +74,25 @@ export function readNamedExports(source: string): string[] { } /** - * Every name a façade subpath exports, sorted — `readNamedExports` widened - * from one source string to the re-export CHAIN behind it, so a barrel - * façade is pinnable too. - * - * `readNamedExports` throws on bare `export * from './x.ts'` because, given - * only a source string, the set it contributes is genuinely unknowable. Given - * the FILE, it is not: the specifier names a sibling module the gate can read - * and enumerate in turn. That is the whole difference here — every - * `@agent-device/contracts` façade (`src/facades/*.ts`) is exactly such a - * barrel, 13 of the 14 subpaths being nothing but bare re-export lines, so - * without chain resolution the package with the largest and fastest-growing - * public surface in the workspace is the one package that cannot be pinned. - * - * The walk models what `export *` ACTUALLY re-exports, which is narrower than - * "every name in the child" in two ways a naive union gets wrong (#1574 - * review P1): - * - * 1. **`default` is excluded.** Per `GetExportedNames`, a star export skips - * the child's default entirely. A private `export default` in a leaf is - * not reachable through the barrel and does not widen the façade, so it is - * passed over rather than rejected. A default on the ENTRY file is still a - * real default export of the façade itself, and still throws. - * 2. **A name from two different star sources is ambiguous, not exported.** - * `ResolveExport` returns `ambiguous` when star resolution finds two - * distinct bindings for one name, and importing it is then a `SyntaxError` - * — the name is not part of the surface at all. Unioning would silently - * pin a symbol no consumer can import, so ambiguity throws instead, naming - * both origins. A diamond (two barrels reaching the SAME declaration) is - * not ambiguous and resolves normally, which is why origins are tracked by - * the binding a name ultimately resolves to — through a chain of named - * re-exports, not just the immediate source — rather than by path taken. - * An explicit export in a module shadows any star-provided name of the - * same name, exactly as the spec's own precedence does. - * - * Resolution is deliberately narrow — a RELATIVE specifier only, and the - * repo's explicit-`.ts`-extension convention means the specifier is already - * the path. A bare star across a PACKAGE specifier still throws: enumerating - * it means resolving `node_modules` and re-entering another package's - * `exports` map, and a façade that re-exports a whole other package wholesale - * is precisely the unbounded widening this gate exists to refuse. Cycles are - * visit-guarded (a barrel pair that re-exported each other would otherwise - * recurse forever). + * Every name a module declares or re-exports BY NAME, ignoring any bare + * `export *` it also carries. `readNamedExports` refuses such a module + * outright, which is right for a façade — a star there is unbounded widening — + * but wrong for a SOURCE the exhaustiveness gate is reading: skipping the whole + * file also skips its direct exports, so removing one of those from a façade + * would narrow the surface undetected (#1614 review P2). The starred names are + * not lost: a façade must re-export the starred module directly too, and that + * path is checked on its own. */ -export function readFacadeExports(entryFile: string): string[] { - // `${declaringFile}#${name}` — binding identity, so the same declaration - // reached by two different barrel paths is one origin, not two. - const cache = new Map>(); - const walking = new Set(); - - /** - * The binding a named re-export ULTIMATELY names, not the module it was - * written against (#1574 review P1). `a` re-exports `x` from `b`, `c` - * re-exports `x` from `a`, and a façade stars both: ESM resolves one `b#x` - * binding, so that is a diamond, not a clash. Stopping at the immediate - * source would identify the two paths as `b#x` and `a#x` and falsely reject - * the façade as ambiguous. The child's own map already carries - * fully-resolved origins, so asking it is the whole fix. - */ - const reExportOrigin = ( - from: string, - specifier: string | undefined, - importedName: string, - localName: string, - ): string => { - if (!specifier) return `${from}#${localName}`; - // A package specifier is not a file this gate reads; its name is a stable - // identity of its own, so two façades re-exporting the same symbol from - // the same package still agree. - if (!specifier.startsWith('.')) return `${specifier}#${importedName}`; - const childPath = path.resolve(path.dirname(from), specifier); - // Falls back to the immediate source when the child does not name it — a - // cycle in progress, or a name oxc cannot attribute. - return exportedNames(childPath).get(importedName) ?? `${childPath}#${importedName}`; - }; - - const exportedNames = (file: string): Map => { - const resolved = path.resolve(file); - const cached = cache.get(resolved); - if (cached) return cached; - // A cycle contributes nothing further; whatever it exports is reached by - // the path that entered it first. - if (walking.has(resolved)) return new Map(); - walking.add(resolved); - - const parsed = parseSync(resolved, fs.readFileSync(resolved, 'utf8')); - const explicit = new Map(); - const starOrigins = new Map>(); - - for (const staticExport of parsed.module.staticExports) { - for (const entry of staticExport.entries) { - const specifier = entry.moduleRequest?.value; - - if (entry.exportName.kind === 'Default') { - // Only the façade's own default is a default export of the façade. - if (resolved === path.resolve(entryFile)) { - throw new Error( - `readFacadeExports cannot enumerate 'export default …' as a named symbol ` + - `(${resolved}) — a facade a caller pins to an exact named-export list must not ` + - 'carry one.', - ); - } - continue; - } - - if (entry.exportName.kind === 'None') { - if (!specifier || !specifier.startsWith('.')) { - throw new Error( - `readFacadeExports cannot enumerate 'export * from ${specifier ?? '…'}' ` + - `(${resolved}) — only a relative re-export names a module this gate can read ` + - 'and enumerate in turn. Name the re-exported symbols explicitly instead.', - ); - } - const childPath = path.resolve(path.dirname(resolved), specifier); - for (const [name, origin] of exportedNames(childPath)) { - // `default` is filtered HERE, at the star, not at the source - // (#1574 review, third round). A child's `export { default } from - // './leaf.ts'` is a NAMED export whose name happens to be - // `default` — oxc reports it as kind `Name`, and it must stay in - // the child's map so a chain like `export { default as x } from - // './that-child.ts'` can resolve its binding. But a star must not - // re-export it: `GetExportedNames` skips `default`, and oxc names - // the star's own import `AllButDefault`. Filtering at the source - // would break identity resolution; filtering here is the spec's - // own split. - if (name === 'default') continue; - let origins = starOrigins.get(name); - if (!origins) starOrigins.set(name, (origins = new Map())); - origins.set(origin, specifier); - } - continue; - } - - const name = entry.exportName.name; - if (!name) continue; - explicit.set( - name, - reExportOrigin(resolved, specifier, entry.importName?.name ?? name, name), - ); - } - } - - const names = new Map(explicit); - for (const [name, origins] of starOrigins) { - if (explicit.has(name)) continue; // explicit export shadows the star - if (origins.size > 1) { - throw new Error( - `readFacadeExports found '${name}' re-exported by ${origins.size} different ` + - `'export *' sources in ${resolved} (${[...origins.values()].sort().join(', ')}). ` + - 'ESM resolves that to `ambiguous`, so the name is not importable at all and must ' + - 'not be pinned as part of the surface. Re-export it explicitly from one source.', - ); - } - names.set(name, [...origins.keys()][0]!); +export function readDirectNamedExports(source: string): string[] { + const parsed = parseSync('facade-source-export-scan.ts', source); + const names = new Set(); + for (const staticExport of parsed.module.staticExports) { + for (const entry of staticExport.entries) { + // `None` is the bare star (unenumerable here, covered elsewhere); + // `Default` is never reachable through a star re-export. + if (entry.exportName.kind !== 'Name') continue; + if (entry.exportName.name) names.add(entry.exportName.name); } - - walking.delete(resolved); - cache.set(resolved, names); - return names; - }; - - const names = exportedNames(entryFile); - // The entry's own default, in EITHER form. `export default …` throws above - // as it is parsed; `export { default } from './x.ts'` reaches here instead, - // because oxc reports it as a named export called `default` — a different - // parse shape for the same fact, that the façade carries a default export. - if (names.has('default')) { - throw new Error( - `readFacadeExports cannot enumerate a default export as a named symbol ` + - `(${path.resolve(entryFile)}) — a facade a caller pins to an exact named-export list ` + - 'must not carry one, whether declared or re-exported under the name `default`.', - ); } - return [...names.keys()].sort(); + return [...names].sort(); } diff --git a/scripts/layering/facade-symbols.ts b/scripts/layering/facade-symbols.ts deleted file mode 100644 index f6584f325..000000000 --- a/scripts/layering/facade-symbols.ts +++ /dev/null @@ -1,996 +0,0 @@ -// The exact exported-symbol list of every workspace package façade — the data -// half of the R11 gate, kept beside `package-boundaries.test.ts` so the -// behavioral tests there stay one bounded read (AGENTS.md module-size -// tripwires; a generated table is data, not behavior, and the two answer -// different questions). -// -// #1555 added the first such pin, for `@agent-device/ad-replay`; that one -// stays asserted inline beside the design rationale for its two-value façade. -// This table covers every other exported subpath. The exports-subpath checks -// prove WHICH files a package exposes; these lists prove WHAT those files -// name, so a façade cannot grow a symbol without editing the gate. -// -// The lists are the honest current surface, deliberately untrimmed: several -// are wider than their owners would design today (`contracts/interaction` -// alone names 140 symbols), and pinning the real number is what makes the -// next widening visible. Narrowing a façade is a change to that package, with -// its own consumers to fix — not a silent edit to this table. -// -// Maintaining it is mechanical: run `pnpm check:layering` and the assertion -// prints the exact added/removed names. -export const FACADE_SYMBOLS: readonly (readonly [string, readonly string[]])[] = [ - [ - '@agent-device/selectors', - [ - 'FIND_LOCATORS', - 'FIND_VALUE_REQUIRED_MESSAGE', - 'FindAction', - 'FindLocator', - 'IS_PREDICATE_USAGE_HINT', - 'IS_TEXT_VALUE_REQUIRED_MESSAGE', - 'IsPredicate', - 'SELECTOR_EXPRESSION_REQUIRED_MESSAGE', - 'SELECTOR_KEY_NAMES', - 'STALE_REF_HINT', - 'SelectorChainMatch', - 'SelectorChainMatchList', - 'SelectorProjection', - 'SelectorResolution', - 'SimpleSelectorTarget', - 'UNSUPPORTED_FIND_ACTION_HINT', - 'buildSelectorCandidates', - 'buildSelectorChainForNode', - 'checkElementTargetArgs', - 'checkFindArgs', - 'checkGetFormat', - 'checkIsArgs', - 'checkIsPredicate', - 'checkWaitText', - 'detectUnknownSelectorKeyToken', - 'evaluateIsPredicate', - 'findBestMatchesByLocator', - 'findSelectorChainMatch', - 'formatSelectorFailure', - 'isReadOnlyFindAction', - 'isRoleHintWord', - 'isSelectorToken', - 'isValidSelectorExpression', - 'listSelectorChainMatches', - 'normalizeIsPositionals', - 'normalizeSelectorText', - 'parseFindArgs', - 'parseFindSelectorExpression', - 'projectSelectorExpression', - 'readReplaySelectorDisplayValue', - 'readSelectorAlternatives', - 'readSelectorExpression', - 'readSimpleSelectorTarget', - 'resolveRecordedTarget', - 'resolveReplaySuggestionCandidate', - 'resolveSelectorChain', - 'selectorContainsValue', - 'selectorFailureHint', - 'splitSelectorFromArgs', - 'validateSelectorExpression', - ], - ], - [ - '@agent-device/selectors/ast', - [ - 'SelectorChain', - 'SelectorDiagnostics', - 'findSelectorChainMatch', - 'formatSelectorFailure', - 'isNodeEditable', - 'isNodeVisible', - 'isSelectorToken', - 'parseSelectorChain', - 'resolveSelectorChain', - 'tryParseSelectorChain', - ], - ], - [ - '@agent-device/ad-script', - [ - 'LocalIdentity', - 'ParsedReplayScript', - 'REPLAY_VAR_KEY_RE', - 'ReplayScriptMetadata', - 'TARGET_ANNOTATION_MAX_ANCESTRY', - 'TARGET_ANNOTATION_MAX_FIELD_BYTES', - 'TARGET_ANNOTATION_MAX_PAYLOAD_BYTES', - 'annotationLocalIdentity', - 'appendScriptSeriesFlags', - 'buildReplayVarScope', - 'classifyTargetBindingMatch', - 'collectReplayScrubbableVarValues', - 'collectReplayShellEnv', - 'demoteNonUniqueLocalIdentity', - 'firstAncestryMismatch', - 'formatDivergenceActionLabel', - 'formatPortableActionLine', - 'formatScriptArg', - 'formatScriptStringLiteral', - 'formatTargetAnnotationLines', - 'idMatchCountInTree', - 'identityFieldMismatches', - 'isClickLikeCommand', - 'isTouchTargetCommand', - 'localIdentitiesEqual', - 'matchesAncestryPrefix', - 'matchesLocalIdentity', - 'parseReplayCliEnvEntries', - 'parseReplayScriptDetailed', - 'parseTargetAnnotationV1Payload', - 'readNodeLocalIdentity', - 'readNodeStructuralDenotation', - 'readReplayCliEnvEntries', - 'readReplayScriptMetadata', - 'readReplayShellEnvSource', - 'resolveDeclaredScriptPlatform', - 'resolveReplayAction', - 'serializeTargetAnnotationV1', - 'siblingOrdinal', - 'stripRecordedRefGeneration', - 'structuralDenotationsEqual', - 'utf8ByteLength', - ], - ], - [ - '@agent-device/contracts/client', - [ - 'AgentDeviceCapabilitiesResult', - 'AgentDeviceClientConfig', - 'AgentDeviceDaemonTransport', - 'AgentDeviceDaemonTransportContext', - 'AgentDeviceDevice', - 'AgentDeviceIdentifiers', - 'AgentDeviceRequestOverrides', - 'AgentDeviceSelectionOptions', - 'AgentDeviceSession', - 'AgentDeviceSessionDevice', - 'AlertCommandOptions', - 'AppCloseOptions', - 'AppCloseResult', - 'AppDeployOptions', - 'AppDeployResult', - 'AppInstallFromSourceOptions', - 'AppInstallFromSourceResult', - 'AppInstallOptions', - 'AppListOptions', - 'AppOpenOptions', - 'AppOpenResult', - 'AppPushOptions', - 'AppStateCommandOptions', - 'AppTriggerEventOptions', - 'AudioOptions', - 'BatchRunOptions', - 'BatchStep', - 'CaptureDiffOptions', - 'CaptureScreenshotOptions', - 'CaptureScreenshotResult', - 'CaptureSnapshotOptions', - 'CaptureSnapshotResult', - 'ClickOptions', - 'ClipboardCommandOptions', - 'CloudArtifactsOptions', - 'CommandExecutionOptions', - 'CommandRequestResult', - 'DeviceBootOptions', - 'DeviceCommandBaseOptions', - 'DeviceShutdownOptions', - 'DoctorCommandOptions', - 'DragOptions', - 'ElementTarget', - 'EventsOptions', - 'FillOptions', - 'FindBaseOptions', - 'FindOptions', - 'FindSnapshotCommandOptions', - 'FlingOptions', - 'FocusOptions', - 'GetOptions', - 'InteractionTarget', - 'InternalRequestOptions', - 'IsOptions', - 'IsStatePredicateOptions', - 'IsTextPredicateOptions', - 'JsonObject', - 'JsonPrimitive', - 'JsonValue', - 'KeyboardCommandOptions', - 'Lease', - 'LeaseAllocateOptions', - 'LeaseOptions', - 'LeaseScopedOptions', - 'LogsOptions', - 'LongPressOptions', - 'MaterializationReleaseOptions', - 'MaterializationReleaseResult', - 'NetworkOptions', - 'PanOptions', - 'PerfOptions', - 'PermissionTarget', - 'PinchOptions', - 'PointTarget', - 'PrepareCommandOptions', - 'PressOptions', - 'ReactNativeCommandOptions', - 'RecordControlOptions', - 'RecordOptions', - 'RefTarget', - 'RepeatedPressOptions', - 'ReplayRunOptions', - 'ReplayTestOptions', - 'RotateGestureOptions', - 'ScrollOptions', - 'SelectorSnapshotCommandOptions', - 'SelectorTarget', - 'SessionCloseResult', - 'SessionSaveScriptOptions', - 'SessionSaveScriptResult', - 'SettingsUpdateOptions', - 'SettleCommandOptions', - 'StartupPerfSample', - 'SwipeGestureOptions', - 'SwipeOptions', - 'TraceOptions', - 'TransformGestureOptions', - 'TypeTextOptions', - 'ViewportCommandOptions', - 'WaitCommandOptions', - 'WaitCommandTarget', - 'isRecord', - ], - ], - [ - '@agent-device/contracts/command', - [ - 'CliFlags', - 'CommandFlags', - 'DEFAULT_BATCH_MAX_STEPS', - 'DaemonBatchStep', - 'DaemonExcludedCliFlag', - 'DispatchedCommand', - 'IOS_SAFARI_BUNDLE_ID', - 'MaestroRuntimeFlags', - 'PrepareCommandResult', - 'PrepareIosRunnerArtifactState', - 'PrepareIosRunnerCacheKind', - 'PrepareIosRunnerTiming', - 'PushCommandResult', - 'assertBatchStepCount', - 'isDeepLinkTarget', - 'isValidBatchMaxSteps', - 'isWebUrl', - 'parseBatchStepRuntime', - 'readBatchStepInputObject', - 'readBatchStepRecord', - 'readOptionalInteger', - 'resolveIosDeviceDeepLinkBundleId', - ], - ], - [ - '@agent-device/contracts/device', - [ - 'AppStateCommandResult', - 'AppsFilter', - 'BootCommandResult', - 'DEFAULT_APPS_FILTER', - 'DEVICE_ROTATIONS', - 'DEVICE_ROTATION_SURFACE_INDEX', - 'DeviceInventoryGroup', - 'DeviceInventoryGroupCounts', - 'DeviceInventoryProvider', - 'DeviceInventoryRequest', - 'DeviceLease', - 'DeviceRotation', - 'LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS', - 'LeaseLifecycleContext', - 'LeaseLifecycleProvider', - 'ProviderDeviceInstallOptions', - 'ProviderDeviceInstallResult', - 'ProviderDeviceRuntime', - 'ProviderExpiredLeaseRecovery', - 'ProviderPortReverseOptions', - 'ShutdownCommandResult', - 'TargetShutdownResult', - 'TriggerAppEventCommandResult', - 'WEB_DESKTOP_DEVICE', - 'assertResolvedAppsFilter', - 'countDeviceInventoryByGroup', - 'deviceRotationOrientation', - 'deviceRotationSurfaceDegrees', - 'parseDeviceRotation', - 'resolveAppsFilter', - 'shouldUseHostMacFastPath', - ], - ], - [ - '@agent-device/contracts/interaction', - [ - 'ALERT_ACTIONS', - 'ALERT_ACTION_RETRY_MS', - 'ALERT_POLL_INTERVAL_MS', - 'AlertAction', - 'AlertInfo', - 'AlertPlatform', - 'AlertSource', - 'AppSwitcherCommandResult', - 'AppleTvRemoteButton', - 'BACK_MODES', - 'BackCommandResult', - 'BackMode', - 'CLICK_BUTTONS', - 'COORDINATE_GESTURE_KINDS', - 'ClickButton', - 'ClickCommandResponseData', - 'ClipboardCommandResult', - 'CoordinateGesturePayload', - 'DEFAULT_ALERT_TIMEOUT_MS', - 'DEFAULT_DRAG_DESTINATION_HOLD_MS', - 'DEFAULT_DRAG_MOVE_MS', - 'DEFAULT_DRAG_SOURCE_HOLD_MS', - 'DisambiguationTiebreak', - 'DragGestureInput', - 'DragGesturePayload', - 'ElementSelectorKey', - 'ElementSelectorTapOptions', - 'ElementTarget', - 'FillCommandResponseData', - 'FillCommandResult', - 'FindCommandResponseData', - 'FlingGesturePayload', - 'GESTURE_DURATION_MAX_MS', - 'GESTURE_DURATION_MIN_MS', - 'GESTURE_FLING_DURATION_MS', - 'GESTURE_INITIAL_ANGLE_DEGREES', - 'GESTURE_KINDS', - 'GESTURE_SAMPLE_INTERVAL_MS', - 'GestureCommandInput', - 'GestureExecutionProfile', - 'GestureIntent', - 'GesturePayload', - 'GesturePlan', - 'GesturePointerCount', - 'GestureReferenceFrame', - 'GestureSamplingProfile', - 'GestureSemanticInput', - 'GuaranteeEnforcement', - 'HomeCommandResult', - 'INTERACTION_DISPATCH_PATHS', - 'INTERACTION_GUARANTEES', - 'INTERACTION_PATH_IDS', - 'InPageSwipeGesturePlan', - 'InteractionEvidence', - 'InteractionGuarantee', - 'InteractionPathContract', - 'InteractionPathId', - 'InteractionTarget', - 'Interactor', - 'KeyboardCommandResult', - 'LongPressCommandResponseData', - 'LongPressCommandResult', - 'MAESTRO_NON_HITTABLE_FALLBACK_MESSAGE', - 'MultiTouchGesturePlan', - 'NormalizedPublicGesture', - 'OrientationCommandResult', - 'PanGesturePayload', - 'PinchGesturePayload', - 'PointTarget', - 'PointerTrajectory', - 'PointerTrajectorySample', - 'PressCommandResponseData', - 'PressCommandResult', - 'RecordingTargetOverride', - 'RefTarget', - 'ResolutionDiagnosticEntry', - 'ResolutionDisclosure', - 'ResolvedInteractionTarget', - 'ResolvedTarget', - 'RotateCommandResult', - 'RotateGesturePayload', - 'RunnerCallOptions', - 'RunnerContext', - 'SCROLL_DIRECTIONS', - 'SCROLL_DURATION_MAX_MS', - 'SCROLL_INPUT_DIRECTIONS', - 'SWIPE_PATTERNS', - 'SWIPE_PAUSE_MAX_MS', - 'SWIPE_PRESETS', - 'SWIPE_REPETITION_MAX', - 'SWIPE_SERIES_MAX_SCHEDULED_DURATION_MS', - 'ScreenshotOptions', - 'ScrollCommandOptions', - 'ScrollDirection', - 'ScrollDistanceOptions', - 'ScrollGestureOptions', - 'ScrollGesturePlan', - 'ScrollInputDirection', - 'ScrollTimingOptions', - 'SelectorTarget', - 'SettleDiffLine', - 'SettleObservation', - 'SettleParams', - 'SettleTailEntry', - 'SinglePointerGesturePlan', - 'SinglePointerTrajectory', - 'SnapshotOptions', - 'SnapshotResult', - 'SwipeGesturePayload', - 'SwipePattern', - 'SwipePayload', - 'SwipePreset', - 'SwipePresetGesturePlan', - 'TEXT_ENTRY_ROUTES', - 'TV_REMOTE_BUTTONS', - 'TV_REMOTE_BUTTON_USAGE', - 'TextEntryRoute', - 'TransformGestureParams', - 'TransformGesturePayload', - 'TvRemoteButton', - 'TvRemoteCommandResult', - 'TypeTextBackendResult', - 'VegaTvRemoteKey', - 'WAIT_REASONS', - 'WaitCommandResult', - 'WaitReason', - 'assertExclusiveScrollDistanceInputs', - 'assertNoRemovedSwipeInput', - 'assertScrollGestureInput', - 'buildDragGesturePlan', - 'buildGesturePlan', - 'buildInPageSwipeGesturePlan', - 'buildScrollGesturePlan', - 'buildSwipePresetGesturePlan', - 'buttonTag', - 'clampGestureCoordinate', - 'describeReplayGestureArityError', - 'dragGesturePayloadFromPositionals', - 'gestureDirectionDelta', - 'gesturePayloadFromPositionals', - 'gesturePayloadToPositionals', - 'getClickButtonValidationError', - 'honoredScrollDurationMs', - 'inferGestureReferenceFrame', - 'interpolateGesturePoint', - 'normalizeGestureCommandInput', - 'normalizePublicGesture', - 'normalizePublicSwipeMotion', - 'normalizeScrollDurationMs', - 'parseScrollDirection', - 'parseTvRemoteButton', - 'readGesturePayload', - 'resolveClickButton', - 'sampleGestureOffsets', - 'singlePointerPlanEndpoints', - 'swipePayloadFromPositionals', - 'toAndroidTvRemoteKeyevent', - 'toAppleTvRemoteButton', - 'toVegaTvRemoteKey', - 'tvRemoteDurationMode', - ], - ], - [ - '@agent-device/contracts/capture', - [ - 'AndroidSnapshotBackendMetadata', - 'BackendSnapshotOptions', - 'BackendSnapshotResult', - 'DiffSnapshotCommandResult', - 'FindLocator', - 'PublicSnapshotCaptureAnnotations', - 'SCREENSHOT_ACTION_FLAG_KEYS', - 'SCREENSHOT_COMMAND_FLAG_KEYS', - 'SCREENSHOT_SPECIFIC_FLAG_DEFINITIONS', - 'ScreenshotDispatchFlags', - 'ScreenshotPublicOptions', - 'ScreenshotRequestFlags', - 'ScreenshotResultData', - 'ScreenshotRuntimeFlags', - 'ScreenshotRuntimeOptions', - 'SnapshotCaptureAnalysis', - 'SnapshotCaptureAnnotations', - 'SnapshotCaptureFreshness', - 'SnapshotDiagnosticsState', - 'SnapshotDiagnosticsSummary', - 'SnapshotDiffLine', - 'SnapshotDiffSummary', - 'SnapshotTimingSample', - 'SnapshotTimingStats', - 'ViewportCommandResult', - 'appendScreenshotScriptFlags', - 'mergeSnapshotDiagnostics', - 'publicSnapshotCaptureAnnotations', - 'readScreenshotScriptFlag', - 'readSerializedSnapshotCaptureAnnotations', - 'readSnapshotDiagnosticsSummary', - 'recordSnapshotTiming', - 'screenshotFlagsFromOptions', - 'screenshotOptionsFromFlags', - 'snapshotCaptureAnnotationsFrom', - 'summarizeSnapshotDiagnostics', - 'summarizeSnapshotTimingSamples', - ], - ], - [ - '@agent-device/contracts/platform', - [ - 'ANDROID_SYSTEM_CHROME_PACKAGE', - 'AndroidInputOwner', - 'AndroidInputOwnership', - 'AndroidInputOwnershipSource', - 'AndroidSystemChromeProvenance', - 'AudioProbeResult', - 'AudioProbeSource', - 'EmptyAudioProbeResultOptions', - 'NormalizeAudioProbeRecordOptions', - 'PlatformGatedProviderResolverKey', - 'PlatformPlugin', - 'RunnerLogicalLeaseContext', - 'assertAppleMultiTouchSupported', - 'classifyAndroidInputOwner', - 'classifyAndroidInputOwnership', - 'emptyAudioProbeResult', - 'hasAndroidSystemChromeProvenance', - 'isAndroidInputMethodOwnedNode', - 'isAndroidSystemChromeWindowResourceId', - 'isAudioProbeSupportedDevice', - 'isFallbackAndroidInputMethodPackage', - 'isFallbackAndroidInputMethodResource', - 'isHostSystemAudioProbeDevice', - 'normalizeAudioProbeRecord', - 'parseAndroidInputMethodPackage', - 'readAndroidActiveInputMethodPackage', - 'stripAndroidSystemChromeProvenance', - 'stripAndroidSystemChromeProvenanceFromNode', - ], - ], - [ - '@agent-device/contracts/settings', - [ - 'PermissionAction', - 'PermissionTarget', - 'SETTINGS_INVALID_ARGS_MESSAGE', - 'SETTINGS_USAGE_OVERRIDE', - 'SettingOptions', - 'getUnsupportedMacOsSettingMessage', - 'isMacOsSettingSupported', - 'parsePermissionAction', - 'parsePermissionTarget', - ], - ], - [ - '@agent-device/contracts/session', - ['SESSION_SURFACES', 'SessionAction', 'SessionSurface', 'parseSessionSurface'], - ], - [ - '@agent-device/contracts/recording', - [ - 'DEFAULT_RECORDING_EXPORT_QUALITY', - 'RECORDING_EXPORT_QUALITIES', - 'RECORDING_SCOPE_VALUES', - 'RecordingAppIdentity', - 'RecordingBackendTag', - 'RecordingCommandResult', - 'RecordingExportQuality', - 'RecordingScope', - 'RecordingStartCommandResult', - 'RecordingStopCommandResult', - 'TraceCommandResult', - 'isRecordingExportQuality', - 'isWholeScreenRecordingScope', - 'recordingQualityInputToExportQuality', - ], - ], - [ - '@agent-device/contracts/observability', - [ - 'AgentArtifactsResult', - 'CloudArtifact', - 'CloudArtifactAvailability', - 'CloudArtifactKind', - 'CloudArtifactProvider', - 'CloudArtifactsQuery', - 'CloudArtifactsResult', - 'CloudArtifactsStatus', - 'CloudProviderSessionResult', - 'DaemonArtifactInventoryEntry', - 'DaemonArtifactsResult', - 'DebugSymbolsCrashFrame', - 'DebugSymbolsCrashSummary', - 'DebugSymbolsImage', - 'DebugSymbolsOptions', - 'DebugSymbolsResult', - 'DoctorCheck', - 'DoctorCommandResult', - 'DoctorKind', - 'DoctorStatus', - 'LAUNCH_CONSOLE_DIRECT_APP_ONLY_MESSAGE', - 'LAUNCH_CONSOLE_IOS_SIMULATOR_ONLY_MESSAGE', - 'LOG_ACTION_VALUES', - 'LogAction', - 'LogBackend', - 'NetworkEntry', - 'PERF_ACTION_ERROR_MESSAGE', - 'PERF_ACTION_VALUES', - 'PERF_AREA_ERROR_MESSAGE', - 'PERF_AREA_VALUES', - 'PERF_KIND_ERROR_MESSAGE', - 'PERF_KIND_VALUES', - 'PERF_MEMORY_KIND_ERROR_MESSAGE', - 'PERF_SUBJECT_ERROR_MESSAGE', - 'PERF_SUBJECT_VALUES', - 'PerfAction', - 'PerfArea', - 'PerfKind', - 'PerfMetricsSamplerTag', - 'PerfSubject', - 'isPerfAction', - 'isPerfArea', - 'isPerfKind', - 'isPerfMemoryKind', - 'isPerfSubject', - ], - ], - [ - '@agent-device/contracts/remote', - [ - 'CloudProviderProfileFields', - 'CompanionTunnelScope', - 'MetroBridgeResult', - 'MetroBridgeScope', - 'MetroPrepareKind', - 'MetroPrepareOptions', - 'MetroPrepareResult', - 'MetroReloadOptions', - 'MetroReloadResult', - 'PROVIDER_DEVICE_ORIENTATIONS', - 'PrepareMetroRuntimeResult', - 'ProviderConnectionResource', - 'ProviderConnectionVerification', - 'ProviderDeviceOrientation', - 'ReloadMetroResult', - 'RemoteConfigMetroOptions', - 'RemoteConnectionProfileFields', - 'ResolvedMetroKind', - ], - ], - [ - '@agent-device/contracts/replay', - [ - 'LocalIdentity', - 'MultiTargetAnnotationV1', - 'NodeStructuralDenotation', - 'REPLAY_TARGET_GUARD_MISMATCH_REASON', - 'RefFrameEffect', - 'ReplayCommandResult', - 'ReplaySuiteAttemptFailure', - 'ReplaySuiteResult', - 'ReplaySuiteTestFailed', - 'ReplaySuiteTestPassed', - 'ReplaySuiteTestResult', - 'ReplaySuiteTestSkipReason', - 'ReplaySuiteTestSkipped', - 'ReplayTargetGuardDenotation', - 'TargetAncestryEntry', - 'TargetAnnotationV1', - 'TargetRect', - 'TargetScrollRegion', - 'TargetVerification', - 'WAIT_LANDMARK_MISMATCH_REASON', - 'WaitLandmarkMismatchEvidence', - ], - ], - [ - '@agent-device/contracts/snapshot', - [ - 'buildSnapshotNodeMap', - 'extractNodeText', - 'findNearestScrollableAncestor', - 'findSnapshotAncestor', - 'isFillableType', - 'isNodeVisibleInEffectiveViewport', - 'isNodeVisibleOnScreen', - 'isScrollableNodeLike', - 'isScrollableType', - 'isTapPointInsideViewport', - 'isUsefulVisibilityAnchor', - 'normalizeType', - 'resolveEffectiveViewportRect', - 'resolveViewportRect', - ], - ], - [ - '@agent-device/contracts/divergence', - [ - 'REPLAY_DIVERGENCE_DEFAULT_REF_LIMIT', - 'REPLAY_DIVERGENCE_DIGEST_REF_LIMIT', - 'REPLAY_DIVERGENCE_LEVEL_BYTE_LIMITS', - 'REPLAY_DIVERGENCE_SUGGESTION_LIMIT', - 'ReplayDivergence', - 'ReplayDivergenceCause', - 'ReplayDivergenceKind', - 'ReplayDivergenceOverflow', - 'ReplayDivergenceResume', - 'ReplayDivergenceScreen', - 'ReplayDivergenceScreenRef', - 'ReplayDivergenceStep', - 'ReplayDivergenceStepSource', - 'ReplayDivergenceSuggestion', - 'ReplayDivergenceSuggestionBasis', - 'ReplayDivergenceTargetBinding', - 'ReplayDivergenceTargetBindingKind', - 'ReplayDivergenceTargetCandidate', - 'ReplayDivergenceTargetIdentity', - 'ReplayRepairHint', - 'ReplayVarScrubEntry', - 'applyReplayDivergenceLevelCaps', - 'boundReplayDivergence', - 'createReplayDivergenceSanitizer', - 'formatReplayDivergenceReport', - 'measureReplayDivergenceBytes', - 'sanitizeReplayDivergenceField', - 'scrubReplayVarValues', - 'truncateUtf8Field', - ], - ], - [ - '@agent-device/contracts/progress', - [ - 'CommandProgressEvent', - 'ReplayTestProgressEvent', - 'ReplayTestSuiteProgressEvent', - 'RequestProgressEvent', - 'RequestProgressSink', - ], - ], - [ - '@agent-device/kernel/errors', - [ - 'AppError', - 'AppErrorCode', - 'AppErrorDetails', - 'DaemonError', - 'KNOWN_APP_ERROR_CODES', - 'KnownAppErrorCode', - 'NormalizedError', - 'asAppError', - 'defaultHintForCode', - 'isAgentDeviceError', - 'normalizeAgentDeviceError', - 'normalizeError', - 'retriableForErrorCode', - 'throwDaemonError', - 'toAppErrorCode', - ], - ], - [ - '@agent-device/kernel/device', - [ - 'AppleOS', - 'ApplePlatform', - 'DEVICE_TARGETS', - 'DeviceInfo', - 'DeviceKind', - 'DeviceSelector', - 'DeviceTarget', - 'PLATFORMS', - 'PLATFORM_SELECTORS', - 'PUBLIC_PLATFORMS', - 'Platform', - 'PlatformSelector', - 'PublicPlatform', - 'deviceFieldsFromPublicPlatform', - 'isAppleOs', - 'isApplePlatform', - 'isIosFamily', - 'isMacOs', - 'isMobilePlatform', - 'isPlatform', - 'isPublicPlatform', - 'isSerialAddressablePlatform', - 'isTvOsDevice', - 'matchesDeviceSelector', - 'matchesPlatformSelector', - 'publicPlatformString', - 'resolveApplePlatformName', - 'resolveAppleSimulatorSetPathForSelector', - 'resolveDevice', - 'resolveDeviceAppleOs', - 'sortAppleDevicesForSelection', - ], - ], - [ - '@agent-device/kernel/snapshot', - [ - 'HiddenContentHint', - 'Point', - 'REF_GRAMMAR_HINT', - 'RawSnapshotNode', - 'Rect', - 'ScreenshotOverlayRef', - 'SnapshotBackend', - 'SnapshotNode', - 'SnapshotOptions', - 'SnapshotPresentationFlagInput', - 'SnapshotQualityVerdict', - 'SnapshotState', - 'SnapshotUnchanged', - 'SnapshotVisibility', - 'SnapshotVisibilityReason', - 'SplitRef', - 'attachRefs', - 'buildSnapshotPresentationKey', - 'centerOfRect', - 'findNodeByRef', - 'isSnapshotBackend', - 'normalizeRef', - 'snapshotPresentationOptionsFromFlags', - 'splitRefGenerationSuffix', - 'usesMobileSnapshotPresentation', - ], - ], - [ - '@agent-device/kernel/contracts', - [ - 'AppErrorCode', - 'CommandRpcParams', - 'DaemonArtifact', - 'DaemonArtifactKnownType', - 'DaemonArtifactType', - 'DaemonInstallSource', - 'DaemonLockPolicy', - 'DaemonRequest', - 'DaemonRequestMeta', - 'DaemonResponse', - 'DaemonResponseData', - 'DaemonServerMode', - 'DaemonTransportPreference', - 'JsonRpcId', - 'JsonRpcRequestEnvelope', - 'LeaseBackend', - 'NETWORK_INCLUDE_MODES', - 'NetworkIncludeMode', - 'RESPONSE_LEVELS', - 'Rect', - 'ResponseCost', - 'ResponseLevel', - 'SessionIsolationMode', - 'SessionRuntimeHints', - 'SnapshotNode', - 'centerOfRect', - 'commandRpcParamsSchema', - 'daemonRuntimeSchema', - 'defaultHintForCode', - 'isNonDefaultResponseLevel', - 'jsonRpcRequestSchema', - 'normalizeError', - ], - ], - ['@agent-device/kernel/collections', ['uniqueStrings']], - [ - '@agent-device/kernel/rect', - [ - 'containsPoint', - 'isPositiveFiniteRect', - 'isRectVisibleInViewport', - 'pickLargestRect', - 'rectArea', - 'rectContains', - ], - ], - ['@agent-device/kernel/redaction', ['redactDiagnosticData']], - ['@agent-device/kernel/bounds', ['parseBounds']], - [ - '@agent-device/maestro', - [ - 'MAESTRO_COMPATIBILITY_ADR_URL', - 'MAESTRO_COMPATIBILITY_ISSUE_URL', - 'MAESTRO_COMPAT_LIMITATIONS', - 'MAESTRO_COMPAT_SUPPORTED_CAPABILITIES', - 'MAESTRO_RUNTIME_ADAPTER_POLICY', - 'MAESTRO_SELECTOR_PROJECTION', - 'MaestroActionEvent', - 'MaestroCompletedActionEvent', - 'MaestroDispatchSelector', - 'MaestroExecutionObserver', - 'MaestroExecutionOptions', - 'MaestroExecutionOutcome', - 'MaestroExportOptions', - 'MaestroExportResult', - 'MaestroExportWarning', - 'MaestroFailedAction', - 'MaestroFlow', - 'MaestroObservation', - 'MaestroObservationCondition', - 'MaestroObservationIdentity', - 'MaestroPlatform', - 'MaestroRuntimeCommand', - 'MaestroRuntimeMetrics', - 'MaestroRuntimeOperationContext', - 'MaestroRuntimeOperationResult', - 'MaestroRuntimeOperations', - 'MaestroRuntimePort', - 'MaestroRuntimePortLifecycle', - 'MaestroRuntimeReadContext', - 'MaestroSelector', - 'MaestroSelectorProjection', - 'MaestroSinglePointerGestureInput', - 'MaestroSnapshotTargetQuery', - 'MaestroTargetMatch', - 'MaestroTargetQuery', - 'MaestroTargetResolution', - 'collectMaestroFailureSuggestions', - 'createMaestroRuntimePort', - 'executeMaestroFlow', - 'exportReplayActionsToMaestro', - 'formatMaestroCompatibilityReference', - 'inspectMaestroFlow', - 'literalFromMaestroRegex', - 'maestroObservationMatches', - 'maestroTestFailure', - 'resolveMaestroScrollableGesture', - 'resolveMaestroTargetFromSnapshot', - ], - ], - [ - '@agent-device/provider-limrun', - [ - 'LIMRUN_PROVIDER', - 'LimrunAndroidDeviceSession', - 'LimrunIosCommandExecution', - 'LimrunIosDeviceSession', - 'LimrunRuntime', - 'LimrunRuntimeDependencies', - 'LimrunRuntimeOptions', - 'createLimrunRuntime', - 'verifyLimrunConnection', - ], - ], - [ - '@agent-device/provider-webdriver', - [ - 'CLOUD_WEBDRIVER_PROVIDERS', - 'CloudWebDriverConnectionVerification', - 'CloudWebDriverConnectionVerificationOptions', - 'CloudWebDriverKnownProviderName', - 'DefaultCloudWebDriverArtifactEnv', - 'DefaultCloudWebDriverProviderRuntimeEnv', - 'ProviderWebDriver', - 'ProviderWebDriverDependencies', - 'RunHostCommand', - 'createProviderWebDriver', - 'isCloudWebDriverProviderName', - 'readAwsDeviceFarmRegionFromArn', - 'rejectBrowserStackOnlyDeviceFeatures', - ], - ], - [ - '@agent-device/replay-test', - [ - 'ReplayTestAttemptFailed', - 'ReplayTestAttemptOutcome', - 'ReplayTestAttemptStepSink', - 'ReplayTestBindAttemptCancellation', - 'ReplayTestDiscoverSources', - 'ReplayTestManifest', - 'ReplayTestResolveShardTargets', - 'ReplayTestShardContext', - 'ReplayTestShardMode', - 'ReplayTestShardTarget', - 'ReplayTestSource', - 'ReplayTestSuiteRequest', - 'runReplayTestSuite', - ], - ], - [ - '@agent-device/xml', - [ - 'XmlNode', - 'decodeXmlCharacterReferences', - 'escapeXmlTextAndAttribute', - 'parseXmlDocumentSync', - ], - ], -]; diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index 03e993b5b..c1bbc69e4 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -7,8 +7,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { test } from 'node:test'; import { listSourceFiles } from './check.ts'; -import { readFacadeExports, readNamedExports } from './facade-exports.ts'; -import { FACADE_SYMBOLS } from './facade-symbols.ts'; +import { readDirectNamedExports, readNamedExports } from './facade-exports.ts'; import { checkPackageBoundaries, checkPackageInternalSites, @@ -77,34 +76,93 @@ test('specifier sites carry 1-based lines for static and dynamic imports', () => ); }); -test('every workspace package façade exports exactly its pinned symbol list', () => { +test('every workspace package façade names its exports explicitly (no bare `export *`)', () => { + // #1574 built a hand-maintained pin table (`facade-symbols.ts`, 816 symbols across every + // workspace-package façade) plus a ~200-line star-chain resolver (`readFacadeExports`) whose + // entire job was enumerating what `export *` hides. Once a façade names its exports explicitly, + // the façade file itself IS the pin — a widening shows up in the diff of the file that grew, + // not in a table two files away that only a gate failure would surface. This structural gate is + // what keeps that property true: every façade a package manifest declares (`exportTargets`), + // plus every file under a `packages/*/src/facades/` directory, must parse through + // `readNamedExports` without hitting the bare-`export *`/`export default` rejection it already + // implements — reusing that check rather than writing a second, regex-based one that would have + // to independently rediscover every export form to be trustworthy. const packages = readWorkspacePackages(repoRoot); - const pinned = new Map(FACADE_SYMBOLS.map(([specifier, names]) => [specifier, names])); - // The table and the manifests must agree in BOTH directions: a new package - // (or a new subpath on an existing one) that nobody pinned is exactly the - // widening this gate exists to catch, so an unpinned façade fails here - // rather than being silently skipped. - const declared = packages - .filter((pkg) => pkg.name !== '@agent-device/ad-replay') - .flatMap((pkg) => [...pkg.exportTargets.keys()]); - assert.deepEqual( - declared.slice().sort(), - [...pinned.keys()].sort(), - 'every exports-map subpath needs a pinned symbol list (and vice versa)', - ); - for (const pkg of packages) { - for (const [specifier, target] of pkg.exportTargets) { - const expected = pinned.get(specifier); - if (!expected) continue; + const facadeFiles = new Set(packages.flatMap((pkg) => [...pkg.exportTargets.values()])); + for (const file of listSourceFiles()) { + if (file.includes('/src/facades/')) facadeFiles.add(file); + } + assert.ok(facadeFiles.size > 0, 'expected at least one workspace package façade to check'); + for (const file of [...facadeFiles].sort()) { + const source = fs.readFileSync(path.join(repoRoot, file), 'utf8'); + try { + readNamedExports(source); + } catch (error) { + assert.fail( + `${file} must name its exports explicitly instead of a bare \`export *\` (or an ` + + '`export default`) — a façade widened by a star re-export hides the new symbol from ' + + 'its own diff, exactly what the retired symbol-pin table (#1574) used to catch by ' + + `hand. Underlying error: ${(error as Error).message}`, + ); + } + } +}); + +test('every façade re-exports its sources exhaustively (no silent narrowing)', () => { + // The star-rejection above catches a façade WIDENING invisibly. This catches the + // opposite, which is the failure an explicit list makes newly possible: a symbol + // added to a source module simply never reaches the façade, and nothing notices. + // `export *` could not narrow by construction; an explicit list can, so the + // property `export *` gave for free is asserted here instead. + // + // Found by review on #1614: this conversion was generated against the surface at + // fork time, and #1567 landed 13 new exports meanwhile (`DragOptions`, the drag + // gesture vocabulary, `MultiTargetAnnotationV1`). The rebase silently dropped all + // 13 and only a human diff caught it. Exhaustiveness is what makes that mechanical. + // + // Scoped to `packages/*/src/facades/` — the barrels this PR converted, which were + // exhaustive by construction because `export *` cannot narrow. A hand-curated + // package `index.ts` is a different thing: `@agent-device/ad-replay` deliberately + // publishes two values out of a much larger `internal/`, and forcing it exhaustive + // would widen a surface its owner narrowed on purpose (#1555). + const facadeFiles = listSourceFiles().filter((file) => file.includes('/src/facades/')); + assert.ok(facadeFiles.length > 0, 'expected at least one converted façade to check'); + for (const file of [...facadeFiles].sort()) { + const absolute = path.join(repoRoot, file); + const exported = new Set(readNamedExports(fs.readFileSync(absolute, 'utf8'))); + for (const specifier of reExportSources(fs.readFileSync(absolute, 'utf8'))) { + const sourcePath = path.resolve(path.dirname(absolute), specifier); + if (!fs.existsSync(sourcePath)) continue; + // A source may itself carry a bare `export *` (contracts' `gesture-plan.ts` + // stars `gesture-plan-types.ts`). Read its DIRECT exports rather than skipping + // the file: skipping would also drop `buildDragGesturePlan` and friends from + // this check, so removing one from a façade would narrow the surface silently + // (#1614 review P2). The starred names are covered because the façade + // re-exports the starred module directly too, and that path is checked here on + // its own turn. + const sourceNames = readDirectNamedExports(fs.readFileSync(sourcePath, 'utf8')); + const dropped = sourceNames.filter((name) => name !== 'default' && !exported.has(name)); assert.deepEqual( - readFacadeExports(path.join(repoRoot, target)), - [...expected], - `${specifier} exports exactly its pinned symbol list`, + dropped, + [], + `${file} re-exports from ${specifier} but omits ${dropped.join(', ')} — an explicit ` + + 'façade list must stay exhaustive over its sources, or a symbol added upstream ' + + 'silently never becomes public. Add the names, or move them out of that module.', ); } } }); +/** The relative specifiers a façade re-exports from, in source order. */ +function reExportSources(source: string): string[] { + const found = new Set(); + for (const match of source.matchAll(/\bfrom\s+'(\.[^']*)'/g)) { + const specifier = match[1]; + if (specifier) found.add(specifier); + } + return [...found]; +} + test('double-quoted and re-export routes into packages are not invisible to R11', () => { // The scanner is the layering parser, so quote style and statement form // cannot carve out a bypass: a double-quoted import, a re-export, and a @@ -330,7 +388,9 @@ test('the real tree parses, declares, and passes R11', () => { '@agent-device/kernel', ]); assert.deepEqual( - readFacadeExports(path.join(repoRoot, 'packages/selectors/src/index.ts')).filter((name) => + readNamedExports( + fs.readFileSync(path.join(repoRoot, 'packages/selectors/src/index.ts'), 'utf8'), + ).filter((name) => ['Selector', 'SelectorChain', 'SelectorTerm', 'SelectorKey', 'parseSelectorChain'].includes( name, ),