From b138f52e12fdda3942855232a2a5bb3ed50d0943 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 14 Nov 2026 17:08:37 +0800 Subject: [PATCH] refactor: prune internal seams and preserve configuration ownership (#10430) * refactor: prune internal seams and preserve configuration ownership * fix(setup): keep YAML aliases and inherited registration settings on refresh - Reuse an unchanged YAML collection only while every alias in it still resolves after the edit; otherwise rebuild it so the alias becomes its value. A sibling anchor replaced or moved in the same refresh no longer leaves an unresolved alias in Hermes or Goose output. - Keep a connection selector whose existing value already chooses stdio (for example an inherited Hermes transport: stdio) instead of superseding it; remote selectors are still replaced. Explicit stdio selectors are therefore no longer rewritten (Kimi expectation updated). - When the whole Hermes registration is inherited through a merge, override it with that merge and write only the changed fields. * fix(setup): edit registration fields whose YAML key is an alias A key written as an alias to a string scalar (for example *field with field: &field env) was treated as a source-only key, so any change to its value was refused in Hermes and Goose. Resolve such keys to their string, keep the aliased spelling while its anchor survives the edit, and write the plain key when the same edit rebuilds the anchor. * test(process): bound cleanup-boundary waits and always release gates Both cancellation tests awaited a bare started promise and released their gates only on success, so a missed boundary waited for the outer deadline and could leave cleanup parked. Use awaitAcknowledgement and release and join the cleanup in finally. * fix(setup): repair only aliases an edit leaves unresolved Replace the pre-edit alias survival guess, which expanded aliases to sibling anchors that survive unchanged, with an exact repair: record what each alias in the replaced value resolves to, write the edit, then replace only aliases left without a preceding anchor by that value. - Keep an aliased key's source position, so its anchor stays ahead of later aliases. - Treat an aliased key as its string only when no other key in the map resolves to the same string; an ambiguous one stays guarded. - Override an inherited Hermes registration only when the requested entry keeps every inherited field, so an explicit replacement can drop remote selectors. * fix(setup): repair aliases whose binding an edit changes, keeping comments An alias whose own anchor was removed could resolve to an earlier anchor with the same name and silently change value. Record the node each alias resolved to and repair any alias that no longer resolves to that node. Carry the alias's comments and spacing onto the repaired value. * test(setup): split the Hermes anchor and merge refresh cases * fix(setup): refuse removing an overridden key its merge would restore The merge-retention check skipped keys the mapping also overrides, so dropping such a key (for example a remote url during an explicit stdio replacement) let the retained merge restore it. Check removal before skipping overridden keys, so the edit is refused with the existing message instead of producing a registration that cannot be applied. --- .oxlintrc.json | 25 +- docs/http3-capture-lab.md | 4 +- scripts/check-dependency-install.mjs | 2 +- scripts/ci/cache-retention.d.mts | 2 - scripts/ci/cache-retention.mjs | 2 +- scripts/ci/failure-report.d.mts | 6 - scripts/ci/failure-report.mjs | 2 +- scripts/ci/pr-ci-health.d.mts | 1 - scripts/ci/pr-ci-health.mjs | 2 +- .../ci/production-reachability-report.d.mts | 3 - scripts/ci/production-reachability-report.mjs | 4 +- scripts/fixtures/apple/macos-bundle.mjs | 2 +- scripts/lib/browser-verifier-assertions.mjs | 2 +- scripts/lib/cursor-plugin-bundle.d.mts | 13 - scripts/lib/cursor-plugin-bundle.mjs | 17 +- scripts/lib/ghidra-windows-byte-search.mjs | 2 +- scripts/lib/hopper-deadline-cleanup.d.mts | 16 + scripts/lib/hopper-deadline-cleanup.mjs | 71 ++++ scripts/lib/http3-capture-assertions.mjs | 2 +- .../lib/javascript-application-analysis.mjs | 30 +- scripts/lib/mcp-verifier-results.mjs | 2 +- scripts/lib/module-boundaries.test.mjs | 176 -------- scripts/lib/owned-verifier-terminal.d.mts | 3 - scripts/lib/real-hopper-cleanup.d.mts | 10 +- scripts/lib/real-hopper-cleanup.mjs | 52 ++- scripts/lib/real-hopper-semantic.d.mts | 9 +- scripts/lib/real-hopper-semantic.mjs | 2 +- scripts/lib/runtime-admission.d.mts | 3 - scripts/lib/runtime-admission.mjs | 2 +- scripts/lib/verifier-artifact-scope.d.mts | 15 + scripts/lib/verifier-artifact-scope.mjs | 32 ++ scripts/lib/verifier-terminal-owner.mts | 6 +- scripts/lint/index.mjs | 4 + .../lint/rules/no-babel-visitor-keys.d.mts | 4 + scripts/lint/rules/no-babel-visitor-keys.mjs | 73 ++++ scripts/lint/rules/no-locale-compare.d.mts | 4 + scripts/lint/rules/no-locale-compare.mjs | 34 ++ scripts/verify-package-environment.d.mts | 10 - scripts/verify-package-environment.mjs | 2 +- scripts/verify-real-ghidra-assertions.mjs | 2 +- scripts/verify-real-ghidra-dol.mjs | 9 +- scripts/verify-real-hopper-callers.mjs | 66 +-- scripts/verify-real-hopper-deadlines.mjs | 35 +- scripts/verify-real-hopper-fat.mjs | 116 +++-- scripts/verify-real-hopper.mjs | 16 +- scripts/verify-test-discovery.mjs | 6 +- scripts/verify/apple/dispatch.mjs | 10 +- scripts/verify/architecture-guards.mjs | 35 +- .../verify/javascript/runtime-observation.mjs | 28 +- scripts/verify/managed/manifest.mjs | 14 +- scripts/verify/rea-numeric-limits.d.mts | 9 +- scripts/verify/rea-numeric-limits.mjs | 17 +- src/android/AndroidNativeInventory.ts | 8 +- src/android/JadxProtocol.ts | 5 - src/apkSignature/apkSignatureFailures.ts | 8 +- src/application/ClientRegistration.ts | 11 +- src/application/ClientRegistrationProfile.ts | 25 ++ .../GooseConfigurationYaml.test.ts | 159 +++++++ src/application/GooseConfigurationYaml.ts | 26 +- .../HermesConfigurationYaml.test.ts | 256 +++++++++++ src/application/HermesConfigurationYaml.ts | 161 ++++++- src/application/Uninstall.ts | 6 +- src/application/YamlConfigurationAliases.ts | 30 ++ src/application/YamlConfigurationSource.ts | 402 ++++++++++++++++-- .../analysisView/AnalysisViewService.test.ts | 2 +- .../analysisView/AnalysisViewService.ts | 6 +- .../analysisView/BrowserScenarioViews.test.ts | 2 +- .../JavaScriptProvenanceViews.test.ts | 2 +- .../analysisView/NativeLoadImageViews.test.ts | 6 +- .../NativeObservationViews.test.ts | 4 +- .../ArtifactComparisonEvidence.test.ts | 6 +- .../artifacts/ArtifactComparisonEvidence.ts | 2 +- src/application/binary/BinarySession.ts | 6 +- src/application/binary/BinarySessionPort.ts | 1 - .../desktop/DesktopBuildCapture.ts | 8 +- .../desktop/DesktopBuildCaptureFailure.ts | 6 +- .../desktop/DesktopBuildCommands.ts | 5 - .../environment/environmentSource.ts | 2 +- .../environment/environmentState.ts | 2 +- .../NativeFunctionMatchingViews.test.ts | 6 +- src/artifacts/StableArtifactFailure.ts | 43 +- src/artifacts/createArtifactReader.ts | 2 - .../history/playwrightTrace/TraceFailure.ts | 8 +- src/cli/analysisEvidenceDelivery.ts | 2 +- src/cli/javascriptApplicationAnalysis.ts | 2 +- src/composition/binaryDiagnostics.ts | 3 - src/composition/capabilityServices.ts | 27 +- src/composition/captures.ts | 7 +- src/composition/evm.ts | 8 +- src/composition/firmware.ts | 7 +- src/composition/javascriptRecovery.ts | 7 +- src/composition/signals.ts | 7 +- src/config/bridgeMaxRequestBytes.test.ts | 6 +- src/config/bridgeMaxRequestBytes.ts | 6 - src/contracts/analysisDetail.test.ts | 4 +- src/contracts/analysisViewToolContracts.ts | 6 +- src/contracts/android/androidToolContracts.ts | 2 +- src/contracts/apiContractToolContracts.ts | 2 +- .../apkSignature/apkSignatureToolContracts.ts | 2 +- src/contracts/applicationToolContracts.ts | 2 +- src/contracts/browserScenarioToolContracts.ts | 2 +- src/contracts/browserToolContracts.ts | 2 +- .../desktopApplicationToolContracts.ts | 2 +- src/contracts/erlang/beamToolContracts.ts | 2 +- src/contracts/evm/evmToolContracts.ts | 2 +- src/contracts/flutter/flutterToolContracts.ts | 2 +- src/contracts/fridaToolContracts.ts | 2 +- src/contracts/gdbToolContracts.ts | 2 +- src/contracts/go/goToolContracts.ts | 2 +- src/contracts/gpu/gpuToolContracts.ts | 2 +- src/contracts/il2cpp/il2cppToolContracts.ts | 2 +- .../javascript/electronToolContracts.ts | 2 +- src/contracts/model/modelToolContracts.ts | 2 +- .../native/binaryDiagnosticsToolContracts.ts | 2 +- src/contracts/pcap/pcapToolContracts.ts | 2 +- src/contracts/playwrightTraceToolContracts.ts | 2 +- .../python/pythonPycToolContracts.ts | 2 +- .../reverseEngineeringToolContracts.ts | 2 +- src/contracts/rust/rustToolContracts.ts | 2 +- src/contracts/toolOutputSchemaGroups.ts | 2 +- src/contracts/wasm/wasmToolContracts.ts | 2 +- .../webNetworkCaptureToolContracts.ts | 2 +- src/contracts/webScriptToolContracts.ts | 2 +- src/cutter/CutterBridgeClient.ts | 2 +- src/desktop/BuildComputerEnvironment.ts | 3 - src/desktop/DesktopBuildDoctorFacts.ts | 2 +- src/desktop/DesktopBuildSetupHost.ts | 2 +- src/domain/analysisView/analysisView.test.ts | 10 +- src/domain/analysisView/analysisView.ts | 85 +--- .../analysisView/analysisViewParent.test.ts | 2 +- .../analysisView/analysisViewRequest.ts | 4 +- src/domain/analysisView/analysisViewResult.ts | 19 +- .../analysisView/androidPackageView.test.ts | 6 +- src/domain/analysisView/androidPackageView.ts | 7 - .../analysisView/androidSymbolView.test.ts | 6 +- .../artifactAvailabilityView.test.ts | 6 +- .../artifactInventorySummaryProjection.ts | 6 +- .../artifactInventoryView.test.ts | 2 +- .../environmentCoverageView.test.ts | 8 +- .../environmentManifestView.test.ts | 6 +- .../analysisView/environmentManifestView.ts | 4 - .../analysisView/environmentStateView.test.ts | 6 +- src/domain/analysisView/goBinaryView.test.ts | 6 +- .../analysisView/gpuCodeObjectsView.test.ts | 2 +- .../analysisView/interfaceBuilderView.test.ts | 8 +- .../analysisView/javascriptRoutesView.test.ts | 6 +- src/domain/analysisView/machoView.test.ts | 8 +- .../analysisView/managedMemberView.test.ts | 2 +- .../analysisView/nativeInventoryView.test.ts | 6 +- .../analysisView/pcapCaptureView.test.ts | 6 +- .../procedurePseudocodeView.test.ts | 6 +- .../analysisView/rustBinaryView.test.ts | 6 +- src/domain/analysisView/webBundleView.test.ts | 6 +- .../analysisView/webNetworkApiSurfaceView.ts | 4 +- src/domain/desktop/appBuild.ts | 2 +- src/domain/desktop/appExport.ts | 1 - .../javascript/javascriptAstFingerprint.ts | 2 +- .../javascriptBundledRequireOrigin.ts | 6 +- .../javascriptCommonJsExportReceivers.ts | 2 +- .../javascript/javascriptNetworkRequests.ts | 2 +- src/domain/javascript/javascriptRouteScan.ts | 2 +- .../javascript/javascriptRpcEndpoints.ts | 2 - .../javascriptSemanticAmbientWrites.ts | 11 +- .../javascript/javascriptSemanticAnalysis.ts | 3 +- .../javascriptSemanticAsyncEffects.ts | 6 +- .../javascriptSemanticDataEffects.ts | 2 +- .../javascriptSemanticGetterResults.ts | 2 +- .../javascriptSemanticIterationSources.ts | 5 - .../javascriptSemanticMapOperations.ts | 2 +- .../javascriptSemanticMemberMutations.ts | 4 +- .../javascriptSemanticObjectReferences.ts | 2 - .../javascriptSemanticProjectedSources.ts | 2 +- .../javascript/javascriptSemanticPromises.ts | 2 +- .../javascriptSemanticRequireOrigin.ts | 7 +- .../javascript/javascriptSemanticResources.ts | 2 +- .../javascriptSemanticTypedArrays.ts | 2 +- .../javascriptStaticAnalysisCalls.ts | 2 +- .../javascriptStaticAnalysisState.ts | 2 +- .../javascriptStaticPathIntrinsics.ts | 2 +- src/domain/referenceSourceRequireBindings.ts | 2 +- src/domain/webBundleAnalyzerInspection.ts | 2 +- src/domain/webSourceLocation.ts | 1 - src/domain/windows/ntfsMft.ts | 1 - src/erlang/BeamProvider.ts | 45 +- src/firmware/devicetree/DevicetreeProvider.ts | 45 +- src/go/GoBinaryProvider.ts | 34 +- src/hopper/BridgeLauncher.ts | 4 +- src/hopper/HopperSessionValues.test.ts | 2 +- src/hopper/HopperSessionValues.ts | 2 +- src/il2cpp/Il2cppMetadataProvider.ts | 33 +- .../analysis/JavaScriptAnalysisTransfer.ts | 1 - .../analysis/JavaScriptAnalysisWorker.ts | 2 +- .../sourceMaps/SourceMapCommand.test.ts | 6 +- src/javascript/sourceMaps/SourceMapCommand.ts | 2 +- src/native/apple/AppleIpsProvider.ts | 37 +- src/native/elf/ElfNotesProvider.ts | 45 +- src/native/elf/ElfUnwindRangesProvider.ts | 43 +- src/native/elf/KernelBuildConfigProvider.ts | 43 +- src/native/elf/KernelModuleProvider.ts | 45 +- src/native/exports/NativeExportsProvider.ts | 37 +- src/native/exports/NativeExportsView.test.ts | 6 +- .../hardening/BinaryHardeningProvider.ts | 37 +- .../NativeInitializersProvider.ts | 37 +- src/native/pe/PeBinaryFailure.ts | 8 +- src/native/pe/PeCertificatesProvider.ts | 45 +- src/native/pe/PeResourcesProvider.ts | 43 +- src/native/pwntools/PwntoolsDecoder.ts | 20 +- src/native/windows/WindowsShortcutProvider.ts | 29 +- src/python/PythonPycProvider.ts | 43 +- src/rust/RustBinaryProvider.ts | 8 +- src/server/contractToolHandler.ts | 2 +- .../registerApplicationTools/helpers.ts | 2 +- src/server/toolResult.ts | 2 +- src/sqlite/SqliteDatabaseFailures.ts | 14 +- src/v8/V8CodeCacheProvider.ts | 45 +- .../acceptance/analysisViewCallerPath.test.ts | 2 +- .../apkSignatureLimitsCliMcp.test.ts | 2 +- .../apkSignatureViewsCliMcp.test.ts | 2 +- .../applications/apiContract.test.ts | 2 +- .../applications/erlangBeamViews.test.ts | 2 +- .../applications/graphqlContract.test.ts | 2 +- ...riptContentDeduplicatedModulePaths.test.ts | 2 +- .../javascriptExportBindingModuleView.test.ts | 2 +- .../javascriptWindowOptionsCli.test.ts | 2 +- .../applications/nativeExportsViews.test.ts | 2 +- .../applications/playwrightTrace.test.ts | 2 +- .../progressiveAnalysisViews.test.ts | 2 +- .../applications/pythonPycViews.test.ts | 2 +- .../staticObservedEndpoints.test.ts | 2 +- .../artifactComparisonEspImage.test.ts | 2 +- ...rtifactComparisonHistoricalSummary.test.ts | 6 +- tests/acceptance/dotnetBundleJourney.test.ts | 2 +- tests/acceptance/godotPck.test.ts | 2 +- tests/acceptance/gpuCodeObjectsCli.test.ts | 2 +- tests/acceptance/il2cppMetadataCli.test.ts | 2 +- tests/acceptance/inesContainer.test.ts | 2 +- tests/acceptance/javascriptRoutesCli.test.ts | 2 +- .../acceptance/managedMemberRecovery.test.ts | 2 +- tests/acceptance/modelArtifactCli.test.ts | 2 +- tests/acceptance/rustDependencyViews.test.ts | 2 +- .../acceptance/tools/protobufCapture.test.ts | 2 +- .../tools/protobufDescriptorFiles.test.ts | 2 +- .../tools/protobufDescriptorIdentity.test.ts | 2 +- tests/acceptance/tools/qlogCapture.test.ts | 2 +- .../unavailableNativeCollectionViews.test.ts | 2 +- .../browser/webSourceMapHttpStatus.test.ts | 2 +- .../cli/electronWrappedCallPaths.test.ts | 2 +- tests/boundary/cli/fridaStatusViewCli.test.ts | 2 +- .../cli/managedMemberSummaryCli.test.ts | 2 +- .../boundary/filesystem/ciChangeScope.test.ts | 2 +- .../hermesClientConfiguration.test.ts | 252 ++++++++++- .../kimiClientConfiguration.test.ts | 19 +- .../webSourceLocationArtifacts.test.ts | 2 +- .../mcp/analysisSummaryDetailMcp.test.ts | 2 +- tests/boundary/mcp/analysisViewMcp.test.ts | 2 +- .../mcp/androidPackageSummaryMcp.test.ts | 2 +- .../mcp/artifactInspectionCoverageMcp.test.ts | 2 +- .../mcp/artifactMcp.inlineInventory.test.ts | 2 +- .../mcp/batchDecompileViewsMcp.test.ts | 2 +- tests/boundary/mcp/bunStandaloneMcp.test.ts | 2 +- tests/boundary/mcp/dartAotSummaryMcp.test.ts | 2 +- tests/boundary/mcp/deferredRuntime.test.ts | 2 +- tests/boundary/mcp/denoStandaloneMcp.test.ts | 2 +- .../mcp/desktopApplicationMcp.test.ts | 2 +- tests/boundary/mcp/espImageMcp.test.ts | 2 +- tests/boundary/mcp/evmInterfaceMcp.test.ts | 2 +- .../boundary/mcp/featureTraceViewsMcp.test.ts | 2 +- tests/boundary/mcp/fitImageMcp.test.ts | 2 +- tests/boundary/mcp/fridaMcp.test.ts | 2 +- tests/boundary/mcp/ghidraNativeApiMcp.test.ts | 2 +- tests/boundary/mcp/goBinaryViews.test.ts | 2 +- tests/boundary/mcp/godotPckMcp.test.ts | 2 +- tests/boundary/mcp/hexRecordsMcp.test.ts | 2 +- .../mcp/javascriptFactsViewMcp.test.ts | 2 +- .../javascriptNativeHandoffIdentity.test.ts | 2 +- .../mcp/managedArtifactSummaryMcp.test.ts | 2 +- .../mcp/managedMemberSummaryMcp.test.ts | 2 +- .../mcp/nativeFeatureTraceViews.test.ts | 2 +- .../nativeInstructionReferencesView.test.ts | 6 +- .../mcp/nativeInventoryViewsMcp.test.ts | 2 +- .../mcp/nativeLoadImageViewsMcp.test.ts | 2 +- .../mcp/nativeObservationViewsMcp.test.ts | 6 +- .../mcp/nativeProcedureReferenceViews.test.ts | 2 +- tests/boundary/mcp/nodeSeaMcp.test.ts | 2 +- .../mcp/pyinstallerArtifactMcp.test.ts | 2 +- .../mcp/sourceToBundleViewsMcp.test.ts | 2 +- tests/boundary/mcp/wasmArtifactMcp.test.ts | 2 +- tests/boundary/mcp/webBundleViewsMcp.test.ts | 2 +- .../process/androidMappingMcp.test.ts | 2 +- .../process/hopperDeadlineCleanup.test.ts | 81 ++++ .../process/http3VerifierNodeTests.test.ts | 31 ++ .../javascriptVerifierBoundedEvidence.test.ts | 8 +- .../process/verifierArtifactScope.test.ts | 107 +++++ .../hopper/hopperVerificationCleanup.test.ts | 59 ++- tests/evaluation/importCycleGuard.test.ts | 31 ++ tests/evaluation/moduleBoundaries.test.ts | 90 +++- .../evaluation/noBabelVisitorKeysRule.test.ts | 84 ++++ tests/evaluation/noLocaleCompareRule.test.ts | 65 +++ tests/evaluation/numericLimitCheck.test.ts | 28 +- .../packetWorkflowSemantics.test.ts | 32 -- tests/evaluation/sazWorkflowSemantics.test.ts | 49 --- .../signalWorkflowSemantics.test.ts | 46 -- tests/fixtures/environmentStateWrites.ts | 6 +- tests/fixtures/importCycles.ts | 20 +- tests/support/analysis-efficiency/README.md | 3 +- tests/support/analysis-efficiency/compare.mjs | 20 +- .../analysis-efficiency/native-entry.mjs | 39 -- .../analysis-efficiency/packet-entry.mjs | 314 -------------- .../analysis-efficiency/packet-oracle.py | 180 -------- .../support/analysis-efficiency/saz-entry.mjs | 326 -------------- .../support/analysis-efficiency/saz-oracle.py | 121 ------ .../analysis-efficiency/signal-entry.mjs | 264 ------------ .../analysis-efficiency/signal-oracle.py | 211 --------- .../analysis-efficiency/test_packet_oracle.py | 67 --- .../analysis-efficiency/test_saz_oracle.py | 48 --- .../analysis-efficiency/test_signal_oracle.py | 149 ------- 316 files changed, 2874 insertions(+), 3510 deletions(-) create mode 100644 scripts/lib/hopper-deadline-cleanup.d.mts create mode 100644 scripts/lib/hopper-deadline-cleanup.mjs delete mode 100644 scripts/lib/module-boundaries.test.mjs create mode 100644 scripts/lib/verifier-artifact-scope.d.mts create mode 100644 scripts/lib/verifier-artifact-scope.mjs create mode 100644 scripts/lint/rules/no-babel-visitor-keys.d.mts create mode 100644 scripts/lint/rules/no-babel-visitor-keys.mjs create mode 100644 scripts/lint/rules/no-locale-compare.d.mts create mode 100644 scripts/lint/rules/no-locale-compare.mjs create mode 100644 tests/boundary/process/hopperDeadlineCleanup.test.ts create mode 100644 tests/boundary/process/http3VerifierNodeTests.test.ts create mode 100644 tests/boundary/process/verifierArtifactScope.test.ts create mode 100644 tests/evaluation/importCycleGuard.test.ts create mode 100644 tests/evaluation/noBabelVisitorKeysRule.test.ts create mode 100644 tests/evaluation/noLocaleCompareRule.test.ts delete mode 100644 tests/evaluation/packetWorkflowSemantics.test.ts delete mode 100644 tests/evaluation/sazWorkflowSemantics.test.ts delete mode 100644 tests/evaluation/signalWorkflowSemantics.test.ts delete mode 100644 tests/support/analysis-efficiency/packet-entry.mjs delete mode 100644 tests/support/analysis-efficiency/packet-oracle.py delete mode 100644 tests/support/analysis-efficiency/saz-entry.mjs delete mode 100644 tests/support/analysis-efficiency/saz-oracle.py delete mode 100644 tests/support/analysis-efficiency/signal-entry.mjs delete mode 100644 tests/support/analysis-efficiency/signal-oracle.py delete mode 100644 tests/support/analysis-efficiency/test_packet_oracle.py delete mode 100644 tests/support/analysis-efficiency/test_saz_oracle.py delete mode 100644 tests/support/analysis-efficiency/test_signal_oracle.py diff --git a/.oxlintrc.json b/.oxlintrc.json index db7907b215..33c616f4d7 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -63,6 +63,18 @@ "rea/no-shared-module-mock": "error" }, "overrides": [ + { + "files": ["src/domain/**/*.ts"], + "rules": { "rea/no-locale-compare": "error" } + }, + { + "files": [ + "src/domain/**/*.test.ts", + "src/domain/**/*.spec.ts", + "src/domain/**/*.fixture.ts" + ], + "rules": { "rea/no-locale-compare": "off" } + }, { "files": [ "scripts/electron-active-hook*.cjs", @@ -76,12 +88,19 @@ { "files": ["src/**/*.ts"], "rules": { + "rea/no-babel-visitor-keys": "error", "typescript/no-misused-promises": "error" } }, { - "files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"], + "files": [ + "tests/**/*.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.fixture.ts" + ], "rules": { + "rea/no-babel-visitor-keys": "off", "max-lines": "off", "max-lines-per-function": [ "error", @@ -94,6 +113,10 @@ "typescript/no-misused-promises": "warn" } }, + { + "files": ["src/domain/javascript/javascriptSemanticTraversal.ts"], + "rules": { "rea/no-babel-visitor-keys": "off" } + }, { "files": ["src/application/**/*.test.ts"], "rules": { diff --git a/docs/http3-capture-lab.md b/docs/http3-capture-lab.md index 77891f19bd..a672915fa7 100644 --- a/docs/http3-capture-lab.md +++ b/docs/http3-capture-lab.md @@ -174,7 +174,9 @@ Official references: [mitmproxy protocol support](https://github.com/mitmproxy/m ## Verifier regressions -After building REA, run these inside the same isolated guest: +The built process-boundary test suite runs the Node verifier regressions on +POSIX hosts. To run them directly after building REA, and to run the Python +laboratory regressions with its pinned dependencies, use the isolated guest: ```sh node --test scripts/lib/http3-verifier-lifecycle.test.mjs scripts/lib/http3-capture-assertions.test.mjs diff --git a/scripts/check-dependency-install.mjs b/scripts/check-dependency-install.mjs index dcdbd64dd6..800907878a 100644 --- a/scripts/check-dependency-install.mjs +++ b/scripts/check-dependency-install.mjs @@ -14,7 +14,7 @@ const directDependencies = (rootPackage) => ({ const installedPackageKey = (name) => `node_modules/${name}`; /** Compare root direct dependencies with npm's hidden installed lockfile. */ -export const dependencyInstallProblems = (rootLock, installedLock) => { +const dependencyInstallProblems = (rootLock, installedLock) => { const rootPackage = rootLock.packages?.[""]; if (rootPackage === undefined) return ["package-lock.json does not contain a root package entry"]; diff --git a/scripts/ci/cache-retention.d.mts b/scripts/ci/cache-retention.d.mts index d5d02c19c8..12c7d67172 100644 --- a/scripts/ci/cache-retention.d.mts +++ b/scripts/ci/cache-retention.d.mts @@ -26,8 +26,6 @@ export type CacheRequest = ( endpoint: string, ) => Promise; -export function assertRetentionPrefix(prefix: string): void; - export function selectPrefixDeletions( caches: readonly unknown[], family?: RetentionFamily, diff --git a/scripts/ci/cache-retention.mjs b/scripts/ci/cache-retention.mjs index dce987bd86..24abf4fdae 100644 --- a/scripts/ci/cache-retention.mjs +++ b/scripts/ci/cache-retention.mjs @@ -22,7 +22,7 @@ const PREFIX_PATTERNS = [ ]; /** Reject prefixes that could match npm downloads or another platform. */ -export function assertRetentionPrefix(prefix) { +function assertRetentionPrefix(prefix) { if ( typeof prefix !== "string" || !PREFIX_PATTERNS.some((pattern) => pattern.test(prefix)) diff --git a/scripts/ci/failure-report.d.mts b/scripts/ci/failure-report.d.mts index 5d24ae8ace..c8170a9c82 100644 --- a/scripts/ci/failure-report.d.mts +++ b/scripts/ci/failure-report.d.mts @@ -99,9 +99,3 @@ export function createGitHubApi(options?: { budget: { requests: number; startedAt: number; limitMs: number }, ) => Promise; }): FailureReportApi; - -export function writeFailureReport( - report: CiFailureReport, - outputDirectory?: string, - summaryPath?: string, -): Promise<{ jsonPath: string; markdownPath: string }>; diff --git a/scripts/ci/failure-report.mjs b/scripts/ci/failure-report.mjs index 01dcc0d700..09d2977d28 100644 --- a/scripts/ci/failure-report.mjs +++ b/scripts/ci/failure-report.mjs @@ -467,7 +467,7 @@ function errorCode(error) { return error instanceof Error ? error.message.slice(0, 160) : "unknown_error"; } -export async function writeFailureReport( +async function writeFailureReport( report, outputDirectory = OUTPUT_DIRECTORY, summaryPath, diff --git a/scripts/ci/pr-ci-health.d.mts b/scripts/ci/pr-ci-health.d.mts index 0db2454bdd..b00a74aeba 100644 --- a/scripts/ci/pr-ci-health.d.mts +++ b/scripts/ci/pr-ci-health.d.mts @@ -91,5 +91,4 @@ export function writePrCiHealthReport( outputDirectory?: string, ): Promise<{ jsonPath: string; markdownPath: string }>; -export function renderMarkdownSummary(report: PrCiHealthReport): string; import type { GitHubApiRequest } from "./github-api.mjs"; diff --git a/scripts/ci/pr-ci-health.mjs b/scripts/ci/pr-ci-health.mjs index d312dcecd9..a982e792f5 100644 --- a/scripts/ci/pr-ci-health.mjs +++ b/scripts/ci/pr-ci-health.mjs @@ -656,7 +656,7 @@ export async function writePrCiHealthReport( return { jsonPath, markdownPath }; } -export function renderMarkdownSummary(report) { +function renderMarkdownSummary(report) { const rows = report.pullRequests.map( (pull) => `| #${String(pull.number ?? "?")} | ${escapeCell(pull.head.repository ?? "unknown")} | ${escapeCell(pull.head.ref ?? "unknown")} | \`${pull.head.sha ?? "unknown"}\` | ${pull.ci.category} | ${escapeCell(pull.ci.reason)} |`, diff --git a/scripts/ci/production-reachability-report.d.mts b/scripts/ci/production-reachability-report.d.mts index 3970ca390f..523812eada 100644 --- a/scripts/ci/production-reachability-report.d.mts +++ b/scripts/ci/production-reachability-report.d.mts @@ -28,6 +28,3 @@ export interface ProductionReachabilityReport { export function parseProductionReachabilityReport( text: string, ): ProductionReachabilityReport; -export function recordProductionReachabilityReport( - root?: string, -): Promise; diff --git a/scripts/ci/production-reachability-report.mjs b/scripts/ci/production-reachability-report.mjs index 8758e6a126..e7aae89bfc 100644 --- a/scripts/ci/production-reachability-report.mjs +++ b/scripts/ci/production-reachability-report.mjs @@ -34,9 +34,7 @@ export const parseProductionReachabilityReport = (text) => reportSchema.parse(JSON.parse(text)); /** Run production reachability without turning candidates into failures. */ -export const recordProductionReachabilityReport = async ( - root = process.cwd(), -) => { +const recordProductionReachabilityReport = async (root = process.cwd()) => { const reportDirectory = join(root, ".cache", "static-checks"); const reportPath = join(reportDirectory, "production-reachability.json"); diff --git a/scripts/fixtures/apple/macos-bundle.mjs b/scripts/fixtures/apple/macos-bundle.mjs index adb1a2c108..de32463616 100644 --- a/scripts/fixtures/apple/macos-bundle.mjs +++ b/scripts/fixtures/apple/macos-bundle.mjs @@ -6,7 +6,7 @@ import { promisify } from "node:util"; const exec = promisify(execFile); /** Commands the macOS bundle fixture needs from Command Line Tools. */ -export const MACOS_BUNDLE_FIXTURE_COMMANDS = [ +const MACOS_BUNDLE_FIXTURE_COMMANDS = [ ["/usr/bin/xcrun", ["--find", "clang"]], ["/usr/bin/xcrun", ["--find", "codesign"]], ["/usr/bin/ditto", ["-h"]], diff --git a/scripts/lib/browser-verifier-assertions.mjs b/scripts/lib/browser-verifier-assertions.mjs index 2ca8360116..f5a7a7d702 100644 --- a/scripts/lib/browser-verifier-assertions.mjs +++ b/scripts/lib/browser-verifier-assertions.mjs @@ -1,4 +1,4 @@ -export function sourceMapSummary(sourceMaps) { +function sourceMapSummary(sourceMaps) { return { status: sourceMaps.status, requested: sourceMaps.requested, diff --git a/scripts/lib/cursor-plugin-bundle.d.mts b/scripts/lib/cursor-plugin-bundle.d.mts index 9c9acceb29..b83b43a5c5 100644 --- a/scripts/lib/cursor-plugin-bundle.d.mts +++ b/scripts/lib/cursor-plugin-bundle.d.mts @@ -1,11 +1,3 @@ -export const CURSOR_PLUGIN_MANIFEST: ".cursor-plugin/plugin.json"; -export const CURSOR_PLUGIN_MCP: "mcp.json"; -export const AUTHORED_SKILL_DIRECTORY: ".agents/skills/reverse-engineer-anything"; -export const CURSOR_PLUGIN_SKILLS: ".cursor-plugin/skills"; -export const CURSOR_PLUGIN_SKILL: "reverse-engineer-anything"; -export const CURSOR_PLUGIN_LOGO: "assets/rea-plugin.png"; -export const CURSOR_PLUGIN_REPOSITORY: "https://github.com/morluto/rea"; - export interface CursorPluginBundleIssue { readonly field: string; } @@ -56,11 +48,6 @@ export interface CursorPluginInspection { readonly issues: readonly CursorPluginBundleIssue[]; } -export function cursorPluginSkillPaths( - directory: string, - prefix?: string, -): Promise; - export function cursorPluginBundleIssues( input: CursorPluginBundleInput, ): readonly CursorPluginBundleIssue[]; diff --git a/scripts/lib/cursor-plugin-bundle.mjs b/scripts/lib/cursor-plugin-bundle.mjs index 6b8359e6dd..5dd7453172 100644 --- a/scripts/lib/cursor-plugin-bundle.mjs +++ b/scripts/lib/cursor-plugin-bundle.mjs @@ -7,14 +7,13 @@ import { ensureGeneratedFile } from "./generated-file.mjs"; // publication (name, release version, repository, skill, and icon). The // Marketplace checkout has no dist/, so the server is the pinned npm command // instead of the bundled launcher. -export const CURSOR_PLUGIN_MANIFEST = ".cursor-plugin/plugin.json"; -export const CURSOR_PLUGIN_MCP = "mcp.json"; -export const AUTHORED_SKILL_DIRECTORY = - ".agents/skills/reverse-engineer-anything"; -export const CURSOR_PLUGIN_SKILLS = ".cursor-plugin/skills"; -export const CURSOR_PLUGIN_SKILL = "reverse-engineer-anything"; -export const CURSOR_PLUGIN_LOGO = "assets/rea-plugin.png"; -export const CURSOR_PLUGIN_REPOSITORY = "https://github.com/morluto/rea"; +const CURSOR_PLUGIN_MANIFEST = ".cursor-plugin/plugin.json"; +const CURSOR_PLUGIN_MCP = "mcp.json"; +const AUTHORED_SKILL_DIRECTORY = ".agents/skills/reverse-engineer-anything"; +const CURSOR_PLUGIN_SKILLS = ".cursor-plugin/skills"; +const CURSOR_PLUGIN_SKILL = "reverse-engineer-anything"; +const CURSOR_PLUGIN_LOGO = "assets/rea-plugin.png"; +const CURSOR_PLUGIN_REPOSITORY = "https://github.com/morluto/rea"; const GENERATE_COMMAND = "node scripts/generate-cursor-plugin-skill.mjs"; const INSTALL_LINK_MARKERS = ["install-mcp", "deeplink/mcp", "mcp-install"]; @@ -27,7 +26,7 @@ const failure = (code, detail) => { const cursorServerArgs = (version) => ["-y", `rea-agents@${version}`, "mcp"]; /** Relative file inventory for one skill directory, in stable order. */ -export const cursorPluginSkillPaths = async (directory, prefix = "") => { +const cursorPluginSkillPaths = async (directory, prefix = "") => { const paths = []; let entries; try { diff --git a/scripts/lib/ghidra-windows-byte-search.mjs b/scripts/lib/ghidra-windows-byte-search.mjs index 25dc5e5e6a..c52b15f275 100644 --- a/scripts/lib/ghidra-windows-byte-search.mjs +++ b/scripts/lib/ghidra-windows-byte-search.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; -export const WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE = Object.freeze({ +const WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE = Object.freeze({ pattern: "a0 ?? c0", mask: "f0 ff f0", startFileOffset: 0x380, diff --git a/scripts/lib/hopper-deadline-cleanup.d.mts b/scripts/lib/hopper-deadline-cleanup.d.mts new file mode 100644 index 0000000000..cf2779d6d3 --- /dev/null +++ b/scripts/lib/hopper-deadline-cleanup.d.mts @@ -0,0 +1,16 @@ +import type { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js"; + +export class HopperDeadlineCleanupFailure extends AggregateError { + readonly _tag: "HopperDeadlineCleanupFailure"; + readonly cleanupOwner: HopperDeadlineCleanup; + constructor(owner: HopperDeadlineCleanup, failures: unknown[]); +} + +export class HopperDeadlineCleanup { + readonly workspace: string; + constructor(workspace: string, profile: ArtifactResourceScope); + setClient(client: { + close(): Promise<{ ok: true } | { ok: false; error: unknown }>; + }): void; + close(): Promise; +} diff --git a/scripts/lib/hopper-deadline-cleanup.mjs b/scripts/lib/hopper-deadline-cleanup.mjs new file mode 100644 index 0000000000..b6ee0dd723 --- /dev/null +++ b/scripts/lib/hopper-deadline-cleanup.mjs @@ -0,0 +1,71 @@ +import { rm } from "node:fs/promises"; + +export class HopperDeadlineCleanupFailure extends AggregateError { + constructor(owner, failures) { + super( + failures, + `Hopper cleanup incomplete; workspace may remain at ${owner.workspace}`, + ); + this._tag = "HopperDeadlineCleanupFailure"; + this.cleanupOwner = owner; + } +} + +/** Own the client, profile scope, and workspace until all cleanup completes. */ +export class HopperDeadlineCleanup { + #client; + #profile; + #pending; + #removed = false; + + constructor(workspace, profile) { + this.workspace = workspace; + this.#profile = profile; + } + + setClient(client) { + this.#client = client; + } + + close() { + if (this.#pending !== undefined) return this.#pending; + const attempt = this.#close(); + const pending = attempt.finally(() => { + if (this.#pending === pending) this.#pending = undefined; + }); + this.#pending = pending; + return pending; + } + + async #close() { + if (this.#removed) return; + const failures = []; + if (this.#client !== undefined) { + try { + const result = await this.#client.close(); + if (!result.ok) throw result.error; + this.#client = undefined; + } catch (cause) { + failures.push(cause); + } + } + if (this.#profile !== undefined) { + try { + await this.#profile.close(); + this.#profile = undefined; + } catch (cause) { + failures.push(cause); + } + } + if (failures.length === 0) { + try { + await rm(this.workspace, { recursive: true, force: true }); + this.#removed = true; + return; + } catch (cause) { + failures.push(cause); + } + } + throw new HopperDeadlineCleanupFailure(this, failures); + } +} diff --git a/scripts/lib/http3-capture-assertions.mjs b/scripts/lib/http3-capture-assertions.mjs index ca08d32ccf..ac7c7862a8 100644 --- a/scripts/lib/http3-capture-assertions.mjs +++ b/scripts/lib/http3-capture-assertions.mjs @@ -64,7 +64,7 @@ function assertHeaders(record, side, headers) { } } -export function assertEndpointHeaders(actual, expected) { +function assertEndpointHeaders(actual, expected) { // The proxy rebuilds pseudo fields from its HTTP state. Compare their values // by name while retaining the exact order and duplicates of regular headers. const partition = (headers) => { diff --git a/scripts/lib/javascript-application-analysis.mjs b/scripts/lib/javascript-application-analysis.mjs index e9b4c88226..2d35f668d8 100644 --- a/scripts/lib/javascript-application-analysis.mjs +++ b/scripts/lib/javascript-application-analysis.mjs @@ -1,28 +1,10 @@ import { analyzeJavaScriptApplication } from "../../dist/application/javascript/JavaScriptApplicationService.js"; -import { ArtifactResourceScope } from "../../dist/artifacts/ArtifactResourceScope.js"; +import { withVerifierArtifactScope } from "./verifier-artifact-scope.mjs"; /** Analyze one JavaScript application and close its caller-owned resources. */ -export const analyzeJavaScriptApplicationWithOwnedScope = async (input) => { - const scope = new ArtifactResourceScope(); - let outcome; - try { +export const analyzeJavaScriptApplicationWithOwnedScope = (input) => + withVerifierArtifactScope(async (scope) => { const result = await analyzeJavaScriptApplication(input, scope); - outcome = result.ok - ? { kind: "completed", value: result.value } - : { kind: "failed", cause: result.error }; - } catch (cause) { - outcome = { kind: "failed", cause }; - } - try { - await scope.close(); - } catch (cleanupCause) { - if (outcome.kind === "failed") - throw new AggregateError( - [outcome.cause, cleanupCause], - "JavaScript analysis and artifact cleanup both failed", - ); - throw cleanupCause; - } - if (outcome.kind === "failed") throw outcome.cause; - return outcome.value; -}; + if (!result.ok) throw result.error; + return result.value; + }); diff --git a/scripts/lib/mcp-verifier-results.mjs b/scripts/lib/mcp-verifier-results.mjs index efef3efd65..75b1163e4e 100644 --- a/scripts/lib/mcp-verifier-results.mjs +++ b/scripts/lib/mcp-verifier-results.mjs @@ -78,7 +78,7 @@ export const expandMcpSessionStatus = (status) => ({ }); /** Expand provider capability groups while retaining candidate identity/state. */ -export const expandProviderCandidates = (candidates) => +const expandProviderCandidates = (candidates) => candidates.map((candidate) => ({ ...candidate, capabilities: expandProviderCapabilities( diff --git a/scripts/lib/module-boundaries.test.mjs b/scripts/lib/module-boundaries.test.mjs deleted file mode 100644 index 3177637570..0000000000 --- a/scripts/lib/module-boundaries.test.mjs +++ /dev/null @@ -1,176 +0,0 @@ -import assert from "node:assert/strict"; -import { spawnSync } from "node:child_process"; -import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import { test } from "node:test"; - -import { inspectModuleBoundaries } from "./module-boundaries.mjs"; - -const fixture = async () => { - const root = await mkdtemp(join(tmpdir(), "rea-module-boundaries-")); - await mkdir(join(root, "src/domain"), { recursive: true }); - await mkdir(join(root, "src/composition"), { recursive: true }); - await writeFile( - join(root, "src/composition/worker.ts"), - "export default 1;\n", - ); - await writeFile( - join(root, "package.json"), - JSON.stringify({ imports: { "#worker": "./dist/composition/worker.js" } }), - ); - return root; -}; - -await test("reports a pure-layer composition import written relatively", async () => { - const root = await fixture(); - try { - const violations = inspectModuleBoundaries( - "src/domain/probe.ts", - 'import worker from "../composition/worker.js";\nexport default worker;\n', - root, - ); - assert.deepEqual( - violations.map(({ boundary, target }) => ({ boundary, target })), - [{ boundary: "pure-layer", target: "src/composition/worker.js" }], - ); - } finally { - await rm(root, { recursive: true, force: true }); - } -}); - -await test("reports a pure-layer composition import written through the package map", async () => { - const root = await fixture(); - try { - const violations = inspectModuleBoundaries( - "src/domain/probe.ts", - 'import worker from "#worker";\nexport default worker;\n', - root, - ); - assert.deepEqual( - violations.map(({ boundary, target }) => ({ boundary, target })), - [{ boundary: "pure-layer", target: "src/composition/worker.ts" }], - ); - } finally { - await rm(root, { recursive: true, force: true }); - } -}); - -for (const [name, specifier, imports] of [ - [ - "conditional", - "#worker", - { - "#worker": { - import: "./dist/composition/worker.js", - default: "./dist/composition/other.js", - }, - }, - ], - ["array", "#worker", { "#worker": ["./dist/composition/worker.js"] }], - ["pattern", "#workers/worker", { "#workers/*": "./dist/composition/*.js" }], -]) { - await test(`refuses an unsupported valid ${name} package map rather than skipping its edge`, async () => { - const root = await fixture(); - try { - await writeFile(join(root, "package.json"), JSON.stringify({ imports })); - const file = "src/domain/probe.ts"; - await writeFile( - join(root, file), - `import ${JSON.stringify(specifier)};\n`, - ); - // Resolve only: no worker module or authored target source is executed. - const resolved = spawnSync( - process.execPath, - [ - "--experimental-import-meta-resolve", - "--input-type=module", - "-e", - "console.log(import.meta.resolve(process.argv[1], process.argv[2]))", - specifier, - pathToFileURL(join(root, file)).href, - ], - { encoding: "utf8" }, - ); - if (resolved.error !== undefined) throw resolved.error; - assert.equal(resolved.status, 0, resolved.stderr); - assert.equal( - resolved.stdout.trim(), - pathToFileURL(join(root, "dist/composition/worker.js")).href, - ); - const checked = spawnSync( - process.execPath, - [ - fileURLToPath( - new URL("../verify-module-boundaries.mjs", import.meta.url), - ), - file, - ], - { cwd: root, encoding: "utf8" }, - ); - if (checked.error !== undefined) throw checked.error; - assert.equal(checked.status, 1, checked.stdout); - assert.match(checked.stderr, /Could not inspect src\/domain\/probe.ts/u); - assert.match(checked.stderr, /Cannot resolve package import/u); - assert.match(checked.stderr, /relative source import/u); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); -} - -for (const target of [ - "./dist/composition/worker.js", - "./dist/%63omposition/worker.js", - "./%64ist/composition/worker.js", -]) { - await test(`checks the URL-normalized source owner of ${target}`, async () => { - const root = await fixture(); - try { - await writeFile( - join(root, "package.json"), - JSON.stringify({ imports: { "#worker": target } }), - ); - await mkdir(join(root, "src/application"), { recursive: true }); - const file = "src/application/probe.ts"; - await writeFile(join(root, file), 'import "#worker";\n'); - const resolved = spawnSync( - process.execPath, - [ - "--experimental-import-meta-resolve", - "--input-type=module", - "-e", - "console.log(import.meta.resolve(process.argv[1], process.argv[2]))", - "#worker", - pathToFileURL(join(root, file)).href, - ], - { encoding: "utf8" }, - ); - if (resolved.error !== undefined) throw resolved.error; - assert.equal(resolved.status, 0, resolved.stderr); - assert.equal( - fileURLToPath(resolved.stdout.trim()), - join(root, "dist/composition/worker.js"), - ); - const checked = spawnSync( - process.execPath, - [ - fileURLToPath( - new URL("../verify-module-boundaries.mjs", import.meta.url), - ), - file, - ], - { cwd: root, encoding: "utf8" }, - ); - if (checked.error !== undefined) throw checked.error; - assert.equal(checked.status, 1, checked.stdout); - assert.match( - checked.stderr, - /application-composition: #worker resolves to src\/composition\/worker.ts/u, - ); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); -} diff --git a/scripts/lib/owned-verifier-terminal.d.mts b/scripts/lib/owned-verifier-terminal.d.mts index 6febc16766..de65ae5b78 100644 --- a/scripts/lib/owned-verifier-terminal.d.mts +++ b/scripts/lib/owned-verifier-terminal.d.mts @@ -1,8 +1,5 @@ export { runOwnedVerifierTerminal } from "./verifier-terminal-owner.mjs"; export type { VerifierTerminalOptions, - VerifierTerminalDependencies, VerifierTerminalResult, - VerifierTerminalFailure, - VerifierTerminalCleanupFailure, } from "./verifier-terminal-owner.mjs"; diff --git a/scripts/lib/real-hopper-cleanup.d.mts b/scripts/lib/real-hopper-cleanup.d.mts index 8ccc9e3187..095fc052d7 100644 --- a/scripts/lib/real-hopper-cleanup.d.mts +++ b/scripts/lib/real-hopper-cleanup.d.mts @@ -1,6 +1,10 @@ -/** Observe concrete Hopper runtime resources for a before/after cleanup check. */ +/** Observe possible Hopper resources, including entries with unresolved ownership. */ export function snapshotHopperRuntime( parent: string, targetLeaseDirectory: string, - ownedProcessIds?: ReadonlySet, -): Promise>; + expectedRunId?: string, +): Promise<{ + readonly paths: Set; + readonly confirmedSessions: Set; + readonly unresolvedSessions: Set; +}>; diff --git a/scripts/lib/real-hopper-cleanup.mjs b/scripts/lib/real-hopper-cleanup.mjs index 85d0a8f336..decbc3fd26 100644 --- a/scripts/lib/real-hopper-cleanup.mjs +++ b/scripts/lib/real-hopper-cleanup.mjs @@ -1,35 +1,38 @@ import { lstat, readFile, readdir } from "node:fs/promises"; import { join } from "node:path"; -/** Observe session paths and target leases without treating their shared parent as a session. */ +/** Observe possible sessions and target leases without discarding uncertainty. */ export async function snapshotHopperRuntime( parent, targetLeaseDirectory, - ownedProcessIds, + expectedRunId, ) { - const resources = new Set(); + const paths = new Set(); + const confirmedSessions = new Set(); + const unresolvedSessions = new Set(); for (const entry of await readdir(parent, { withFileTypes: true })) { const path = join(parent, entry.name); if (!entry.name.startsWith("rea-") || path === targetLeaseDirectory) continue; - if ( - ownedProcessIds !== undefined && - (!entry.isDirectory() || - !(await belongsToOwnedProcess(path, ownedProcessIds))) - ) - continue; - resources.add(path); + const confirmed = + typeof expectedRunId === "string" && + expectedRunId.length > 0 && + entry.isDirectory() && + (await hasRunIdentity(path, expectedRunId)); + (confirmed ? confirmedSessions : unresolvedSessions).add(path); + paths.add(path); } const leaseRoot = await lstat(targetLeaseDirectory).catch(missingIsAbsent); - if (leaseRoot === undefined) return resources; - if (!leaseRoot.isDirectory() || leaseRoot.isSymbolicLink()) { - resources.add(targetLeaseDirectory); - return resources; + if (leaseRoot !== undefined) { + if (!leaseRoot.isDirectory() || leaseRoot.isSymbolicLink()) + paths.add(targetLeaseDirectory); + else { + const leases = await readdir(targetLeaseDirectory).catch(missingIsAbsent); + for (const name of leases ?? []) + paths.add(join(targetLeaseDirectory, name)); + } } - const leases = await readdir(targetLeaseDirectory).catch(missingIsAbsent); - for (const name of leases ?? []) - resources.add(join(targetLeaseDirectory, name)); - return resources; + return { paths, confirmedSessions, unresolvedSessions }; } const missingIsAbsent = (cause) => { @@ -37,16 +40,21 @@ const missingIsAbsent = (cause) => { throw cause; }; -const belongsToOwnedProcess = async (path, ownedProcessIds) => { +const hasRunIdentity = async (path, expectedRunId) => { const encoded = await readFile(join(path, "ownership.json"), "utf8").catch( missingIsAbsent, ); if (encoded === undefined) return false; - const ownership = JSON.parse(encoded); + let ownership; + try { + ownership = JSON.parse(encoded); + } catch (cause) { + if (cause instanceof SyntaxError) return false; + throw cause; + } return ( ownership !== null && typeof ownership === "object" && - Number.isSafeInteger(ownership.parent_pid) && - ownedProcessIds.has(ownership.parent_pid) + ownership.run_id === expectedRunId ); }; diff --git a/scripts/lib/real-hopper-semantic.d.mts b/scripts/lib/real-hopper-semantic.d.mts index 8be63c510a..0c0082dbfe 100644 --- a/scripts/lib/real-hopper-semantic.d.mts +++ b/scripts/lib/real-hopper-semantic.d.mts @@ -1,4 +1,4 @@ -export interface FixtureProcedure { +interface FixtureProcedure { readonly address: string; readonly name: string; } @@ -26,13 +26,6 @@ export function requireCurrentDocument( normalizedResult: (value: unknown, operation: string) => unknown, ): Promise; -export function analyzeFixtureProcedure( - client: ToolClient, - options: unknown, - procedure: FixtureProcedure, - normalizedResult: (value: unknown, operation: string) => unknown, -): Promise; - export function verifyRealHopperFixture(options: { readonly client: ToolClient; readonly options: unknown; diff --git a/scripts/lib/real-hopper-semantic.mjs b/scripts/lib/real-hopper-semantic.mjs index d6e7752fb1..f07d0b313d 100644 --- a/scripts/lib/real-hopper-semantic.mjs +++ b/scripts/lib/real-hopper-semantic.mjs @@ -310,7 +310,7 @@ export async function resolveFixtureProcedure( } /** Request the assembly evidence required by the fixture oracle. */ -export const analyzeFixtureProcedure = async ( +const analyzeFixtureProcedure = async ( client, options, procedure, diff --git a/scripts/lib/runtime-admission.d.mts b/scripts/lib/runtime-admission.d.mts index 76d8727d01..02040fe769 100644 --- a/scripts/lib/runtime-admission.d.mts +++ b/scripts/lib/runtime-admission.d.mts @@ -6,6 +6,3 @@ export function unsupportedRuntimeMessage( range: string, version: string, ): string | undefined; - -/** Human-readable release families for an `engines.node` range. */ -export function supportedReleaseProse(range: string): string; diff --git a/scripts/lib/runtime-admission.mjs b/scripts/lib/runtime-admission.mjs index 5ba168383f..aadb9f58ca 100644 --- a/scripts/lib/runtime-admission.mjs +++ b/scripts/lib/runtime-admission.mjs @@ -21,7 +21,7 @@ export const unsupportedRuntimeMessage = (range, version) => { }; /** Human-readable release families for an `engines.node` range. */ -export const supportedReleaseProse = (range) => +const supportedReleaseProse = (range) => `Node.js ${range .split("||") .map((clause) => { diff --git a/scripts/lib/verifier-artifact-scope.d.mts b/scripts/lib/verifier-artifact-scope.d.mts new file mode 100644 index 0000000000..fdf76198f2 --- /dev/null +++ b/scripts/lib/verifier-artifact-scope.d.mts @@ -0,0 +1,15 @@ +import type { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js"; + +export class VerifierArtifactCleanupFailure extends AggregateError { + readonly _tag: "VerifierArtifactCleanupFailure"; + readonly cleanupOwner: ArtifactResourceScope; + constructor( + scope: ArtifactResourceScope, + failures: unknown[], + cause: unknown, + ); +} + +export function withVerifierArtifactScope( + operation: (scope: ArtifactResourceScope) => Promise, +): Promise; diff --git a/scripts/lib/verifier-artifact-scope.mjs b/scripts/lib/verifier-artifact-scope.mjs new file mode 100644 index 0000000000..d90976fc38 --- /dev/null +++ b/scripts/lib/verifier-artifact-scope.mjs @@ -0,0 +1,32 @@ +import { ArtifactResourceScope } from "../../dist/artifacts/ArtifactResourceScope.js"; + +/** A failed close retains the scope that can retry all of its owners. */ +export class VerifierArtifactCleanupFailure extends AggregateError { + constructor(scope, failures, cause) { + super(failures, "Verifier cleanup failed", { cause }); + this._tag = "VerifierArtifactCleanupFailure"; + this.cleanupOwner = scope; + } +} + +/** Run one verifier operation and retain its exact scope if close fails. */ +export async function withVerifierArtifactScope(operation) { + const scope = new ArtifactResourceScope(); + let outcome; + try { + outcome = { ok: true, value: await operation(scope) }; + } catch (cause) { + outcome = { ok: false, cause }; + } + try { + await scope.close(); + } catch (cleanupCause) { + throw new VerifierArtifactCleanupFailure( + scope, + outcome.ok ? [cleanupCause] : [outcome.cause, cleanupCause], + outcome.ok ? cleanupCause : outcome.cause, + ); + } + if (!outcome.ok) throw outcome.cause; + return outcome.value; +} diff --git a/scripts/lib/verifier-terminal-owner.mts b/scripts/lib/verifier-terminal-owner.mts index d0a0055c4f..d541c78fb3 100644 --- a/scripts/lib/verifier-terminal-owner.mts +++ b/scripts/lib/verifier-terminal-owner.mts @@ -40,7 +40,7 @@ export interface VerifierTerminalOptions { readonly signal?: AbortSignal; } -export interface VerifierTerminalDependencies { +interface VerifierTerminalDependencies { readonly loadPty?: () => Promise; readonly cleanupProcessGroup?: typeof cleanupOwnedProcessGroup; } @@ -59,7 +59,7 @@ type TerminalFailureReason = | "unsupported_platform"; /** Expected terminal failures retain the decoded output observed at failure. */ -export class VerifierTerminalFailure extends Error { +class VerifierTerminalFailure extends Error { readonly _tag = "VerifierTerminalFailure"; constructor( @@ -74,7 +74,7 @@ export class VerifierTerminalFailure extends Error { } /** The adopted owner remains the exact authority for retrying this release. */ -export class VerifierTerminalCleanupFailure extends Error { +class VerifierTerminalCleanupFailure extends Error { readonly _tag = "VerifierTerminalCleanupFailure"; readonly reason = "cleanup_incomplete"; diff --git a/scripts/lint/index.mjs b/scripts/lint/index.mjs index 66b7f47cd3..72075f0698 100644 --- a/scripts/lint/index.mjs +++ b/scripts/lint/index.mjs @@ -5,6 +5,8 @@ import noEvidenceJsonPreadmission from "./rules/no-evidence-json-preadmission.mj import noFixtureRootCleanup from "./rules/no-fixture-root-cleanup.mjs"; import noPositionalToolContract from "./rules/no-positional-tool-contract.mjs"; import noSharedModuleMock from "./rules/no-shared-module-mock.mjs"; +import noLocaleCompare from "./rules/no-locale-compare.mjs"; +import noBabelVisitorKeys from "./rules/no-babel-visitor-keys.mjs"; export default { meta: { name: "rea" }, @@ -15,5 +17,7 @@ export default { "no-fixture-root-cleanup": noFixtureRootCleanup, "no-positional-tool-contract": noPositionalToolContract, "no-shared-module-mock": noSharedModuleMock, + "no-locale-compare": noLocaleCompare, + "no-babel-visitor-keys": noBabelVisitorKeys, }, }; diff --git a/scripts/lint/rules/no-babel-visitor-keys.d.mts b/scripts/lint/rules/no-babel-visitor-keys.d.mts new file mode 100644 index 0000000000..86d61729e2 --- /dev/null +++ b/scripts/lint/rules/no-babel-visitor-keys.d.mts @@ -0,0 +1,4 @@ +import type { RuleTester } from "oxlint/plugins-dev"; + +declare const rule: Parameters[1]; +export default rule; diff --git a/scripts/lint/rules/no-babel-visitor-keys.mjs b/scripts/lint/rules/no-babel-visitor-keys.mjs new file mode 100644 index 0000000000..588233f4ca --- /dev/null +++ b/scripts/lint/rules/no-babel-visitor-keys.mjs @@ -0,0 +1,73 @@ +import { findBinding } from "../bindings.mjs"; + +const nameOf = (node) => + node.type === "Identifier" + ? node.name + : node.type === "Literal" + ? node.value + : node.type === "TemplateLiteral" && node.expressions.length === 0 + ? node.quasis[0]?.value.cooked + : undefined; + +export default { + meta: { + type: "problem", + docs: { description: "Keep Babel child expansion in its shared owner." }, + schema: [], + messages: { + owner: + "Use childNodes or traverseJavaScriptAst instead of Babel VISITOR_KEYS.", + }, + }, + create(context) { + const source = context.sourceCode; + const isBabelNamespace = (node) => { + if (node?.type !== "Identifier") return false; + const definition = findBinding(source, node)?.defs[0]; + if ( + definition?.type !== "ImportBinding" || + definition.node.type !== "ImportNamespaceSpecifier" + ) + return false; + const declaration = definition.parent ?? definition.node.parent; + return declaration?.source?.value === "@babel/types"; + }; + return { + ImportSpecifier(node) { + if ( + node.parent.source.value === "@babel/types" && + (node.imported.name ?? node.imported.value) === "VISITOR_KEYS" + ) + context.report({ node, messageId: "owner" }); + }, + ExportNamedDeclaration(node) { + if (node.source?.value !== "@babel/types") return; + for (const specifier of node.specifiers) + if ( + specifier.type === "ExportSpecifier" && + nameOf(specifier.local) === "VISITOR_KEYS" + ) + context.report({ node: specifier, messageId: "owner" }); + }, + MemberExpression(node) { + if ( + (!node.computed || node.property.type !== "Identifier") && + nameOf(node.property) === "VISITOR_KEYS" && + isBabelNamespace(node.object) + ) + context.report({ node, messageId: "owner" }); + }, + VariableDeclarator(node) { + if (node.id.type !== "ObjectPattern" || !isBabelNamespace(node.init)) + return; + for (const property of node.id.properties) + if ( + property.type === "Property" && + (!property.computed || property.key.type !== "Identifier") && + nameOf(property.key) === "VISITOR_KEYS" + ) + context.report({ node: property, messageId: "owner" }); + }, + }; + }, +}; diff --git a/scripts/lint/rules/no-locale-compare.d.mts b/scripts/lint/rules/no-locale-compare.d.mts new file mode 100644 index 0000000000..86d61729e2 --- /dev/null +++ b/scripts/lint/rules/no-locale-compare.d.mts @@ -0,0 +1,4 @@ +import type { RuleTester } from "oxlint/plugins-dev"; + +declare const rule: Parameters[1]; +export default rule; diff --git a/scripts/lint/rules/no-locale-compare.mjs b/scripts/lint/rules/no-locale-compare.mjs new file mode 100644 index 0000000000..41c01e67fa --- /dev/null +++ b/scripts/lint/rules/no-locale-compare.mjs @@ -0,0 +1,34 @@ +export default { + meta: { + type: "problem", + docs: { + description: "Keep domain ordering independent of the host locale.", + }, + schema: [], + messages: { + locale: + "Use compareUnicodeCodePoints instead of localeCompare for domain ordering.", + }, + }, + create(context) { + return { + CallExpression(node) { + const member = node.callee; + if (member.type !== "MemberExpression") return; + if ( + (!member.computed && + member.property.type === "Identifier" && + member.property.name === "localeCompare") || + (member.computed && + member.property.type === "Literal" && + member.property.value === "localeCompare") || + (member.computed && + member.property.type === "TemplateLiteral" && + member.property.expressions.length === 0 && + member.property.quasis[0]?.value.cooked === "localeCompare") + ) + context.report({ node, messageId: "locale" }); + }, + }; + }, +}; diff --git a/scripts/verify-package-environment.d.mts b/scripts/verify-package-environment.d.mts index 9f1b3c55fc..0a17e6565c 100644 --- a/scripts/verify-package-environment.d.mts +++ b/scripts/verify-package-environment.d.mts @@ -1,13 +1,3 @@ -export interface PackageHopperEnvironment { - readonly HOPPER_LAUNCHER_PATH: string; - readonly HOPPER_LOADER_ARGS_JSON: string; -} - -export function packageHopperEnvironment( - root: string, - platform?: string, -): PackageHopperEnvironment; - export function verifyPackageEnvironment(input: { readonly root: string; readonly workspace: string; diff --git a/scripts/verify-package-environment.mjs b/scripts/verify-package-environment.mjs index 91834d3fa1..5a0424c19b 100644 --- a/scripts/verify-package-environment.mjs +++ b/scripts/verify-package-environment.mjs @@ -117,7 +117,7 @@ export async function verifyPackageEnvironment({ }; } -export const packageHopperEnvironment = (root, platform = process.platform) => { +const packageHopperEnvironment = (root, platform = process.platform) => { const fakeLauncher = join(root, "tests/fixtures/fakeLauncher.mjs"); if (platform !== "linux") return { diff --git a/scripts/verify-real-ghidra-assertions.mjs b/scripts/verify-real-ghidra-assertions.mjs index c5fed71b0a..445d739569 100644 --- a/scripts/verify-real-ghidra-assertions.mjs +++ b/scripts/verify-real-ghidra-assertions.mjs @@ -414,7 +414,7 @@ export function matchesSymbol(value, expected) { return tail === expected || tail === `_${expected}`; } -export function symbolTail(value) { +function symbolTail(value) { if (typeof value !== "string") return undefined; return value.split("::").at(-1); } diff --git a/scripts/verify-real-ghidra-dol.mjs b/scripts/verify-real-ghidra-dol.mjs index d92d6ea452..bdc7a5444d 100644 --- a/scripts/verify-real-ghidra-dol.mjs +++ b/scripts/verify-real-ghidra-dol.mjs @@ -9,7 +9,7 @@ import { promisify } from "node:util"; import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js"; -import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js"; +import { withVerifierArtifactScope } from "./lib/verifier-artifact-scope.mjs"; import { readGhidraLanguageCatalog } from "../dist/ghidra/GhidraLanguageCatalog.js"; import { parseEvidence } from "../dist/domain/evidence.js"; import { functionDossierSchema } from "../dist/domain/native/functionDossier.js"; @@ -82,16 +82,13 @@ for (const setting of ["GHIDRA_HEADLESS_MAXMEM", "GHIDRA_MAXMEM"]) { * installation catalog reader it uses. User extensions are not in that catalog. */ async function languageInstalled(languageId) { - const scope = new ArtifactResourceScope(); - try { + return withVerifierArtifactScope(async (scope) => { const catalog = await readGhidraLanguageCatalog( installation.installDir, scope, ); return catalog.some((definition) => definition.id === languageId); - } finally { - await scope.close(); - } + }); } /** One MCP session plus CLI check. `languageOverride` is set only for the Gekko pass. */ diff --git a/scripts/verify-real-hopper-callers.mjs b/scripts/verify-real-hopper-callers.mjs index 7407114e6c..3f9cca6414 100644 --- a/scripts/verify-real-hopper-callers.mjs +++ b/scripts/verify-real-hopper-callers.mjs @@ -11,6 +11,10 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import { promisify } from "node:util"; import { artifactMcpCall } from "./lib/artifact-e2e.mjs"; import { snapshotHopperRuntime } from "./lib/real-hopper-cleanup.mjs"; +import { + closeVerifierResources, + withVerifierCleanup, +} from "./lib/verifier-cleanup.mjs"; import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/process/HopperLease.js"; assert.equal(process.platform, "darwin", "requires macOS"); @@ -48,11 +52,9 @@ const digest = async (path) => createHash("sha256") .update(await readFile(path)) .digest("hex"); -const sessionsBefore = await snapshotHopperRuntime( - "/tmp", - HOPPER_TARGET_LEASE_DIRECTORY, -); -const ownedProcessIds = new Set(); +const sessionsBefore = ( + await snapshotHopperRuntime("/tmp", HOPPER_TARGET_LEASE_DIRECTORY) +).paths; const client = new Client({ name: "hopper-callers-verifier", version: "1" }); const transport = new StdioClientTransport({ command: process.execPath, @@ -93,7 +95,7 @@ const matchingRef = (ref, site, from, to) => ref.source_procedure?.address === from && ref.target_procedure?.address === to && ref.call?.classification === "direct"; -try { +await withVerifierCleanup(async () => { await run(clang, [ "-isysroot", sdk, @@ -129,7 +131,6 @@ try { await client.connect(transport); if (transport.pid === null) throw new Error("MCP process identity unavailable"); - ownedProcessIds.add(transport.pid); let notifications = 0; const original = transport.onmessage; transport.onmessage = (message) => { @@ -287,23 +288,36 @@ try { sha256: beforeLarge, }), ); -} finally { - await client.close(); - await transport.close(); - await new Promise((resolve) => setTimeout(resolve, 500)); - const leaked = [ - ...(await snapshotHopperRuntime( - "/tmp", - HOPPER_TARGET_LEASE_DIRECTORY, - ownedProcessIds, - )), - ].filter((path) => !sessionsBefore.has(path)); - if (leaked.length === 0) +}, [ + async () => { + try { + await closeVerifierResources([ + () => client.close(), + () => transport.close(), + async () => { + await new Promise((resolve) => setTimeout(resolve, 500)); + const leaked = [ + ...( + await snapshotHopperRuntime("/tmp", HOPPER_TARGET_LEASE_DIRECTORY) + ).paths, + ].filter((path) => !sessionsBefore.has(path)); + assert.deepEqual( + leaked, + [], + `Hopper resources remain: ${leaked.join(", ")}`, + ); + }, + ]); + } catch (cause) { + throw new Error( + `Hopper cleanup failed; fixture retained at ${directory}`, + { + cause, + }, + ); + } finally { + if (stderr) process.stderr.write(stderr); + } await rm(directory, { recursive: true, force: true }); - if (stderr) process.stderr.write(stderr); - assert.deepEqual( - leaked, - [], - `Hopper resources remain: ${leaked.join(", ")}; fixture retained at ${directory}`, - ); -} + }, +]); diff --git a/scripts/verify-real-hopper-deadlines.mjs b/scripts/verify-real-hopper-deadlines.mjs index 77f3b6b329..2cbf81df81 100644 --- a/scripts/verify-real-hopper-deadlines.mjs +++ b/scripts/verify-real-hopper-deadlines.mjs @@ -1,10 +1,11 @@ import assert from "node:assert/strict"; -import { copyFile, mkdtemp, realpath, rm } from "node:fs/promises"; +import { copyFile, mkdtemp, realpath } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { firstProcedureAddress } from "./lib/real-hopper-assertions.mjs"; +import { HopperDeadlineCleanup } from "./lib/hopper-deadline-cleanup.mjs"; import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js"; import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js"; import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js"; @@ -29,6 +30,7 @@ const root = await realpath( ); const target = join(root, "deadline-fixture"); const profileResources = new ArtifactResourceScope(); +const cleanupOwner = new HopperDeadlineCleanup(root, profileResources); const profileResourceScope = { retain: (owner, resource) => profileResources.retain({ @@ -40,6 +42,7 @@ const profileResourceScope = { }; let client; let primaryFailure; +let operationFailed = false; try { await copyFile(fixtures.primary.path, target); const parsed = await parseBinaryTarget(target); @@ -148,31 +151,19 @@ try { ), ); } catch (cause) { + operationFailed = true; primaryFailure = cause; throw cause; } finally { - let cleanupFailure; - if (client !== undefined) { - const closed = await client.close(); - if (!closed.ok) cleanupFailure = closed.error; - } try { - await profileResources.close(); - } catch (cause) { - cleanupFailure = - cleanupFailure === undefined - ? cause - : new AggregateError( - [cleanupFailure, cause], - "Hopper profile and client cleanup failed", - ); - } - await rm(root, { recursive: true, force: true }); - if (cleanupFailure !== undefined) - throw primaryFailure === undefined - ? cleanupFailure - : new AggregateError( + if (client !== undefined) cleanupOwner.setClient(client); + await cleanupOwner.close(); + } catch (cleanupFailure) { + throw operationFailed + ? new AggregateError( [primaryFailure, cleanupFailure], "Hopper verification and cleanup failed", - ); + ) + : cleanupFailure; + } } diff --git a/scripts/verify-real-hopper-fat.mjs b/scripts/verify-real-hopper-fat.mjs index 930935ef7c..99dd1330f2 100644 --- a/scripts/verify-real-hopper-fat.mjs +++ b/scripts/verify-real-hopper-fat.mjs @@ -23,6 +23,10 @@ import { requireMcpOperationResult, } from "./lib/mcp-verifier-results.mjs"; import { snapshotHopperRuntime } from "./lib/real-hopper-cleanup.mjs"; +import { + closeVerifierResources, + withVerifierCleanup, +} from "./lib/verifier-cleanup.mjs"; import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/process/HopperLease.js"; import { parseConfig } from "../dist/config/parseConfig.js"; import { @@ -70,14 +74,12 @@ const intel = join(directory, "c-x86_64"); const fat = join(directory, "c-fat"); const fat64 = join(directory, "c-fat64"); const singleFat = join(directory, "c-single-fat"); -const owned = new Set(); const extraFat64 = []; const invalidFat64 = []; let preparedImagePath; -const before = await snapshotHopperRuntime( - "/tmp", - HOPPER_TARGET_LEASE_DIRECTORY, -); +const before = ( + await snapshotHopperRuntime("/tmp", HOPPER_TARGET_LEASE_DIRECTORY) +).paths; const client = new Client({ name: "hopper-fat-verifier", version: "1" }); const transport = new StdioClientTransport({ command: process.execPath, @@ -94,7 +96,7 @@ const ajv = new Ajv({ strict: false }); addFormats(ajv); const observations = []; const sourceBinding = []; -try { +await withVerifierCleanup(async () => { await run(toolPaths.clang, [ "-isysroot", sdk, @@ -183,9 +185,17 @@ try { "overlap", /slices overlap/u, (bytes) => { - const cpu = process.arch === "arm64" ? 0x100000c : 0x1000007; - const base = bytes.readUInt32BE(8) === cpu ? 8 : 40; - bytes.copy(bytes, base === 8 ? 40 : 8, base, base + 32); + const firstOffset = bytes.readBigUInt64BE(16); + const secondOffset = bytes.readBigUInt64BE(48); + const earlier = firstOffset < secondOffset ? 8 : 40; + const laterOffset = + firstOffset < secondOffset ? secondOffset : firstOffset; + const earlierOffset = + firstOffset < secondOffset ? firstOffset : secondOffset; + bytes.writeBigUInt64BE( + laterOffset - earlierOffset + 16n, + earlier + 16, + ); }, ], ]) { @@ -211,11 +221,14 @@ try { } const path = join(directory, "c-fat64-ambiguous"); await writeFile(path, ambiguous); - invalidFat64.push({ path, reason: /selection.*ambiguous/u }); + invalidFat64.push({ + path, + reason: + /FAT binary declares several (?:arm64|x86_64) slices; no slice was selected/u, + }); } await client.connect(transport); assert.ok(transport.pid !== null); - owned.add(transport.pid); const catalog = await client.listTools(); const call = async (name, args = {}) => { const reply = await client.callTool( @@ -359,14 +372,27 @@ try { if (verifyFat64) { await call("open_binary", { path: fat64 }); await call("list_procedures"); - const resources = [ - ...(await snapshotHopperRuntime( - "/tmp", - HOPPER_TARGET_LEASE_DIRECTORY, - owned, - )), - ].filter((path) => !before.has(path)); - for (const root of resources) { + const session = await call("binary_session"); + const runId = session.analysis_run?.run_id; + assert.ok( + typeof runId === "string" && runId.length > 0, + "Missing analysis run identity", + ); + const resources = await snapshotHopperRuntime( + "/tmp", + HOPPER_TARGET_LEASE_DIRECTORY, + runId, + ); + const unresolvedNew = [...resources.unresolvedSessions].filter( + (path) => !before.has(path), + ); + assert.deepEqual( + unresolvedNew, + [], + "Cannot identify a prepared image while new Hopper resource ownership is unresolved", + ); + for (const root of resources.confirmedSessions) { + if (before.has(root)) continue; const image = join(root, "image.macho"); if ( await access(image).then( @@ -403,27 +429,43 @@ try { }, ); assert.ok(pending.child.pid !== undefined); - owned.add(pending.child.pid); const cli = JSON.parse((await pending).stdout); assert.deepEqual(cli.normalized_result, expected.mapping); } -} finally { - await client.close(); - await transport.close(); - await rm(directory, { recursive: true, force: true }); -} -const retained = [ - ...(await snapshotHopperRuntime( - "/tmp", - HOPPER_TARGET_LEASE_DIRECTORY, - owned, - )), -].filter((path) => !before.has(path)); -assert.deepEqual( - retained, - [], - "FAT verification retained owned Hopper sessions or leases", -); +}, [ + async () => { + try { + await closeVerifierResources([ + () => client.close(), + () => transport.close(), + ]); + } catch (cause) { + throw new Error( + `Hopper cleanup failed; fixture retained at ${directory}`, + { + cause, + }, + ); + } + try { + const retained = [ + ...(await snapshotHopperRuntime("/tmp", HOPPER_TARGET_LEASE_DIRECTORY)) + .paths, + ].filter((path) => !before.has(path)); + assert.deepEqual( + retained, + [], + "Hopper resources remain or are unresolved", + ); + } catch (cause) { + throw new Error( + `Hopper resource verification failed; fixture retained at ${directory}`, + { cause }, + ); + } + await rm(directory, { recursive: true, force: true }); + }, +]); console.log( JSON.stringify( { diff --git a/scripts/verify-real-hopper.mjs b/scripts/verify-real-hopper.mjs index 6e6cce01e5..ca183ffd0b 100644 --- a/scripts/verify-real-hopper.mjs +++ b/scripts/verify-real-hopper.mjs @@ -62,10 +62,9 @@ const parseServerArgs = (encoded) => { }; const runtimeParent = process.platform === "darwin" ? "/tmp" : tmpdir(); -const sessionsBefore = await snapshotHopperRuntime( - runtimeParent, - HOPPER_TARGET_LEASE_DIRECTORY, -); +const sessionsBefore = ( + await snapshotHopperRuntime(runtimeParent, HOPPER_TARGET_LEASE_DIRECTORY) +).paths; const ownedProcessIds = new Set(); const textValue = mcpTextValue; @@ -557,11 +556,8 @@ try { await new Promise((resolve) => setTimeout(resolve, 500)); const sessionsAfter = [ - ...(await snapshotHopperRuntime( - runtimeParent, - HOPPER_TARGET_LEASE_DIRECTORY, - ownedProcessIds, - )), + ...(await snapshotHopperRuntime(runtimeParent, HOPPER_TARGET_LEASE_DIRECTORY)) + .paths, ] .filter((path) => !sessionsBefore.has(path)) .sort(); @@ -573,7 +569,7 @@ if (sessionsAfter.length > 0) { retainedRuntimePaths: sessionsAfter, }); throw new Error( - `The MCP runtime retained Hopper resources: ${sessionsAfter.join(", ")}`, + `Hopper resources remain or have unresolved ownership: ${sessionsAfter.join(", ")}`, ); } if (summary === undefined) diff --git a/scripts/verify-test-discovery.mjs b/scripts/verify-test-discovery.mjs index 5db6905852..57d6d177ca 100644 --- a/scripts/verify-test-discovery.mjs +++ b/scripts/verify-test-discovery.mjs @@ -40,7 +40,7 @@ try { const expected = []; for (const path of files.stdout .split("\0") - .filter((path) => path.endsWith(".test.ts"))) { + .filter((path) => /\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(path))) { try { if ((await stat(join(root, path))).isFile()) expected.push(path); } catch (cause) { @@ -65,4 +65,6 @@ try { await rm(temporary, { recursive: true, force: true }); } if (discovered !== undefined) - console.log(`Discovered all ${discovered} test files exactly once.`); + console.log( + `Discovered all ${discovered} test and spec files in src and tests exactly once.`, + ); diff --git a/scripts/verify/apple/dispatch.mjs b/scripts/verify/apple/dispatch.mjs index cfb77eff32..7021a80bbf 100644 --- a/scripts/verify/apple/dispatch.mjs +++ b/scripts/verify/apple/dispatch.mjs @@ -6,7 +6,7 @@ import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js"; -import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js"; +import { withVerifierArtifactScope } from "../../lib/verifier-artifact-scope.mjs"; import { inspectAppleDispatchMetadata } from "../../../dist/native/AppleDispatchMetadata.js"; import { artifactCliEvidence, @@ -57,8 +57,7 @@ const fixupCoverage = (metadata) => const inspect = async (path) => { const target = await parseBinaryTarget(path); if (!target.ok) throw target.error; - const resources = new ArtifactResourceScope(); - try { + return withVerifierArtifactScope(async (resources) => { return { target: target.value, metadata: ( @@ -67,10 +66,7 @@ const inspect = async (path) => { ) ).result, }; - } finally { - // Retry any descriptor whose close failed, surfacing unresolved cleanup. - await resources.close(); - } + }); }; const checkObjc = (metadata, mode) => { diff --git a/scripts/verify/architecture-guards.mjs b/scripts/verify/architecture-guards.mjs index c3b51552fb..540c72f76f 100644 --- a/scripts/verify/architecture-guards.mjs +++ b/scripts/verify/architecture-guards.mjs @@ -5,17 +5,12 @@ import { join, relative, sep } from "node:path"; import { fileURLToPath } from "node:url"; // Architecture guards for the strict-identity / best-effort-inference split. -// - Evidence ordering must be locale-independent (ICU varies by host), so -// src/domain orders with compareUnicodeCodePoints, never localeCompare. // - Composite dedup keys must not NUL-join variable-length or free-form // MIDDLE fields: ["a\0b"] collapses with ["a", "b"] and drops a finding. -// compositeKey (JSON tuple) is the single owner for composite keys. +// The text check below only flags one spelling; it is not a complete audit. // - Transparent wrappers unwrap in exactly one place // (javascriptAstValues.unwrapJavaScriptExpression); direct isTS* unwrapping // elsewhere drifts (the `satisfies` class of bug). -// - AST walks go through traverseJavaScriptAst/childNodes: bespoke -// Object.values walkers diverge on loc/comment fields and overflow the -// stack on deep generated chains. const root = fileURLToPath(new URL("../../", import.meta.url)); @@ -27,6 +22,7 @@ const listFiles = async (dir) => { else if ( entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts") && + !entry.name.endsWith(".spec.ts") && !entry.name.endsWith(".fixture.ts") ) output.push(path); @@ -48,8 +44,6 @@ const files = await Promise.all( content: stripComments(await readFile(path, "utf8")), })), ); -const inDir = (dir) => files.filter((file) => file.path.startsWith(dir)); - const failures = []; const check = (label, selected, pattern, allow = []) => { const offenders = selected.filter( @@ -68,21 +62,7 @@ check( /providers\/ProviderInspectionScope\.js/, ); -check( - "src/domain uses localeCompare (use compareUnicodeCodePoints)", - inDir("src/domain/"), - /\.localeCompare\(/, -); - -check( - "NUL-joined list key (use compositeKey)", - files, - /\.join\(["']\\0["']\)/, - [ - // Sorted pair of constrained trace IDs; documented in plan. - "src/domain/process/processTraceSpecification.ts", - ], -); +check("NUL-joined list key (use compositeKey)", files, /\.join\(["']\\0["']\)/); check( "direct transparent-wrapper unwrap outside javascriptAstValues (use unwrapJavaScriptExpression)", @@ -91,13 +71,6 @@ check( ["src/domain/javascript/javascriptAstValues.ts"], ); -check( - "bespoke AST child walker (use traverseJavaScriptAst/childNodes)", - files, - /VISITOR_KEYS/, - ["src/domain/javascript/javascriptSemanticTraversal.ts"], -); - // A producer that pins both the soft and hard ceiling of every resource takes // the vector from one owner. prlimitCeilingArguments is the command-less prefix // of prlimitArgumentVector; both live in src/process/LinuxResourceLimits.ts. @@ -163,7 +136,6 @@ check( "src/application/GuidanceAccess.ts", "src/application/javascript/ElectronBoundaryAnalysis.ts", "src/application/javascript/ElectronBoundaryGraphContext.ts", - "src/application/javascript/JavaScriptArtifactGraphContext.ts", "src/browser/CdpCaptureCompleteness.ts", "src/browser/CdpCaptureEventHandlers.ts", "src/browser/CdpPageCapture.ts", @@ -179,7 +151,6 @@ check( "src/domain/javascript/javascriptNativeHandoff.ts", "src/domain/javascript/javascriptRuntimeReconciliationMatching.ts", "src/domain/javascript/javascriptRuntimeReconciliationSchemas.ts", - "src/domain/javascript/javascriptSemanticQuery.ts", "src/domain/javascript/javascriptStaticAnalysisBundler.ts", "src/domain/javascript/javascriptStaticAnalysisCalls.ts", "src/domain/javascript/javascriptStaticAnalysisFindings.ts", diff --git a/scripts/verify/javascript/runtime-observation.mjs b/scripts/verify/javascript/runtime-observation.mjs index bd1781ca52..d77d6790fa 100644 --- a/scripts/verify/javascript/runtime-observation.mjs +++ b/scripts/verify/javascript/runtime-observation.mjs @@ -5,7 +5,6 @@ import { parseArgs } from "node:util"; import { readStableArtifact } from "../../../dist/artifacts/readStableArtifact.js"; import { ArtifactReaderFailure } from "../../../dist/artifacts/ArtifactReader.js"; -import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js"; import { ArtifactFileCleanupFailure, artifactFileCompletion, @@ -14,6 +13,7 @@ import { import { reconcileJavaScriptRuntimeEvidence } from "../../../dist/application/javascript/JavaScriptRuntimeReconciliationService.js"; import { analyzeJavaScriptApplicationWithOwnedScope } from "../../lib/javascript-application-analysis.mjs"; +import { withVerifierArtifactScope } from "../../lib/verifier-artifact-scope.mjs"; import { completeVerifierRun, createVerifierRun, @@ -106,33 +106,23 @@ process.stdout.write( ); async function readEvidenceFile(path) { - const scope = new ArtifactResourceScope(); let outcome; try { outcome = { ok: true, - value: await readStableArtifact( - await realpath(path), - 64 * 1024 * 1024, - undefined, - undefined, - scope, + value: await withVerifierArtifactScope(async (scope) => + readStableArtifact( + await realpath(path), + 64 * 1024 * 1024, + undefined, + undefined, + scope, + ), ), }; } catch (cause) { outcome = { ok: false, cause }; } - try { - await scope.close(); - } catch (cleanupCause) { - if (!outcome.ok) - throw new AggregateError( - [outcome.cause, cleanupCause], - "Verifier cleanup failed", - { cause: outcome.cause }, - ); - throw cleanupCause; - } if (!outcome.ok && outcome.cause instanceof ArtifactFileCleanupFailure) { const completion = artifactFileCompletion(outcome.cause); outcome = diff --git a/scripts/verify/managed/manifest.mjs b/scripts/verify/managed/manifest.mjs index 23be723fd6..77d82f0571 100644 --- a/scripts/verify/managed/manifest.mjs +++ b/scripts/verify/managed/manifest.mjs @@ -1,10 +1,9 @@ import { basename, dirname, isAbsolute, resolve } from "node:path"; -import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js"; import { readManagedSnapshot, verifyManagedSourceIdentity, } from "../../../dist/dotnet/ManagedSnapshot.js"; -import { withVerifierCleanup } from "../../lib/verifier-cleanup.mjs"; +import { withVerifierArtifactScope } from "../../lib/verifier-artifact-scope.mjs"; import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js"; import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js"; @@ -24,13 +23,10 @@ import { const READ_FILE_MAX_BYTES = 0x7fff_ffff; export async function verifyManagedAppManifest(rawManifest, manifestPath) { - const resources = new ArtifactResourceScope(); - return withVerifierCleanup( - () => - resources.run(() => - verifyManifestSnapshot(rawManifest, manifestPath, resources), - ), - [() => resources.close()], + return withVerifierArtifactScope((resources) => + resources.run(() => + verifyManifestSnapshot(rawManifest, manifestPath, resources), + ), ); } diff --git a/scripts/verify/rea-numeric-limits.d.mts b/scripts/verify/rea-numeric-limits.d.mts index f61bd401c8..9b3762519c 100644 --- a/scripts/verify/rea-numeric-limits.d.mts +++ b/scripts/verify/rea-numeric-limits.d.mts @@ -1,13 +1,6 @@ /** Relative path of the registry. Its text is a declaration, not a producer. */ export const NUMERIC_LIMIT_DECLARATION: "src/domain/numericLimitRegistry.ts"; -/** Limits that must become analysis settings and cannot be registry exceptions. */ -export const REQUIRED_RUNTIME_LIMITS: readonly [ - "REA_NTFS_MFT_MAX_FILE_BYTES", - "REA_REGF_HIVE_MAX_FILE_BYTES", - "REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES", -]; - /** True for a numeric `REA_` limit, including the documented endpoint-candidate count. */ export function isNumericLimit(name: string): boolean; @@ -55,6 +48,8 @@ export function discoverNumericLimitUses( readonly unresolved: readonly { readonly path: string; readonly offset: number; + readonly line: number; + readonly column: number; readonly reason: string; }[]; }; diff --git a/scripts/verify/rea-numeric-limits.mjs b/scripts/verify/rea-numeric-limits.mjs index 60cfdf21af..53f981ae6f 100644 --- a/scripts/verify/rea-numeric-limits.mjs +++ b/scripts/verify/rea-numeric-limits.mjs @@ -25,6 +25,7 @@ const listFiles = async (dir) => { } else if ( entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts") && + !entry.name.endsWith(".spec.ts") && !entry.name.endsWith(".fixture.ts") ) output.push(path); @@ -39,7 +40,7 @@ export const NUMERIC_LIMIT_DECLARATION = "src/domain/numericLimitRegistry.ts"; * These limits are moving onto runtime analysis settings on their own PRs. * A producer or a registry entry cannot satisfy that requirement. */ -export const REQUIRED_RUNTIME_LIMITS = [ +const REQUIRED_RUNTIME_LIMITS = [ "REA_NTFS_MFT_MAX_FILE_BYTES", "REA_REGF_HIVE_MAX_FILE_BYTES", "REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES", @@ -170,8 +171,16 @@ const stringBindings = (sourceFile, symbols, unresolved) => { item.offset === offset && item.reason === reason, ) - ) - unresolved.push({ path, offset, reason }); + ) { + const location = sourceFile.getLineAndCharacterOfPosition(offset); + unresolved.push({ + path, + offset, + line: location.line + 1, + column: location.character + 1, + reason, + }); + } }; const initializer = (identifier) => { if (!candidates.has(identifier.text)) return undefined; @@ -949,7 +958,7 @@ if (direct) { for (const issue of discovered.unresolved) console.log( - `${issue.path}:${issue.offset}: numeric limit candidate unresolved (${issue.reason})`, + `${issue.path}:${issue.line}:${issue.column}: numeric limit candidate unresolved (${issue.reason})`, ); for (const [name, file] of [...discovered].sort(([left], [right]) => diff --git a/src/android/AndroidNativeInventory.ts b/src/android/AndroidNativeInventory.ts index 1000c37e23..0b11746e7c 100644 --- a/src/android/AndroidNativeInventory.ts +++ b/src/android/AndroidNativeInventory.ts @@ -1,6 +1,8 @@ import type { JadxToolPort } from "./JadxAnalysis.js"; -import type { AndroidNativeDeclarations } from "../domain/android/androidNativeDeclarations.js"; -import { jadxNativeDeclarationsSchema } from "./JadxProtocol.js"; +import { + androidNativeDeclarationsSchema, + type AndroidNativeDeclarations, +} from "../domain/android/androidNativeDeclarations.js"; import { AnalysisOutputError } from "../domain/analysisErrorCore.js"; /** Validate the native-only batch at its producing boundary, preserving raw facts. */ @@ -9,7 +11,7 @@ export const readAndroidNativeInventory = async ( snapshot: string, ): Promise => { const raw = await tools.json("rea_native_declarations", { path: snapshot }); - const parsed = jadxNativeDeclarationsSchema.safeParse(raw); + const parsed = androidNativeDeclarationsSchema.safeParse(raw); if (!parsed.success) throw new AnalysisOutputError( "inspect_android_jni_bindings", diff --git a/src/android/JadxProtocol.ts b/src/android/JadxProtocol.ts index 278bc2dd42..66bfbd39ed 100644 --- a/src/android/JadxProtocol.ts +++ b/src/android/JadxProtocol.ts @@ -10,11 +10,6 @@ import { dexDescriptorSchema, dexTypeSchema, } from "../domain/android/dexTypes.js"; -import { androidNativeDeclarationsSchema } from "../domain/android/androidNativeDeclarations.js"; - -/** Native-only raw DEX facts authored before decompiler deduplication. */ -export const jadxNativeDeclarationsSchema = androidNativeDeclarationsSchema; - const count = z.int().nonnegative(); /** Typed allocation failures authored before upstream erases the JVM exception. */ const jadxMemoryFailureSchema = z.strictObject({ diff --git a/src/apkSignature/apkSignatureFailures.ts b/src/apkSignature/apkSignatureFailures.ts index d63a2942b2..4e29403e84 100644 --- a/src/apkSignature/apkSignatureFailures.ts +++ b/src/apkSignature/apkSignatureFailures.ts @@ -1,6 +1,6 @@ import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; -import { stableArtifactInputRecovery } from "../artifacts/StableArtifactFailure.js"; +import { stableArtifactInputFailure } from "../artifacts/StableArtifactFailure.js"; import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, @@ -231,11 +231,7 @@ const artifactReaderFailure = ( }, ); case "path": - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION); default: return adapterFailure(cause, path); } diff --git a/src/application/ClientRegistration.ts b/src/application/ClientRegistration.ts index a0824ccc1f..6fb7fa6aa2 100644 --- a/src/application/ClientRegistration.ts +++ b/src/application/ClientRegistration.ts @@ -18,7 +18,10 @@ import { type ClientRegistrationDialect, } from "./ClientConfigurationDocument.js"; import type { SetupClient } from "./SupportedClients.js"; -import type { ClientRegistrationTarget } from "./ClientRegistrationProfile.js"; +import { + stdioSelectingFields, + type ClientRegistrationTarget, +} from "./ClientRegistrationProfile.js"; import { classifyClientRegistrationCommand, equivalentClientRegistrationCommands, @@ -277,12 +280,18 @@ export const clientRegistrationSourceFacts = ( ), ); } else { + const launcherFields = new Set(fields); for (const field of connection.kind === "cline" ? ["transport", "type", "transportType", "url"] : connection.kind === "commandcode" ? ["type", "url"] : connection.fields) fields.add(field); + // An existing selector that already chooses stdio stays, including one + // inherited through a YAML merge; only remote selectors are superseded. + if (connection.kind === "flat") + for (const field of stdioSelectingFields(source ?? {}, connection)) + if (!launcherFields.has(field)) fields.delete(field); } return ok({ ...parsed, connectionFields: fields }); }; diff --git a/src/application/ClientRegistrationProfile.ts b/src/application/ClientRegistrationProfile.ts index 31904a31ca..0365750d04 100644 --- a/src/application/ClientRegistrationProfile.ts +++ b/src/application/ClientRegistrationProfile.ts @@ -51,3 +51,28 @@ export interface ClientRegistrationTarget { readonly dialect: ClientRegistrationDialect; readonly profile: ClientRegistrationProfile; } + +/** + * Enum selectors whose current value already chooses stdio. Replacing the + * launcher with a stdio launcher leaves them correct, so a refresh keeps them + * instead of superseding them like a remote selector. + */ +export const stdioSelectingFields = ( + source: Readonly>, + connection: Extract, +): ReadonlySet => + new Set( + connection.stdio.flatMap((condition) => { + if ( + condition.kind !== "required-enum" && + condition.kind !== "optional-enum" + ) + return []; + const value = Object.hasOwn(source, condition.field) + ? source[condition.field] + : undefined; + return typeof value === "string" && condition.values.includes(value) + ? [condition.field] + : []; + }), + ); diff --git a/src/application/GooseConfigurationYaml.test.ts b/src/application/GooseConfigurationYaml.test.ts index c12ceb116b..727196ef2d 100644 --- a/src/application/GooseConfigurationYaml.test.ts +++ b/src/application/GooseConfigurationYaml.test.ts @@ -18,6 +18,123 @@ const extension = { timeout: 300, }; +describe("Goose YAML source key retention", () => { + it("updates and removes an extension selected through aliased path keys", () => { + const source = + "table: &table extensions\nname: &name rea\n? *table\n:\n ? *name\n : {cmd: old, type: stdio}\n"; + const updated = editGooseConfigurationYaml(source, ["extensions", "rea"], { + cmd: "rea", + type: "stdio", + }); + expect(updated).toContain("*table"); + expect(updated).toContain("*name"); + expect(parseGooseConfigurationYaml(updated)).toMatchObject({ + extensions: { rea: { cmd: "rea", type: "stdio" } }, + }); + const removed = editGooseConfigurationYaml( + updated, + ["extensions", "rea"], + undefined, + ); + expect(removed).not.toContain("*name"); + expect(parseGooseConfigurationYaml(removed)).toMatchObject({ + extensions: {}, + }); + const ambiguous = + "name: &name rea\nextensions:\n ? *name\n : {cmd: first}\n rea: {cmd: second}\n"; + expect(() => + editGooseConfigurationYaml(ambiguous, ["extensions", "rea"], { + cmd: "rea", + }), + ).toThrow(/more than one key resolving to "rea"/); + }); + + it("retains a collection mapping key when an adjacent field changes", () => { + const source = + "extensions:\n rea:\n cmd: old\n ? [one, two]\n : retained\n"; + const parsed = parseGooseConfigurationYaml(source) as { + extensions: { rea: Record }; + }; + const updated = editGooseConfigurationYaml(source, ["extensions", "rea"], { + ...parsed.extensions.rea, + cmd: "rea", + }); + const document = parseDocument(updated); + expect(document.errors).toEqual([]); + const native = document.toJS({ mapAsMap: true }) as Map; + const extensions = native.get("extensions") as Map; + const entry = extensions.get("rea") as Map; + expect([...entry.entries()]).toContainEqual([["one", "two"], "retained"]); + expect(entry.get("cmd")).toBe("rea"); + }); + + it("retains an aliased mapping key when an adjacent launcher field changes", () => { + const source = + 'label: &label 1\nextensions:\n rea:\n cmd: old\n ? *label\n : numeric\n "1": string\n'; + const parsed = parseGooseConfigurationYaml(source) as { + extensions: { rea: Record }; + }; + const updated = editGooseConfigurationYaml(source, ["extensions", "rea"], { + ...parsed.extensions.rea, + cmd: "rea", + }); + expect(updated).toContain("*label"); + expect(updated).toContain("numeric"); + expect(updated).toContain('"1": string'); + }); + + it("preserves the order of an anchor and a retained non-string key alias", () => { + const source = + 'extensions:\n rea:\n cmd: old\n shared: &seed value\n 1: *seed\n "1": string\n'; + const parsed = parseGooseConfigurationYaml(source) as { + extensions: { rea: Record }; + }; + const updated = editGooseConfigurationYaml(source, ["extensions", "rea"], { + ...parsed.extensions.rea, + cmd: "rea", + }); + expect(updated.indexOf("&seed")).toBeLessThan(updated.indexOf("*seed")); + expect(parseGooseConfigurationYaml(updated)).toMatchObject({ + extensions: { rea: { shared: "value", cmd: "rea" } }, + }); + }); + + it("retains distinct keys beside an edited field and rejects an ambiguous edit", () => { + const source = + 'extensions:\n rea:\n cmd: old\n 1: numeric\n "1": string\n'; + const parsed = parseGooseConfigurationYaml(source) as { + extensions: { rea: Record }; + }; + const entry = parsed.extensions.rea; + const updated = editGooseConfigurationYaml(source, ["extensions", "rea"], { + ...entry, + cmd: "rea", + }); + expect(updated).toContain(' 1: numeric\n "1": string'); + expect(updated).toContain("cmd: rea"); + expect(() => + editGooseConfigurationYaml(source, ["extensions", "rea"], { + ...entry, + "1": "changed", + }), + ).toThrow(/source identity is not a plain string key/); + }); + + it("keeps distinct native mapping keys in unchanged extension settings", () => { + const source = + 'extensions:\n rea:\n cmd: old\n metadata:\n 1: numeric\n "1": string\n'; + const parsed = parseGooseConfigurationYaml(source) as { + extensions: { rea: Record }; + }; + const updated = editGooseConfigurationYaml(source, ["extensions", "rea"], { + ...parsed.extensions.rea, + cmd: "rea", + }); + expect(updated).toContain(' 1: numeric\n "1": string'); + expect(updated).toContain("cmd: rea"); + }); +}); + describe("Goose YAML 1.2 source facts", () => { it("keeps native strings and exact large integer facts distinct", () => { const source = @@ -344,3 +461,45 @@ describe("pinned Goose serde_yaml scalar resolution", () => { }, ); }); + +it("materializes an alias whose sibling anchor the refresh replaces", () => { + const source = + "extensions:\n rea:\n cmd: &launcher /old/rea\n envs: {REA_LAUNCHER: *launcher}\n"; + const out = editGooseConfigurationYaml(source, ["extensions", "rea"], { + cmd: "/new/rea", + envs: { REA_LAUNCHER: "/old/rea" }, + }); + expect(parseGooseConfigurationYaml(out)).toMatchObject({ + extensions: { + rea: { cmd: "/new/rea", envs: { REA_LAUNCHER: "/old/rea" } }, + }, + }); +}); + +it("edits a field whose key is an alias to a shared string, keeping the alias", () => { + const source = + "field: &field envs\nextensions:\n rea:\n cmd: rea\n *field : {A: '1'}\n"; + const out = editGooseConfigurationYaml(source, ["extensions", "rea"], { + cmd: "rea", + envs: { A: "2" }, + }); + expect(out).toContain("*field"); + expect(parseGooseConfigurationYaml(out)).toMatchObject({ + field: "envs", + extensions: { rea: { cmd: "rea", envs: { A: "2" } } }, + }); +}); + +it("keeps an anchored value with an aliased key ahead of its later aliases", () => { + const source = + "field: &field envs\nextensions:\n rea:\n cmd: old\n *field : &settings {A: '1'}\n 1: *settings\n \"1\": string\n"; + const out = editGooseConfigurationYaml(source, ["extensions", "rea"], { + cmd: "new", + envs: { A: "1" }, + 1: "string", + }); + expect(out.indexOf("&settings")).toBeLessThan(out.indexOf("*settings")); + expect(parseGooseConfigurationYaml(out)).toMatchObject({ + extensions: { rea: { cmd: "new", envs: { A: "1" } } }, + }); +}); diff --git a/src/application/GooseConfigurationYaml.ts b/src/application/GooseConfigurationYaml.ts index 8d1ae5cd78..2f77b3704e 100644 --- a/src/application/GooseConfigurationYaml.ts +++ b/src/application/GooseConfigurationYaml.ts @@ -13,9 +13,12 @@ import { ConfigurationError } from "../domain/configurationErrors.js"; import { copyYamlAliasSource, preserveYamlAliasValues, + resolveYamlEditPath, } from "./YamlConfigurationAliases.js"; import { + materializeUnresolvedYamlAliases, retainYamlConfigurationSource, + yamlAliasValues, retainYamlScalarSource, } from "./YamlConfigurationSource.js"; @@ -251,7 +254,7 @@ export const editGooseConfigurationYaml = ( if (!isMap(document.contents)) throw new ConfigurationError("Goose configuration root must be a mapping"); for (let length = 1; length < path.length; length++) { - const parent = path.slice(0, length); + const parent = resolveYamlEditPath(document, path.slice(0, length)); let previous = document.getIn(parent, true); if (isAlias(previous)) { previous = copyYamlAliasSource(previous, document); @@ -262,30 +265,33 @@ export const editGooseConfigurationYaml = ( document.setIn(parent, document.createNode({})); } else if (!isMap(previous)) throw new ConfigurationError( - `Goose configuration ${parent.join(".")} must be a mapping`, + `Goose configuration ${path.slice(0, length).join(".")} must be a mapping`, ); } - const previous = document.getIn(path, true); - const parent = document.getIn(path.slice(0, -1), true); + const sourcePath = resolveYamlEditPath(document, path); + const previous = document.getIn(sourcePath, true); + const parent = document.getIn(sourcePath.slice(0, -1), true); const removedKey = value === undefined && isMap(parent) - ? parent.items.find( - (pair) => isScalar(pair.key) && pair.key.value === path.at(-1), - )?.key + ? parent.items.find((pair) => pair.key === sourcePath.at(-1))?.key : undefined; + const aliasValues = yamlAliasValues(previous, document); const retained = preserveYamlAliasValues(document, [previous, removedKey]); if (value === undefined) { - if (!document.deleteIn(path)) return text; - } else + if (!document.deleteIn(sourcePath)) return text; + } else { document.setIn( - path, + sourcePath, isMap(previous) || isSeq(previous) ? retainYamlConfigurationSource( previous, document.createNode(value), retained, + document, ) : value, ); + materializeUnresolvedYamlAliases(document, aliasValues); + } return document.toString({ lineWidth: 0 }); }; diff --git a/src/application/HermesConfigurationYaml.test.ts b/src/application/HermesConfigurationYaml.test.ts index 01e5d5e528..1b07b013eb 100644 --- a/src/application/HermesConfigurationYaml.test.ts +++ b/src/application/HermesConfigurationYaml.test.ts @@ -1,10 +1,13 @@ import { describe, expect, it } from "vitest"; import { parse } from "yaml"; import { + effectiveClientServer, parseClientConfiguration, serializeClientConfiguration, withClientServers, } from "./ClientConfigurationDocument.js"; +import { refreshClientRegistration } from "./ClientRegistration.js"; +import { supportedClients } from "./SupportedClients.js"; import { editHermesConfigurationYaml, @@ -12,6 +15,69 @@ import { stringifyHermesConfigurationYaml, } from "./HermesConfigurationYaml.js"; +it("uses the first inherited mapping when creating an override", () => { + const source = + "first: &first\n mcp_servers:\n one: {command: first}\nsecond: &second\n mcp_servers:\n two: {command: second}\n<<: [*first, *second]\n"; + const updated = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + }); + const parsed = parseHermesConfigurationYaml(updated) as { + mcp_servers: Record; + }; + expect(parsed.mcp_servers).toEqual({ + one: { command: "first" }, + rea: { command: "rea" }, + }); + expect( + parseHermesConfigurationYaml( + updated.replace("command: first", "command: changed"), + ), + ).toMatchObject({ + mcp_servers: { one: { command: "changed" } }, + }); +}); + +it("refuses an inherited override when an existing anchor name is shadowed", () => { + const source = + "shared: &shared\n mcp_servers: &table\n other: {command: first}\nshadow: &table {other: {command: second}}\n<<: *shared\n"; + expect(() => + editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + }), + ).toThrow(/anchor is shadowed/); +}); + +it("retains inherited YAML settings when updating a registration", () => { + const source = + "defaults: &defaults\n enabled: false\nmcp_servers:\n rea:\n <<: *defaults\n command: old\n"; + const parsed = parseHermesConfigurationYaml(source) as { + mcp_servers: { rea: Record }; + }; + const updated = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + ...parsed.mcp_servers.rea, + command: "rea", + }); + expect(updated).toContain("<<: *defaults"); + const changedDefault = updated.replace("enabled: false", "enabled: true"); + expect(parseHermesConfigurationYaml(changedDefault)).toMatchObject({ + mcp_servers: { rea: { command: "rea", enabled: true } }, + }); + const overridden = editHermesConfigurationYaml( + source, + ["mcp_servers", "rea"], + { ...parsed.mcp_servers.rea, command: "rea", enabled: true }, + ); + expect(overridden).toContain("<<: *defaults"); + expect(parseHermesConfigurationYaml(overridden)).toMatchObject({ + mcp_servers: { rea: { command: "rea", enabled: true } }, + }); + expect(() => + editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + }), + ).toThrow(/Cannot remove inherited YAML setting/); +}); + it("preserves annotations on changed registration mappings and scalar fields", () => { const source = "mcp_servers:\n rea: # registration note\n command: old # command note\n env: # environment note\n MODE: old # mode note\n args: # arguments note\n - old # argument note\n"; @@ -601,3 +667,193 @@ it("keeps argument annotations with unique values and retains removed annotation ]) expect(updated.split(comment)).toHaveLength(2); }); + +describe("Hermes registration refresh around anchors and merges", () => { + it("keeps an alias's value when an earlier same-name anchor would capture it", () => { + const source = + "defaults: &launcher /other/rea\nmcp_servers:\n rea:\n command: &launcher /old/rea\n env: {REA_LAUNCHER: *launcher}\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "/new/rea", + env: { REA_LAUNCHER: "/old/rea" }, + }); + expect(parseHermesConfigurationYaml(out)).toMatchObject({ + defaults: "/other/rea", + mcp_servers: { + rea: { command: "/new/rea", env: { REA_LAUNCHER: "/old/rea" } }, + }, + }); + }); + + it("keeps the comment on an alias the refresh materializes", () => { + const source = + "mcp_servers:\n rea:\n command: &launcher /old/rea\n env:\n REA_LAUNCHER: *launcher # explanation\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "/new/rea", + env: { REA_LAUNCHER: "/old/rea" }, + }); + expect(out).toContain("# explanation"); + expect(parseHermesConfigurationYaml(out)).toMatchObject({ + mcp_servers: { rea: { env: { REA_LAUNCHER: "/old/rea" } } }, + }); + }); + + it("keeps an alias to a sibling anchor that survives the refresh", () => { + const source = + "mcp_servers:\n rea:\n command: old\n setting: &setting keep\n metadata: {nested: *setting}\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "new", + setting: "keep", + metadata: { nested: "keep" }, + }); + expect(out).toContain("nested: *setting"); + expect( + parseHermesConfigurationYaml( + out.replace("&setting keep", "&setting changed"), + ), + ).toMatchObject({ + mcp_servers: { rea: { metadata: { nested: "changed" } } }, + }); + }); +}); + +describe("Hermes registration refresh through inherited merges", () => { + it("refuses to drop locally overridden settings that its merge would restore", () => { + const source = + "remote: &remote\n url: https://example.invalid\n transport: sse\nmcp_servers:\n rea:\n <<: *remote\n url: https://local.invalid\n transport: sse\n"; + expect(() => + editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + args: ["mcp"], + }), + ).toThrow(/Cannot remove inherited YAML setting/); + }); + + it("replaces an inherited remote registration without its remote selectors", () => { + const source = + "shared: &shared\n rea: {url: https://example.invalid, transport: sse}\nmcp_servers:\n <<: *shared\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + args: ["mcp"], + }); + const parsed = parseHermesConfigurationYaml(out) as { + mcp_servers: { rea: Record }; + }; + expect(parsed.mcp_servers.rea).toEqual({ command: "rea", args: ["mcp"] }); + }); + + it("refuses to edit a field whose aliased and plain keys name the same setting", () => { + const source = + "field: &field env\nmcp_servers:\n rea:\n command: rea\n *field : {A: first}\n env: {A: second}\n"; + expect(() => + editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + env: { A: "third" }, + }), + ).toThrow(/env/); + }); + + it("edits a field whose key is an alias to a shared string, keeping the alias", () => { + const source = + "field: &field env\nmcp_servers:\n rea:\n command: rea\n *field : {A: '1'}\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + env: { A: "2" }, + }); + expect(out).toContain("*field"); + expect(parseHermesConfigurationYaml(out)).toMatchObject({ + field: "env", + mcp_servers: { rea: { command: "rea", env: { A: "2" } } }, + }); + }); + + it("writes a plain key when an aliased key's anchor is rebuilt by the same edit", () => { + const source = + "mcp_servers:\n rea:\n command: &field env\n *field : {A: '1'}\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + env: { A: "2" }, + }); + expect(out).not.toContain("*field"); + expect(parseHermesConfigurationYaml(out)).toMatchObject({ + mcp_servers: { rea: { command: "rea", env: { A: "2" } } }, + }); + }); + + it("materializes an alias whose sibling anchor the refresh replaces", () => { + const source = + "mcp_servers:\n rea:\n command: &launcher /old/rea\n env: {REA_LAUNCHER: *launcher}\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "/new/rea", + env: { REA_LAUNCHER: "/old/rea" }, + }); + expect(parseHermesConfigurationYaml(out)).toMatchObject({ + mcp_servers: { + rea: { command: "/new/rea", env: { REA_LAUNCHER: "/old/rea" } }, + }, + }); + }); + + it("materializes an alias to an anchor moved to an outside consumer", () => { + const source = + "mcp_servers:\n rea:\n command: rea\n setting: &setting keep\n metadata: {nested: *setting}\nobserver: *setting\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea2", + setting: "changed", + metadata: { nested: "keep" }, + }); + expect(parseHermesConfigurationYaml(out)).toMatchObject({ + observer: "keep", + mcp_servers: { + rea: { setting: "changed", metadata: { nested: "keep" } }, + }, + }); + }); + + it("keeps an inherited stdio transport through a launcher refresh", () => { + const source = + "defaults: &defaults\n transport: stdio\n command: rea\n args: [mcp]\nmcp_servers:\n rea:\n <<: *defaults\n"; + const client = supportedClients("/home/rea-test").find( + ({ name }) => name === "hermes", + )!; + const parsed = parseClientConfiguration(source, "hermes"); + const registration = refreshClientRegistration( + client, + "hermes", + ["rea", "mcp"], + { A: "1" }, + effectiveClientServer(parsed, "rea"), + { + analysisProvider: "auto", + ghidraStartupTimeoutMs: 1, + hopperStartupTimeoutMs: 1, + }, + ); + if (!registration.ok) throw registration.error; + const written = serializeClientConfiguration( + withClientServers(parsed, { ...parsed.servers, rea: registration.value }), + "hermes", + source, + ); + if (!written.ok) throw written.error; + expect(written.value).toContain("<<: *defaults"); + expect(parseHermesConfigurationYaml(written.value)).toMatchObject({ + mcp_servers: { + rea: { transport: "stdio", command: "rea", env: { A: "1" } }, + }, + }); + }); + + it("overrides an inherited registration with only its changed fields", () => { + const source = + "shared: &shared\n rea: {command: rea, metadata: {marker: old}}\nmcp_servers:\n <<: *shared\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + metadata: { marker: "old" }, + env: { A: "1" }, + }); + const changed = out.replace("marker: old", "marker: new"); + expect(parseHermesConfigurationYaml(changed)).toMatchObject({ + mcp_servers: { rea: { metadata: { marker: "new" }, env: { A: "1" } } }, + }); + }); +}); diff --git a/src/application/HermesConfigurationYaml.ts b/src/application/HermesConfigurationYaml.ts index d88437d586..a91a41db91 100644 --- a/src/application/HermesConfigurationYaml.ts +++ b/src/application/HermesConfigurationYaml.ts @@ -3,19 +3,26 @@ import { isMap, isScalar, isSeq, + Pair, parseDocument, Document, Schema, + YAMLMap, + type Node, type ParsedNode, type ScalarTag, type Tags, } from "yaml"; +import { ConfigurationError } from "../domain/configurationErrors.js"; import { preserveYamlAliasValues, copyYamlAliasSource, + resolveYamlEditPath, } from "./YamlConfigurationAliases.js"; import { + materializeUnresolvedYamlAliases, retainYamlConfigurationSource, + yamlAliasValues, retainYamlScalarSource, yamlNumericValuesEqual, } from "./YamlConfigurationSource.js"; @@ -268,6 +275,108 @@ const parseHermesDocument = (text: string) => { return document; }; +/** The value a path resolves to, merges included, or `absent`. */ +const projectedYamlValue = ( + document: Document, + path: readonly string[], +): { readonly value: unknown } | "absent" => { + let current: unknown = document.toJS(); + for (const segment of path) { + if ( + typeof current !== "object" || + current === null || + !Object.hasOwn(current, segment) + ) + return "absent"; + current = (current as Record)[segment]; + } + return { value: current }; +}; + +const projectedYamlPathExists = ( + document: Document, + path: readonly string[], +): boolean => projectedYamlValue(document, path) !== "absent"; + +const inheritedRegistrationRemovalError = () => + new ConfigurationError( + "The registration is inherited from a YAML merge. Remove it at the shared source before rerunning uninstall", + ); + +/** Find the source node selected by YAML merge precedence. */ +const inheritedYamlValue = ( + document: Document, + source: unknown, + name: string, + visiting = new Set(), +): unknown => { + const node = isAlias(source) ? source.resolve(document) : source; + if (!isMap(node) || visiting.has(node)) return undefined; + visiting.add(node); + try { + const matches = node.items.filter((pair) => { + const key = isAlias(pair.key) ? pair.key.resolve(document) : pair.key; + return isScalar(key) && key.value === name; + }); + if (matches.length > 1) + throw new ConfigurationError( + `YAML mapping contains more than one key resolving to ${JSON.stringify(name)}`, + ); + if (matches[0] !== undefined) return matches[0].value; + for (const pair of node.items) { + if ( + !isScalar(pair.key) || + typeof pair.key.value !== "symbol" || + pair.key.source !== "<<" + ) + continue; + const merged = isAlias(pair.value) + ? pair.value.resolve(document) + : pair.value; + const sources = isSeq(merged) ? merged.items : [merged]; + for (const candidate of sources) { + const found = inheritedYamlValue(document, candidate, name, visiting); + if (found !== undefined) return found; + } + } + return undefined; + } finally { + visiting.delete(node); + } +}; + +/** Create a local override that continues to inherit the shared mapping. */ +const inheritedYamlMapOverride = ( + document: Document, + parent: readonly (string | Node)[], + name: string, +): void => { + const owner = + parent.length === 1 + ? document.contents + : document.getIn(parent.slice(0, -1), true); + let inherited = inheritedYamlValue(document, owner, name); + if (isAlias(inherited)) inherited = inherited.resolve(document); + if (!isMap(inherited)) + throw new ConfigurationError( + `Inherited YAML setting ${JSON.stringify(name)} is not a mapping`, + ); + const mergeDocument = parseHermesDocument("<<: {}\n"); + if ( + !isMap(mergeDocument.contents) || + mergeDocument.contents.items[0] === undefined + ) + throw new ConfigurationError("Cannot create a YAML merge key"); + const alias = document.createAlias(inherited, "rea_inherited"); + const override = new YAMLMap(document.schema); + override.items.push(new Pair(mergeDocument.contents.items[0].key, alias)); + document.setIn(parent, override); + if (alias.resolve(document) !== inherited) + throw new ConfigurationError( + "The inherited YAML anchor is shadowed; edit its shared source before rerunning setup", + ); +}; + /** Parse a Hermes `config.yaml`, preserving the distinction between no root and explicit null. */ export const parseHermesConfigurationYaml = (text: string): unknown => { const document = parseHermesDocument(text); @@ -299,7 +408,7 @@ export const editHermesConfigurationYaml = ( document.contents = map; } for (let length = 1; length < path.length; length++) { - const parent = path.slice(0, length); + const parent = resolveYamlEditPath(document, path.slice(0, length)); const previous = document.getIn(parent, true); if (isAlias(previous)) { const copied = copyYamlAliasSource(previous, document); @@ -308,38 +417,74 @@ export const editHermesConfigurationYaml = ( document.setIn(parent, copied); } } else if (value !== undefined && !document.hasIn(parent)) { + if (projectedYamlPathExists(document, path.slice(0, length))) { + inheritedYamlMapOverride(document, parent, path[length - 1]!); + continue; + } // Implicit JS Map parents serialize as !!omap in YAML 1.1. document.setIn(parent, document.createNode({})); } } - const previous = document.getIn(path, true); + // A registration inherited whole through a merge keeps that inheritance: + // override it with the merge, then write only the fields that differ. + const inheritedSourcePath = resolveYamlEditPath(document, path); + // An explicit replacement that drops inherited fields, such as remote + // connection selectors, writes a local registration instead. + const inheritedRegistration = document.hasIn(inheritedSourcePath) + ? "absent" + : projectedYamlValue(document, path); + if ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + inheritedRegistration !== "absent" && + typeof inheritedRegistration.value === "object" && + inheritedRegistration.value !== null && + Object.keys(inheritedRegistration.value).every((key) => + Object.hasOwn(value, key), + ) + ) + inheritedYamlMapOverride(document, inheritedSourcePath, path.at(-1)!); + const sourcePath = resolveYamlEditPath(document, path); + const previous = document.getIn(sourcePath, true); // Deletion removes the pair's key too; replacement retains that key node. const parent = - value === undefined ? document.getIn(path.slice(0, -1), true) : undefined; + value === undefined + ? document.getIn(sourcePath.slice(0, -1), true) + : undefined; + if ( + value === undefined && + !document.hasIn(sourcePath) && + projectedYamlPathExists(document, path) + ) + throw inheritedRegistrationRemovalError(); const removedKey = isMap(parent) - ? parent.items.find( - (pair) => isScalar(pair.key) && pair.key.value === path.at(-1), - )?.key + ? parent.items.find((pair) => pair.key === sourcePath.at(-1))?.key : undefined; + const aliasValues = yamlAliasValues(previous, document); const retainedSources = preserveYamlAliasValues(document, [ previous, removedKey, ]); if (value === undefined) { - if (!document.deleteIn(path)) return text; + if (!document.deleteIn(sourcePath)) return text; + if (projectedYamlPathExists(document, path)) + throw inheritedRegistrationRemovalError(); } else { // Let YAML's scalar update preserve numeric formats for a direct edit. // Replacing containers needs explicit retention of unchanged child nodes. document.setIn( - path, + sourcePath, isMap(previous) || isSeq(previous) ? retainYamlConfigurationSource( previous, document.createNode(value), retainedSources, + document, ) : value, ); + materializeUnresolvedYamlAliases(document, aliasValues); } // Editing REA must not reflow unrelated user prompts or other YAML scalars. return document.toString({ diff --git a/src/application/Uninstall.ts b/src/application/Uninstall.ts index 5cc8a452db..b128764c1e 100644 --- a/src/application/Uninstall.ts +++ b/src/application/Uninstall.ts @@ -1089,11 +1089,13 @@ const removeClient = async ( // clientRegistrationRemoval admitted only names whose repeats REA owns. { removeShadowedMembersAt: editedPaths }, ); - } catch { + } catch (cause: unknown) { return item( client.name, "failed", - "Configuration could not be updated, so no change was made. Repair the configuration, then rerun uninstall.", + cause instanceof ConfigurationError + ? `Configuration could not be updated, so no change was made. ${cause.message}.` + : "Configuration could not be updated, so no change was made. Repair the configuration, then rerun uninstall.", ); } if (!serialized.ok) diff --git a/src/application/YamlConfigurationAliases.ts b/src/application/YamlConfigurationAliases.ts index 9f3b8a8283..1dd01f8786 100644 --- a/src/application/YamlConfigurationAliases.ts +++ b/src/application/YamlConfigurationAliases.ts @@ -9,6 +9,36 @@ import { type Document, type Node, } from "yaml"; +import { ConfigurationError } from "../domain/configurationErrors.js"; + +/** Resolve literal and aliased string keys to their source nodes for editing. */ +export const resolveYamlEditPath = ( + document: Document, + path: readonly string[], +): (string | Node)[] => { + const resolved: (string | Node)[] = []; + let current: unknown = document.contents; + for (const name of path) { + if (isAlias(current)) current = current.resolve(document); + if (!isMap(current)) { + resolved.push(name); + current = undefined; + continue; + } + const matches = current.items.filter((pair) => { + const key = isAlias(pair.key) ? pair.key.resolve(document) : pair.key; + return isScalar(key) && key.value === name; + }); + if (matches.length > 1) + throw new ConfigurationError( + `YAML mapping contains more than one key resolving to ${JSON.stringify(name)}.`, + ); + const pair = matches[0]; + resolved.push(pair !== undefined && isNode(pair.key) ? pair.key : name); + current = pair?.value; + } + return resolved; +}; interface AliasCopies { readonly targets: ReadonlyMap; diff --git a/src/application/YamlConfigurationSource.ts b/src/application/YamlConfigurationSource.ts index 58c127fd67..0483279680 100644 --- a/src/application/YamlConfigurationSource.ts +++ b/src/application/YamlConfigurationSource.ts @@ -1,12 +1,21 @@ import { + isAlias, isMap, isNode, isScalar, isSeq, + Pair, + visit, + YAMLMap, + type Document, type Node, + type Alias, type ScalarTag, type Tags, + type YAMLSeq, } from "yaml"; +import { isDeepStrictEqual } from "node:util"; +import { ConfigurationError } from "../domain/configurationErrors.js"; import { retainYamlSequenceAnnotations } from "./YamlSequenceAnnotations.js"; export const yamlNumericValuesEqual = ( @@ -26,12 +35,99 @@ export const yamlNumericValuesEqual = ( ); }; -/** Keep source annotations and unchanged scalars when updating a value. */ +const normalizeYamlInteger = (_key: unknown, value: unknown): unknown => + typeof value === "bigint" && Number.isSafeInteger(Number(value)) + ? Number(value) + : value; + +const containsRetainedSource = ( + roots: readonly unknown[], + retainedSources: ReadonlySet, +): boolean => { + if (retainedSources.size === 0) return false; + let found = false; + for (const root of roots) { + if (!isNode(root)) continue; + visit(root, { + Value: (_key, node) => { + if (retainedSources.has(node)) found = true; + }, + }); + } + return found; +}; + +/** What each alias inside `root` resolves to before an edit. */ +export interface YamlAliasBinding { + readonly target: Node; + readonly value: unknown; +} + +/** The node and value each alias inside `root` resolves to before an edit. */ +export const yamlAliasValues = ( + root: unknown, + document: Document, +): ReadonlyMap => { + const bindings = new Map(); + if (!isNode(root)) return bindings; + visit(root, { + Alias: (_key, alias) => { + const target = alias.resolve(document); + if (target !== undefined) + bindings.set(alias, { target, value: target.toJS(document) }); + }, + }); + return bindings; +}; + +/** + * Replace each alias that no longer resolves to its original node, whether + * its anchor is gone or an earlier same-name anchor now captures it, with the + * value it resolved to before the edit. Its comments and spacing are kept. + */ +export const materializeUnresolvedYamlAliases = ( + document: Document, + bindings: ReadonlyMap, +): void => { + if (bindings.size === 0) return; + visit(document, { + Alias: (_key, alias) => { + const binding = bindings.get(alias); + if (binding === undefined || alias.resolve(document) === binding.target) + return undefined; + const replacement = document.createNode(binding.value); + if (alias.comment !== undefined) replacement.comment = alias.comment; + if (alias.commentBefore !== undefined) + replacement.commentBefore = alias.commentBefore; + if (alias.spaceBefore !== undefined) + replacement.spaceBefore = alias.spaceBefore; + return replacement; + }, + }); +}; + +/** Keep source nodes and annotations when updating a value. */ export const retainYamlConfigurationSource = ( previous: unknown, next: Node, retainedSources: ReadonlySet, + document: Document, ): Node => { + if ( + (isMap(previous) || isSeq(previous)) && + ((isMap(previous) && isMap(next)) || (isSeq(previous) && isSeq(next))) + ) { + // Object projection can collapse distinct YAML keys. Reuse an unchanged + // collection before reconstructing its children from that projection. + if ( + !containsRetainedSource([previous], retainedSources) && + isDeepStrictEqual( + previous.toJS(document, { reviver: normalizeYamlInteger }), + next.toJS(document, { reviver: normalizeYamlInteger }), + ) + ) + return previous; + } if ( isScalar(previous) && isScalar(next) && @@ -60,53 +156,273 @@ export const retainYamlConfigurationSource = ( next.spaceBefore = previous.spaceBefore; } if (isMap(previous) && isMap(next)) { - const entries = new Map( - previous.items.flatMap((pair) => - isScalar(pair.key) && typeof pair.key.value === "string" - ? [[pair.key.value, pair] as const] - : [], - ), - ); - for (const pair of next.items) { - const key = isScalar(pair.key) ? pair.key.value : undefined; - const prior = typeof key === "string" ? entries.get(key) : undefined; - if (prior === undefined) continue; - pair.key = isNode(pair.key) - ? retainYamlConfigurationSource(prior.key, pair.key, retainedSources) - : prior.key; - if (isNode(pair.value)) - pair.value = retainYamlConfigurationSource( - prior.value, - pair.value, - retainedSources, - ); - } + retainYamlMapSource(previous, next, retainedSources, document); } else if (isSeq(previous) && isSeq(next)) { - const stringItems = (items: readonly unknown[]) => - items.every((item) => isScalar(item) && typeof item.value === "string"); - if (stringItems(previous.items) && stringItems(next.items)) - retainYamlSequenceAnnotations( - previous, - next, - (item) => (isScalar(item) ? item.value : undefined), - (source, target) => - retainYamlConfigurationSource(source, target, retainedSources), - (source) => !retainedSources.has(source), - ); - else - next.items = next.items.map((item, index) => - isNode(item) - ? retainYamlConfigurationSource( - previous.items[index], - item, - retainedSources, - ) - : item, - ); + retainYamlSequenceSource(previous, next, retainedSources, document); } return next; }; +const retainYamlSequenceSource = ( + previous: YAMLSeq, + next: YAMLSeq, + retainedSources: ReadonlySet, + document: Document, +): void => { + const stringItems = (items: readonly unknown[]) => + items.every((item) => isScalar(item) && typeof item.value === "string"); + if (stringItems(previous.items) && stringItems(next.items)) { + retainYamlSequenceAnnotations( + previous, + next, + (item) => (isScalar(item) ? item.value : undefined), + (source, target) => + retainYamlConfigurationSource( + source, + target, + retainedSources, + document, + ), + (source) => !retainedSources.has(source), + ); + return; + } + next.items = next.items.map((item, index) => + isNode(item) + ? retainYamlConfigurationSource( + previous.items[index], + item, + retainedSources, + document, + ) + : item, + ); +}; + +/** + * Each pair's key as the string YAML resolves it to. A key written as an alias + * of a string scalar counts as that string only when no other pair in the map + * resolves to the same string; an ambiguous one stays a source-only key. + */ +const resolvedStringKeys = ( + map: YAMLMap, + document: Document, +): Map<(typeof map.items)[number], string> => { + const resolved = new Map<(typeof map.items)[number], string>(); + const aliased = new Map<(typeof map.items)[number], string>(); + for (const pair of map.items) { + if (isScalar(pair.key) && typeof pair.key.value === "string") + resolved.set(pair, pair.key.value); + else if (isAlias(pair.key)) { + const target = pair.key.resolve(document); + if (isScalar(target) && typeof target.value === "string") + aliased.set(pair, target.value); + } + } + const counts = new Map(); + for (const key of [...resolved.values(), ...aliased.values()]) + counts.set(key, (counts.get(key) ?? 0) + 1); + for (const [pair, key] of aliased) + if (counts.get(key) === 1) resolved.set(pair, key); + return resolved; +}; + +const retainYamlMapSource = ( + previous: YAMLMap, + next: YAMLMap, + retainedSources: ReadonlySet, + document: Document, +): void => { + const hasMerges = retainYamlMergeSources(previous, next, document); + const hasSourceOnlyKeys = retainProjectedYamlKeys( + previous, + next, + retainedSources, + document, + ); + const previousKeys = resolvedStringKeys(previous, document); + // The source pair each rewritten pair replaces, including aliased keys. + const matched = new Map< + (typeof next.items)[number], + (typeof previous.items)[number] + >(); + const entries = new Map( + [...previousKeys].map(([pair, key]) => [key, pair] as const), + ); + for (const pair of next.items) { + if (previous.items.includes(pair)) continue; + const key = isScalar(pair.key) ? pair.key.value : undefined; + const prior = typeof key === "string" ? entries.get(key) : undefined; + if (prior === undefined) continue; + matched.set(pair, prior); + // An aliased key keeps its spelling; a later repair materializes it if + // the edit leaves it without an anchor. + pair.key = isAlias(prior.key) + ? prior.key + : isNode(pair.key) + ? retainYamlConfigurationSource( + prior.key, + pair.key, + retainedSources, + document, + ) + : prior.key; + if (isNode(pair.value)) + pair.value = retainYamlConfigurationSource( + prior.value, + pair.value, + retainedSources, + document, + ); + } + if (hasSourceOnlyKeys || hasMerges) { + // Object property ordering moves integer-like keys ahead of their + // source neighbors. Restore source order so aliases stay after anchors. + const positions = new Map( + previous.items.map((pair, index) => [pair, index] as const), + ); + const position = (pair: (typeof next.items)[number]): number => { + const key = isScalar(pair.key) ? pair.key.value : undefined; + const original = positions.has(pair) + ? pair + : (matched.get(pair) ?? + (typeof key === "string" ? entries.get(key) : undefined)); + return original === undefined + ? Infinity + : (positions.get(original) ?? Infinity); + }; + next.items.sort((left, right) => position(left) - position(right)); + } +}; + +const isYamlMergeKey = (key: unknown): boolean => + isScalar(key) && typeof key.value === "symbol" && key.source === "<<"; + +/** Retain inheritance while materializing only explicit changes to it. */ +const retainYamlMergeSources = ( + previous: YAMLMap, + next: YAMLMap, + document: Document, +): boolean => { + const merges = previous.items.filter((pair) => isYamlMergeKey(pair.key)); + if (merges.length === 0) return false; + const inherited = new YAMLMap(document.schema); + inherited.items = merges; + const direct = new YAMLMap(document.schema); + direct.items = previous.items.filter((pair) => !isYamlMergeKey(pair.key)); + const project = (map: YAMLMap): Record => + map.toJS(document, { reviver: normalizeYamlInteger }) as Record< + string, + unknown + >; + const inheritedValues = project(inherited); + const directValues = project(direct); + const desired = project(next); + const inheritedUnchanged = new Set(); + for (const [key, value] of Object.entries(inheritedValues)) { + // Removing a key the merge supplies would resurrect it, even when the + // mapping also overrides it locally. + if (!Object.hasOwn(desired, key)) + throw new ConfigurationError( + `Cannot remove inherited YAML setting ${JSON.stringify(key)} while retaining its merge.`, + ); + if (Object.hasOwn(directValues, key)) continue; + if (isDeepStrictEqual(value, desired[key])) inheritedUnchanged.add(key); + } + next.items = [ + ...merges, + ...next.items.filter((pair) => { + const key = isScalar(pair.key) ? pair.key.value : undefined; + return typeof key !== "string" || !inheritedUnchanged.has(key); + }), + ]; + return true; +}; + +const retainProjectedYamlKeys = ( + previous: YAMLMap, + next: YAMLMap, + retainedSources: ReadonlySet, + document: Document, +): boolean => { + // A JS property can represent several distinct native YAML keys. Keep + // those source pairs together when the property was not edited. + const groups = new Map(); + const sourceOnlyKeys = new Set(); + const stringKeys = resolvedStringKeys(previous, document); + for (const pair of previous.items) { + if (isYamlMergeKey(pair.key)) continue; + let key: string; + const stringKey = stringKeys.get(pair); + if (stringKey !== undefined) key = stringKey; + else { + const singleton = new YAMLMap(document.schema); + singleton.items.push(new Pair(pair.key, null)); + const projected: unknown = singleton.toJS(document); + if (typeof projected !== "object" || projected === null) + throw new ConfigurationError( + "YAML mapping key could not be resolved safely", + ); + const [projectedKey] = Object.keys(projected); + if (projectedKey === undefined || Object.keys(projected).length !== 1) + throw new ConfigurationError( + "YAML mapping key could not be resolved safely", + ); + key = projectedKey; + sourceOnlyKeys.add(key); + } + const group = groups.get(key) ?? []; + group.push(pair); + groups.set(key, group); + } + if (sourceOnlyKeys.size > 0) { + const oldValue: unknown = previous.toJS(document, { + reviver: normalizeYamlInteger, + }); + const newValue: unknown = next.toJS(document, { + reviver: normalizeYamlInteger, + }); + if ( + typeof oldValue !== "object" || + oldValue === null || + typeof newValue !== "object" || + newValue === null + ) + throw new ConfigurationError("YAML mapping could not be compared safely"); + const oldProperties = oldValue as Record; + const newProperties = newValue as Record; + const replacements = new Map(); + for (const key of sourceOnlyKeys) { + const group = groups.get(key); + if (group === undefined) continue; + if ( + !Object.hasOwn(newProperties, key) || + !isDeepStrictEqual(oldProperties[key], newProperties[key]) + ) + throw new ConfigurationError( + `Cannot safely change YAML mapping key ${JSON.stringify(key)} because its source identity is not a plain string key.`, + ); + if ( + containsRetainedSource( + group.flatMap((pair) => [pair.key, pair.value]), + retainedSources, + ) + ) + throw new ConfigurationError( + `Cannot safely preserve YAML mapping key ${JSON.stringify(key)} while its anchor is moved.`, + ); + replacements.set(key, group); + } + next.items = next.items.flatMap((pair) => { + const key = isScalar(pair.key) ? pair.key.value : undefined; + const replacement = + typeof key === "string" ? replacements.get(key) : undefined; + return replacement ?? [pair]; + }); + } + return sourceOnlyKeys.size > 0; +}; + /** Keep a source spelling only while the selected schema resolves its value unchanged. */ const sameResolvedScalarValue = (left: unknown, right: unknown): boolean => Object.is(left, right) || diff --git a/src/application/analysisView/AnalysisViewService.test.ts b/src/application/analysisView/AnalysisViewService.test.ts index e60bf12000..ca94588e65 100644 --- a/src/application/analysisView/AnalysisViewService.test.ts +++ b/src/application/analysisView/AnalysisViewService.test.ts @@ -21,7 +21,7 @@ import { parseEvidence, rebindEvidenceToTargetPath, } from "../../domain/evidence.js"; -import { inspectAnalysisViewInputSchema } from "../../domain/analysisView/analysisView.js"; +import { inspectAnalysisViewInputSchema } from "../../domain/analysisView/analysisViewRequest.js"; import { functionDossierSchema } from "../../domain/native/functionDossier.js"; import { ghidraFunctionDossier } from "../../domain/ghidraValues.fixture.js"; import { diff --git a/src/application/analysisView/AnalysisViewService.ts b/src/application/analysisView/AnalysisViewService.ts index c84b91a885..acdb51d639 100644 --- a/src/application/analysisView/AnalysisViewService.ts +++ b/src/application/analysisView/AnalysisViewService.ts @@ -28,11 +28,11 @@ import { } from "../../domain/evidence.js"; import { inspectAnalysisViewInputSchema, - prepareAnalysisViewProjection, - type AnalysisViewResult, type AnalysisViewRequest, type InspectAnalysisViewInput, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewRequest.js"; +import { prepareAnalysisViewProjection } from "../../domain/analysisView/analysisView.js"; +import type { AnalysisViewResult } from "../../domain/analysisView/analysisViewResult.js"; import { immutableJsonSnapshotMember, isImmutableJsonSnapshot, diff --git a/src/application/analysisView/BrowserScenarioViews.test.ts b/src/application/analysisView/BrowserScenarioViews.test.ts index 82b2c3f9a3..26e2a27880 100644 --- a/src/application/analysisView/BrowserScenarioViews.test.ts +++ b/src/application/analysisView/BrowserScenarioViews.test.ts @@ -15,7 +15,7 @@ import { inspectAnalysisView } from "./AnalysisViewService.js"; import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; import { sanitizeBrowserUrl } from "../../domain/browserObservation.js"; import type { BrowserScenarioCapture } from "../../domain/browserScenarioCapture.js"; -import type { AnalysisViewRequest } from "../../domain/analysisView/analysisView.js"; +import type { AnalysisViewRequest } from "../../domain/analysisView/analysisViewRequest.js"; import { projectBrowserScenarioView } from "../../domain/analysisView/browserScenarioView.js"; const tree = (text: string) => ({ diff --git a/src/application/analysisView/JavaScriptProvenanceViews.test.ts b/src/application/analysisView/JavaScriptProvenanceViews.test.ts index 194dc28a90..ff6853e657 100644 --- a/src/application/analysisView/JavaScriptProvenanceViews.test.ts +++ b/src/application/analysisView/JavaScriptProvenanceViews.test.ts @@ -6,7 +6,7 @@ import { analysisViewJavaScriptEvidence, } from "../../../tests/fixtures/analysisView.js"; import { SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE } from "../../contracts/javascript/javascriptApplicationWorkflowExamples.js"; -import { analysisViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; import type { Evidence } from "../../domain/evidence.js"; import { jsonValueValidationIssue } from "../../domain/jsonValue.js"; import { compareSourceToBundle } from "../../domain/javascript/sourceToBundleComparison.js"; diff --git a/src/application/analysisView/NativeLoadImageViews.test.ts b/src/application/analysisView/NativeLoadImageViews.test.ts index 53563d78ea..cf8643147c 100644 --- a/src/application/analysisView/NativeLoadImageViews.test.ts +++ b/src/application/analysisView/NativeLoadImageViews.test.ts @@ -1,9 +1,7 @@ import { expect, it } from "vitest"; import { createEvidence } from "../../domain/evidence.js"; -import { - analysisViewResultSchema, - inspectAnalysisViewInputSchema, -} from "../../domain/analysisView/analysisView.js"; +import { inspectAnalysisViewInputSchema } from "../../domain/analysisView/analysisViewRequest.js"; +import { analysisViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; import { RELOCATION_SYMBOL_UNRECORDED, projectNativeLoadImageView, diff --git a/src/application/analysisView/NativeObservationViews.test.ts b/src/application/analysisView/NativeObservationViews.test.ts index 7673a47b33..c34ade9b59 100644 --- a/src/application/analysisView/NativeObservationViews.test.ts +++ b/src/application/analysisView/NativeObservationViews.test.ts @@ -13,11 +13,11 @@ import { nativeCallGraphSchema, type NativeCallGraph, } from "../../domain/native/nativeCallGraph.js"; +import type { AnalysisViewRequest } from "../../domain/analysisView/analysisViewRequest.js"; import { analysisViewResultSchema, - type AnalysisViewRequest, type AnalysisViewResult, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import { inspectAnalysisView, projectRetainedAnalysis, diff --git a/src/application/artifacts/ArtifactComparisonEvidence.test.ts b/src/application/artifacts/ArtifactComparisonEvidence.test.ts index 5410cbff0f..9c0d7f16a4 100644 --- a/src/application/artifacts/ArtifactComparisonEvidence.test.ts +++ b/src/application/artifacts/ArtifactComparisonEvidence.test.ts @@ -2,10 +2,8 @@ import { expect, it } from "vitest"; import { inspectAnalysisView } from "../analysisView/AnalysisViewService.js"; import { ARTIFACT_COMPARISON_EXAMPLE } from "../../contracts/artifactComparisonExample.js"; -import { - artifactInventorySummaryViewResultSchema, - sealAnalysisView, -} from "../../domain/analysisView/analysisView.js"; +import { sealAnalysisView } from "../../domain/analysisView/analysisView.js"; +import { artifactInventorySummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; import { createEvidence, parseEvidence, diff --git a/src/application/artifacts/ArtifactComparisonEvidence.ts b/src/application/artifacts/ArtifactComparisonEvidence.ts index 94d2b7f7c4..dd3d4c727e 100644 --- a/src/application/artifacts/ArtifactComparisonEvidence.ts +++ b/src/application/artifacts/ArtifactComparisonEvidence.ts @@ -9,7 +9,7 @@ import { inspectAnalysisViewValidated } from "../analysisView/AnalysisViewServic import type { EvidenceFileReadOptions } from "../analysisView/evidenceFileSource.js"; import { artifactComparisonInputSchema } from "../../contracts/artifactComparisonContracts.js"; import { parseAnalysisInput } from "../AnalysisInput.js"; -import { artifactInventorySummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { artifactInventorySummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; import { compareArtifacts, type ArtifactComparisonResult, diff --git a/src/application/binary/BinarySession.ts b/src/application/binary/BinarySession.ts index 05199df91e..2c50ef3ec4 100644 --- a/src/application/binary/BinarySession.ts +++ b/src/application/binary/BinarySession.ts @@ -7,7 +7,10 @@ import { } from "../EvidenceBundleFiles.js"; import type { ResetInvestigationReceipt } from "../../domain/investigationLifecycle.js"; -import { retainSavedAnalysisSnapshot } from "../../domain/savedAnalysisSnapshot.js"; +import { + retainSavedAnalysisSnapshot, + type SavedAnalysisSnapshot, +} from "../../domain/savedAnalysisSnapshot.js"; import type { BinaryTarget } from "../../domain/binaryTargetTypes.js"; import { @@ -39,7 +42,6 @@ import type { BinarySessionPort, BinaryOpenResult, OpenSelectionReceipt, - SavedAnalysisSnapshot, } from "./BinarySessionPort.js"; import { binaryTargetSummary } from "./BinaryTargetSummary.js"; import { diff --git a/src/application/binary/BinarySessionPort.ts b/src/application/binary/BinarySessionPort.ts index 0cff1a77d2..b96a4ba8a9 100644 --- a/src/application/binary/BinarySessionPort.ts +++ b/src/application/binary/BinarySessionPort.ts @@ -30,7 +30,6 @@ import type { } from "../AnalysisProvider.js"; import type { SavedAnalysisSnapshot } from "../../domain/savedAnalysisSnapshot.js"; -export type { SavedAnalysisSnapshot } from "../../domain/savedAnalysisSnapshot.js"; /** Selection observed at the end of one serialized open attempt. */ export type OpenSelectionReceipt = diff --git a/src/application/desktop/DesktopBuildCapture.ts b/src/application/desktop/DesktopBuildCapture.ts index c3652f4fed..668511c6ad 100644 --- a/src/application/desktop/DesktopBuildCapture.ts +++ b/src/application/desktop/DesktopBuildCapture.ts @@ -28,7 +28,11 @@ import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; import { err, ok, type Result } from "../../domain/result.js"; import { AGENTENV_PROVIDER } from "../../desktop/AgentEnvComputerHelper.js"; import type { AgentEnvComputer } from "../../desktop/AgentEnvComputer.js"; -import type { ControlledComputer } from "../../desktop/BuildComputerController.js"; +import { + controller, + type Answer, + type ControlledComputer, +} from "../../desktop/BuildComputerController.js"; import { BuildComputerLongRuns } from "../../desktop/BuildComputerLongRuns.js"; import { endedCapture, @@ -37,12 +41,10 @@ import { } from "./DesktopBuildCaptureFailure.js"; import { adapterError, - controller, errorText, refused, runRea, WORK, - type Answer, } from "./DesktopBuildCommands.js"; import { exportBuild, diff --git a/src/application/desktop/DesktopBuildCaptureFailure.ts b/src/application/desktop/DesktopBuildCaptureFailure.ts index 4358aba483..e9fe9f0d10 100644 --- a/src/application/desktop/DesktopBuildCaptureFailure.ts +++ b/src/application/desktop/DesktopBuildCaptureFailure.ts @@ -7,12 +7,14 @@ import { } from "../../domain/desktop/desktopCapture.js"; import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; import { AGENTENV_PROVIDER } from "../../desktop/AgentEnvComputerHelper.js"; -import type { ControlledComputer } from "../../desktop/BuildComputerController.js"; +import type { + Answer, + ControlledComputer, +} from "../../desktop/BuildComputerController.js"; import { adapterError, readOutput, reportedFailure, - type Answer, } from "./DesktopBuildCommands.js"; /** The most lines, and characters, a message quotes from the end of the app's stderr. */ diff --git a/src/application/desktop/DesktopBuildCommands.ts b/src/application/desktop/DesktopBuildCommands.ts index ac4fb4f4e2..175670b6a1 100644 --- a/src/application/desktop/DesktopBuildCommands.ts +++ b/src/application/desktop/DesktopBuildCommands.ts @@ -22,11 +22,6 @@ import { type ControlledComputer, } from "../../desktop/BuildComputerController.js"; -export { - controller, - type Answer, -} from "../../desktop/BuildComputerController.js"; - /** The work folder REA keeps under the computer user's home; every capture leaves it out. */ export const WORK = ".rea-build"; /** The checkpoint options the draft and the export read the seed and the end with. */ diff --git a/src/application/environment/environmentSource.ts b/src/application/environment/environmentSource.ts index 4c56cd6382..3299a12347 100644 --- a/src/application/environment/environmentSource.ts +++ b/src/application/environment/environmentSource.ts @@ -8,7 +8,7 @@ import { import { analysisViewResultSchema, type AnalysisViewResult, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import { scenarioCaptureRecords } from "../../domain/analysisView/scenarioCaptureRecords.js"; import { readRecordedResponseBody, diff --git a/src/application/environment/environmentState.ts b/src/application/environment/environmentState.ts index af5ed7d782..cac8541a75 100644 --- a/src/application/environment/environmentState.ts +++ b/src/application/environment/environmentState.ts @@ -1,4 +1,4 @@ -import type { AnalysisViewResult } from "../../domain/analysisView/analysisView.js"; +import type { AnalysisViewResult } from "../../domain/analysisView/analysisViewResult.js"; import { changesState, type StateDocument, diff --git a/src/application/native/NativeFunctionMatchingViews.test.ts b/src/application/native/NativeFunctionMatchingViews.test.ts index b80dc719ad..1f306908ea 100644 --- a/src/application/native/NativeFunctionMatchingViews.test.ts +++ b/src/application/native/NativeFunctionMatchingViews.test.ts @@ -1,7 +1,5 @@ -import { - projectAnalysisView, - analysisViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +import { projectAnalysisView } from "../../domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; import { nativeFunctionMatchingSchema } from "../../domain/native/functionMatching.js"; import { expect, it } from "vitest"; import { matchNativeFunctions } from "./NativeFunctionMatchingService.js"; diff --git a/src/artifacts/StableArtifactFailure.ts b/src/artifacts/StableArtifactFailure.ts index a5b9615443..5977ac8019 100644 --- a/src/artifacts/StableArtifactFailure.ts +++ b/src/artifacts/StableArtifactFailure.ts @@ -3,14 +3,43 @@ import type { AnalysisError } from "../domain/analysisErrorBase.js"; import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; import { ArtifactReaderFailure } from "./ArtifactReader.js"; -import { bindArtifactFileCleanup } from "./ArtifactFileOperation.js"; +import { + artifactFileCancellationCause, + bindArtifactFileCleanup, + type ArtifactFilePrimaryFacts, +} from "./ArtifactFileOperation.js"; import { completedStableArtifactObservation } from "./StableArtifactObservation.js"; import { isSelectedRegularFileError } from "../filesystem/RegularFile.js"; import { AnalysisAccessDeniedError, + AnalysisArtifactChangedError, + AnalysisCancelledError, AnalysisInputError, } from "../domain/analysisErrorCore.js"; +/** Preserve ownership classification for stable-file identity and cancellation. */ +export const projectOwnedArtifactIdentityFailure = ( + facts: ArtifactFilePrimaryFacts, + operation: string, + path: string, +): AnalysisError | undefined => { + const { cause, origin } = facts; + if ( + origin !== "owned-or-classified" || + !(cause instanceof ArtifactReaderFailure) + ) + return undefined; + if (cause.reason === "integrity") + return new AnalysisArtifactChangedError(operation, path, cause.message, { + cause, + }); + if (cause.reason === "cancelled") + return new AnalysisCancelledError(operation, { + cause: artifactFileCancellationCause(cause), + }); + return undefined; +}; + /** Project filesystem errors that identify a refused or unreadable selected file. */ export const projectSelectedArtifactFileFailure = ( cause: unknown, @@ -57,6 +86,18 @@ export const stableArtifactInputRecovery = (cause: ArtifactReaderFailure) => } : {}; +/** Report a rejected artifact path or format with its source-provided recovery. */ +export const stableArtifactInputFailure = ( + cause: ArtifactReaderFailure, + operation: string, + field = "path", +): AnalysisInputError => + new AnalysisInputError( + operation, + { cause, ...stableArtifactInputRecovery(cause) }, + [{ path: [field], reason: "invalid_format", message: cause.message }], + ); + const artifactCleanupFailure = ( cause: unknown, providerId: string, diff --git a/src/artifacts/createArtifactReader.ts b/src/artifacts/createArtifactReader.ts index 3cb90b7602..739e489e8c 100644 --- a/src/artifacts/createArtifactReader.ts +++ b/src/artifacts/createArtifactReader.ts @@ -5,8 +5,6 @@ import { type ArtifactReaderConstruction, } from "./artifactFormats.js"; -export type { ArtifactReaderConstruction } from "./artifactFormats.js"; - /** Formats with an artifact entry reader; availability policy belongs to the caller. */ export type ArtifactReaderFormat = { [Id in keyof typeof ARTIFACT_FORMATS]: (typeof ARTIFACT_FORMATS)[Id] extends { diff --git a/src/browser/history/playwrightTrace/TraceFailure.ts b/src/browser/history/playwrightTrace/TraceFailure.ts index 0cd225ca5e..54ce863b7f 100644 --- a/src/browser/history/playwrightTrace/TraceFailure.ts +++ b/src/browser/history/playwrightTrace/TraceFailure.ts @@ -11,7 +11,7 @@ import { PLAYWRIGHT_TRACE_OPERATION as operation } from "../../../domain/playwri import { ArtifactReaderFailure } from "../../../artifacts/ArtifactReader.js"; import { artifactFileCancellationCause } from "../../../artifacts/ArtifactFileOperation.js"; import { isSelectedRegularFileError } from "../../../filesystem/RegularFile.js"; -import { stableArtifactInputRecovery } from "../../../artifacts/StableArtifactFailure.js"; +import { stableArtifactInputFailure } from "../../../artifacts/StableArtifactFailure.js"; /** Classify the archive owner's facts without interpreting opaque cancellation reasons. */ export const traceFailure = (cause: unknown, path: string): AnalysisError => { @@ -38,11 +38,7 @@ export const traceFailure = (cause: unknown, path: string): AnalysisError => { }, ); if (cause.reason === "format" || cause.reason === "path") - return new AnalysisInputError( - operation, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, operation); return new ArtifactOperationError( operation, cause.reason, diff --git a/src/cli/analysisEvidenceDelivery.ts b/src/cli/analysisEvidenceDelivery.ts index 5b637d8c80..9d85c6c7c8 100644 --- a/src/cli/analysisEvidenceDelivery.ts +++ b/src/cli/analysisEvidenceDelivery.ts @@ -3,7 +3,7 @@ import { analysisEvidenceSources, undeliveredSourceClosure, } from "../application/AnalysisEvidenceSources.js"; -import type { AnalysisViewRequest } from "../domain/analysisView/analysisView.js"; +import type { AnalysisViewRequest } from "../domain/analysisView/analysisViewRequest.js"; import { z } from "incur"; import { isImmutableEvidence, diff --git a/src/cli/javascriptApplicationAnalysis.ts b/src/cli/javascriptApplicationAnalysis.ts index d26bdbabbb..5f444855c6 100644 --- a/src/cli/javascriptApplicationAnalysis.ts +++ b/src/cli/javascriptApplicationAnalysis.ts @@ -1,4 +1,4 @@ -import type { AnalysisViewRequest } from "../domain/analysisView/analysisView.js"; +import type { AnalysisViewRequest } from "../domain/analysisView/analysisViewRequest.js"; import { publishCliAnalysisEvidence } from "./analysisEvidenceDelivery.js"; import { JAVASCRIPT_APPLICATION_PROVIDER } from "../domain/investigationProviders.js"; import { createProgressReporter } from "../application/ProgressReporter.js"; diff --git a/src/composition/binaryDiagnostics.ts b/src/composition/binaryDiagnostics.ts index 381789750a..8ee7221c46 100644 --- a/src/composition/binaryDiagnostics.ts +++ b/src/composition/binaryDiagnostics.ts @@ -7,7 +7,6 @@ import { RecordedCrashService } from "../application/binaryDiagnostics/RecordedC import { PwntoolsRecordedCrashProvider } from "../native/pwntools/PwntoolsRecordedCrashProvider.js"; import { BinaryLayoutService } from "../application/binaryDiagnostics/BinaryLayoutService.js"; import { PwntoolsLayoutProvider } from "../native/pwntools/PwntoolsLayoutProvider.js"; -import { pwntoolsHostSupported } from "../native/pwntools/PwntoolsDecoder.js"; import { PeResourcesService } from "../application/binaryDiagnostics/PeResourcesService.js"; import { PeResourcesProvider } from "../native/pe/PeResourcesProvider.js"; import { PeCertificatesService } from "../application/binaryDiagnostics/PeCertificatesService.js"; @@ -37,8 +36,6 @@ import { DevicetreeProvider } from "../firmware/devicetree/DevicetreeProvider.js export const createDevicetreeService = (): DevicetreeService => new DevicetreeService(new DevicetreeProvider()); -export { pwntoolsHostSupported }; - /** Portable PE inspection with no engine, installation or target acquisition. */ export const createPeResourcesService = (): PeResourcesService => new PeResourcesService(new PeResourcesProvider()); diff --git a/src/composition/capabilityServices.ts b/src/composition/capabilityServices.ts index ae5e5cd80d..205f281f16 100644 --- a/src/composition/capabilityServices.ts +++ b/src/composition/capabilityServices.ts @@ -12,18 +12,17 @@ import { } from "./capabilityDeclarations.js"; import { isAbsolute } from "node:path"; import { createGpuCodeObjectsService } from "./gpu.js"; -import { createPacketCaptureService, tsharkHostSupported } from "./captures.js"; -import { - createSignalRecordingService, - sigrokHostSupported, -} from "./signals.js"; +import { createPacketCaptureService } from "./captures.js"; +import { tsharkHostSupported } from "../captures/TsharkCaptureProvider.js"; +import { createSignalRecordingService } from "./signals.js"; +import { sigrokHostSupported } from "../signals/SigrokRecordingProvider.js"; import { wasmArtifactModule } from "./wasm.js"; import { apkSignatureModule } from "./apkSignature.js"; +import { evmInterfaceModule } from "./evm.js"; import { - evmInterfaceModule, evmHostSupported, evmLimiterAvailability, -} from "./evm.js"; +} from "../evm/EvmoleInterfaceProvider.js"; import { createSqliteDatabaseService } from "./sqlite.js"; import { createDesktopApplicationService } from "./desktop.js"; import { DesktopBuildComputers } from "../application/desktop/DesktopBuildComputers.js"; @@ -40,10 +39,8 @@ import { createWebNetworkCaptureService } from "./webNetworkCaptures.js"; import { createWebSourceLocationService } from "./webSourceLocations.js"; import { createWebModuleTraceService } from "./webModules.js"; import { createWebRuntimeService } from "./webRuntime.js"; -import { - createJavaScriptRecoveryProvider, - wakaruHostSupported, -} from "./javascriptRecovery.js"; +import { createJavaScriptRecoveryProvider } from "./javascriptRecovery.js"; +import { wakaruHostSupported } from "../javascript/recovery/WakaruCommand.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { createReverseEngineeringService } from "./reverseEngineering.js"; import type { @@ -51,10 +48,8 @@ import type { CommandProviderLaunchFailures, } from "../application/reverse/ReverseEngineeringService.js"; import { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysisService.js"; -import { - createFirmwareAnalysisProvider, - firmwareHostSupported, -} from "./firmware.js"; +import { createFirmwareAnalysisProvider } from "./firmware.js"; +import { firmwareHostSupported } from "../firmware/FirmwareCommand.js"; import { createAndroidAnalysisProvider } from "./android.js"; import { createPcapAnalysisProvider } from "./pcap.js"; import { createFlutterBuildAnalysisProvider } from "./flutter.js"; @@ -79,7 +74,6 @@ import { createWindowsShortcutService, createRecordedCrashService, createBinaryLayoutService, - pwntoolsHostSupported, createDevicetreeService, createElfNotesService, createNativeInitializersService, @@ -88,6 +82,7 @@ import { createKernelModuleService, createKernelBuildConfigService, } from "./binaryDiagnostics.js"; +import { pwntoolsHostSupported } from "../native/pwntools/PwntoolsDecoder.js"; import type { PacketCaptureService } from "../application/captures/PacketCaptureService.js"; import type { SignalRecordingService } from "../application/signals/SignalRecordingService.js"; diff --git a/src/composition/captures.ts b/src/composition/captures.ts index ab033cfa3d..b8f41a0ef5 100644 --- a/src/composition/captures.ts +++ b/src/composition/captures.ts @@ -1,10 +1,5 @@ import { PacketCaptureService } from "../application/captures/PacketCaptureService.js"; -import { - TsharkCaptureProvider, - tsharkHostSupported, -} from "../captures/TsharkCaptureProvider.js"; - -export { tsharkHostSupported }; +import { TsharkCaptureProvider } from "../captures/TsharkCaptureProvider.js"; export const createPacketCaptureService = ( environment: Readonly = process.env, diff --git a/src/composition/evm.ts b/src/composition/evm.ts index 853cd73b02..f51903a0f4 100644 --- a/src/composition/evm.ts +++ b/src/composition/evm.ts @@ -1,9 +1,5 @@ import { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js"; -import { - EvmoleInterfaceProvider, - evmHostSupported, - evmLimiterAvailability, -} from "../evm/EvmoleInterfaceProvider.js"; +import { EvmoleInterfaceProvider } from "../evm/EvmoleInterfaceProvider.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; import { INSPECT_EVM_INTERFACE_CONTRACT } from "../contracts/evm/evmToolContracts.js"; @@ -13,8 +9,6 @@ export const createEvmInterfaceService = ( ): EvmInterfaceService => new EvmInterfaceService(new EvmoleInterfaceProvider(environment)); -export { evmHostSupported, evmLimiterAvailability }; - /** * Static binding of the existing operation, command and fresh service factory. * The host owns injected service identity and its retryable cleanup lifetime. diff --git a/src/composition/firmware.ts b/src/composition/firmware.ts index 78e16d9176..f3bed02938 100644 --- a/src/composition/firmware.ts +++ b/src/composition/firmware.ts @@ -1,12 +1,7 @@ import type { FirmwareAnalysisPort } from "../application/firmware/FirmwareAnalysisPort.js"; -import { - firmwareHostSupported, - type FirmwareLauncher, -} from "../firmware/FirmwareCommand.js"; +import type { FirmwareLauncher } from "../firmware/FirmwareCommand.js"; import { FirmwareProvider } from "../firmware/FirmwareProvider.js"; -export { firmwareHostSupported }; - /** Construct one firmware provider without launching or acquiring an engine. */ export const createFirmwareAnalysisProvider = ( environment: Readonly> = process.env, diff --git a/src/composition/javascriptRecovery.ts b/src/composition/javascriptRecovery.ts index f57c984d6f..f3f2ae49d4 100644 --- a/src/composition/javascriptRecovery.ts +++ b/src/composition/javascriptRecovery.ts @@ -1,11 +1,6 @@ import type { JavaScriptRecoveryPort } from "../application/javascript/JavaScriptRecoveryPort.js"; import { WakaruProvider } from "../javascript/recovery/WakaruProvider.js"; -import { - wakaruHostSupported, - type WakaruLauncher, -} from "../javascript/recovery/WakaruCommand.js"; - -export { wakaruHostSupported }; +import type { WakaruLauncher } from "../javascript/recovery/WakaruCommand.js"; /** Construct optional recovery without acquiring tools, processes or workspaces. */ export const createJavaScriptRecoveryProvider = ( diff --git a/src/composition/signals.ts b/src/composition/signals.ts index d21395389f..0bcfbfdb9a 100644 --- a/src/composition/signals.ts +++ b/src/composition/signals.ts @@ -1,10 +1,5 @@ import { SignalRecordingService } from "../application/signals/SignalRecordingService.js"; -import { - SigrokRecordingProvider, - sigrokHostSupported, -} from "../signals/SigrokRecordingProvider.js"; - -export { sigrokHostSupported }; +import { SigrokRecordingProvider } from "../signals/SigrokRecordingProvider.js"; export const createSignalRecordingService = ( environment: Readonly = process.env, diff --git a/src/config/bridgeMaxRequestBytes.test.ts b/src/config/bridgeMaxRequestBytes.test.ts index be87473a68..c0ef5a8e39 100644 --- a/src/config/bridgeMaxRequestBytes.test.ts +++ b/src/config/bridgeMaxRequestBytes.test.ts @@ -12,11 +12,13 @@ import { runWithAnalysisOperationSnapshot } from "../domain/analysisOperationSna import { BRIDGE_MAX_REQUEST_BYTES_SETTING, DEFAULT_BRIDGE_MAX_REQUEST, + parseBridgeMaxRequestBytes, +} from "./bridgeMaxRequestBytes.js"; +import { DEFAULT_BRIDGE_MAX_REQUEST_BYTES, MAXIMUM_BRIDGE_MAX_REQUEST_BYTES, MINIMUM_BRIDGE_MAX_REQUEST_BYTES, - parseBridgeMaxRequestBytes, -} from "./bridgeMaxRequestBytes.js"; +} from "../domain/analysisResourceLimits.js"; const configured = (maximumBytes: number) => ({ maximumBytes, source: "setting" }) as const; diff --git a/src/config/bridgeMaxRequestBytes.ts b/src/config/bridgeMaxRequestBytes.ts index 91c089eeb8..c3a47dcadc 100644 --- a/src/config/bridgeMaxRequestBytes.ts +++ b/src/config/bridgeMaxRequestBytes.ts @@ -13,12 +13,6 @@ import { export const BRIDGE_MAX_REQUEST_BYTES_SETTING = "REA_BRIDGE_MAX_REQUEST_BYTES"; -export { - DEFAULT_BRIDGE_MAX_REQUEST_BYTES, - MAXIMUM_BRIDGE_MAX_REQUEST_BYTES, - MINIMUM_BRIDGE_MAX_REQUEST_BYTES, -}; - /** * Smallest ceiling that still admits the largest bounded bridge request. * Hopper symbol names stop at 1024 UTF-16 units. A name of U+0001 repeated diff --git a/src/contracts/analysisDetail.test.ts b/src/contracts/analysisDetail.test.ts index fdca656e78..674d1bdc9d 100644 --- a/src/contracts/analysisDetail.test.ts +++ b/src/contracts/analysisDetail.test.ts @@ -2,11 +2,11 @@ import { expect, it } from "vitest"; import { z } from "zod"; import { Ajv2020 } from "ajv/dist/2020.js"; +import { projectAnalysisView } from "../domain/analysisView/analysisView.js"; import { binaryLayoutSummaryViewResultSchema, javascriptApplicationSummaryViewResultSchema, - projectAnalysisView, -} from "../domain/analysisView/analysisView.js"; +} from "../domain/analysisView/analysisViewResult.js"; import { analysisViewRequestSchema } from "../domain/analysisView/analysisViewRequest.js"; import { analysisViewCaptureEvidence, diff --git a/src/contracts/analysisViewToolContracts.ts b/src/contracts/analysisViewToolContracts.ts index 8cd2fd648d..71b54fc43c 100644 --- a/src/contracts/analysisViewToolContracts.ts +++ b/src/contracts/analysisViewToolContracts.ts @@ -1,9 +1,7 @@ import { API_SURFACE_EXAMPLE } from "./analysisViewApiSurfaceExample.js"; import { ENVIRONMENT_MANIFEST_EXAMPLE } from "./analysisViewEnvironmentManifestExample.js"; -import { - analysisViewResultSchema, - inspectAnalysisViewInputSchema, -} from "../domain/analysisView/analysisView.js"; +import { inspectAnalysisViewInputSchema } from "../domain/analysisView/analysisViewRequest.js"; +import { analysisViewResultSchema } from "../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "./toolContractTypes.js"; import { toolContractMetadata } from "./toolEffects.js"; import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js"; diff --git a/src/contracts/android/androidToolContracts.ts b/src/contracts/android/androidToolContracts.ts index 057ee6f7a4..3be5889ac3 100644 --- a/src/contracts/android/androidToolContracts.ts +++ b/src/contracts/android/androidToolContracts.ts @@ -15,7 +15,7 @@ import { androidMethodSummaryViewResultSchema, androidJniSummaryViewResultSchema, androidJniPageViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "../toolContractTypes.js"; import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; import { toolContractMetadata } from "../toolEffects.js"; diff --git a/src/contracts/apiContractToolContracts.ts b/src/contracts/apiContractToolContracts.ts index 9abf548273..2f8321e204 100644 --- a/src/contracts/apiContractToolContracts.ts +++ b/src/contracts/apiContractToolContracts.ts @@ -6,7 +6,7 @@ import { apiContractSummaryViewResultSchema, apiContractPageViewResultSchema, graphqlContractPageViewResultSchema, -} from "../domain/analysisView/analysisView.js"; +} from "../domain/analysisView/analysisViewResult.js"; import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js"; import { toolContractMetadata } from "./toolEffects.js"; import type { ToolContract } from "./toolContractTypes.js"; diff --git a/src/contracts/apkSignature/apkSignatureToolContracts.ts b/src/contracts/apkSignature/apkSignatureToolContracts.ts index ce792ee9d1..4856b58f3c 100644 --- a/src/contracts/apkSignature/apkSignatureToolContracts.ts +++ b/src/contracts/apkSignature/apkSignatureToolContracts.ts @@ -4,7 +4,7 @@ import { inspectApkSignatureInputSchema } from "../../domain/apkSignature/apkSig import { apkSignaturePageViewResultSchema, apkSignatureSummaryViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "../toolContractTypes.js"; import { toolContractMetadata } from "../toolEffects.js"; import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; diff --git a/src/contracts/applicationToolContracts.ts b/src/contracts/applicationToolContracts.ts index bbcb1cd1a0..2e26a4a0bb 100644 --- a/src/contracts/applicationToolContracts.ts +++ b/src/contracts/applicationToolContracts.ts @@ -10,7 +10,7 @@ import { sourceToBundlePageViewResultSchema, endpointComparisonSummaryViewResultSchema, endpointComparisonPageViewResultSchema, -} from "../domain/analysisView/analysisView.js"; +} from "../domain/analysisView/analysisViewResult.js"; import { staticObservedEndpointComparisonSchema } from "../domain/javascript/staticObservedEndpointSchemas.js"; import { STATIC_OBSERVED_ENDPOINT_COMPARISON_EXAMPLE } from "./javascript/staticObservedEndpointExample.js"; import { applicationVersionComparisonResultSchema } from "../domain/javascript/javascriptApplicationVersionComparisonSchemas.js"; diff --git a/src/contracts/browserScenarioToolContracts.ts b/src/contracts/browserScenarioToolContracts.ts index b11dea37fa..3f04d935a5 100644 --- a/src/contracts/browserScenarioToolContracts.ts +++ b/src/contracts/browserScenarioToolContracts.ts @@ -4,7 +4,7 @@ import { analysisDetailOutputSchemaOf, analysisDetailSchema, } from "./analysisDetail.js"; -import { browserScenarioSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; +import { browserScenarioSummaryViewResultSchema } from "../domain/analysisView/analysisViewResult.js"; import { browserScenarioInputSchema } from "../domain/browserScenario.js"; import { browserScenarioCaptureSchema } from "../domain/browserScenarioCapture.js"; import type { JsonValue } from "../domain/jsonValue.js"; diff --git a/src/contracts/browserToolContracts.ts b/src/contracts/browserToolContracts.ts index 5c12c273e8..4152cd6c01 100644 --- a/src/contracts/browserToolContracts.ts +++ b/src/contracts/browserToolContracts.ts @@ -7,7 +7,7 @@ import { javaScriptRoutesViewResultSchema, webBundleSummaryViewResultSchema, webBundlePageViewResultSchema, -} from "../domain/analysisView/analysisView.js"; +} from "../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "./toolContractTypes.js"; import { inspectWebPageInputSchema, diff --git a/src/contracts/desktopApplicationToolContracts.ts b/src/contracts/desktopApplicationToolContracts.ts index a544d9dd97..f910d93a60 100644 --- a/src/contracts/desktopApplicationToolContracts.ts +++ b/src/contracts/desktopApplicationToolContracts.ts @@ -3,7 +3,7 @@ import { desktopApplicationSchema, inspectDesktopApplicationInputSchema, } from "../domain/desktopApplication.js"; -import { desktopApplicationSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; +import { desktopApplicationSummaryViewResultSchema } from "../domain/analysisView/analysisViewResult.js"; import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js"; import { toolContractMetadata } from "./toolEffects.js"; import type { ToolContract } from "./toolContractTypes.js"; diff --git a/src/contracts/erlang/beamToolContracts.ts b/src/contracts/erlang/beamToolContracts.ts index d6803b8a6e..e96778ba93 100644 --- a/src/contracts/erlang/beamToolContracts.ts +++ b/src/contracts/erlang/beamToolContracts.ts @@ -9,7 +9,7 @@ import { z } from "zod"; import { erlangBeamSummaryViewResultSchema, erlangBeamTableViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import { erlangBeamCompleteDeliveryShape, erlangBeamSummaryDeliveryShape, diff --git a/src/contracts/evm/evmToolContracts.ts b/src/contracts/evm/evmToolContracts.ts index 4c33d3f691..59f2d59112 100644 --- a/src/contracts/evm/evmToolContracts.ts +++ b/src/contracts/evm/evmToolContracts.ts @@ -8,7 +8,7 @@ import { analysisDetailSchema, analysisDetailOutputSchemaOf, } from "../analysisDetail.js"; -import { evmInterfaceSummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { evmInterfaceSummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; /** Delivery selection leaves the existing provider input and original Evidence unchanged. */ const evmInterfaceInspectionInputSchema = inspectEvmInterfaceInputSchema.extend( diff --git a/src/contracts/flutter/flutterToolContracts.ts b/src/contracts/flutter/flutterToolContracts.ts index 6f3b6efee4..29426653ed 100644 --- a/src/contracts/flutter/flutterToolContracts.ts +++ b/src/contracts/flutter/flutterToolContracts.ts @@ -4,7 +4,7 @@ import { dartAotInspectionInputSchema, flutterResultSchemas, } from "../../domain/flutter/flutterBuildAnalysis.js"; -import { dartAotSummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { dartAotSummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "../toolContractTypes.js"; import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; import { toolContractMetadata } from "../toolEffects.js"; diff --git a/src/contracts/fridaToolContracts.ts b/src/contracts/fridaToolContracts.ts index daffd7e656..dec537e2a6 100644 --- a/src/contracts/fridaToolContracts.ts +++ b/src/contracts/fridaToolContracts.ts @@ -15,7 +15,7 @@ import { fridaSessionStatusInputSchema, fridaSessionStatusSchema, } from "../domain/fridaSessionStatus.js"; -import { fridaSessionStatusSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; +import { fridaSessionStatusSummaryViewResultSchema } from "../domain/analysisView/analysisViewResult.js"; import { fridaDeviceListSchema, fridaProcessListSchema, diff --git a/src/contracts/gdbToolContracts.ts b/src/contracts/gdbToolContracts.ts index ee15fc43ec..b419e2579c 100644 --- a/src/contracts/gdbToolContracts.ts +++ b/src/contracts/gdbToolContracts.ts @@ -5,7 +5,7 @@ import { commandEvidenceDetailSchema, gdbCommandResultSchema, } from "../domain/commandObservation.js"; -import { gdbCommandSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; +import { gdbCommandSummaryViewResultSchema } from "../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "./toolContractTypes.js"; import { analysisDetailOutputSchemaOf } from "./analysisDetail.js"; import { toolContractMetadata } from "./toolEffects.js"; diff --git a/src/contracts/go/goToolContracts.ts b/src/contracts/go/goToolContracts.ts index 6d9ee52aed..d4b33fd430 100644 --- a/src/contracts/go/goToolContracts.ts +++ b/src/contracts/go/goToolContracts.ts @@ -5,7 +5,7 @@ import { import type { ToolContract } from "../toolContractTypes.js"; import { toolContractMetadata } from "../toolEffects.js"; import { analysisDetailOutputSchemaOf } from "../analysisDetail.js"; -import { goBinarySummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { goBinarySummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; /** Static Go metadata is independent of active native-analysis providers and host toolchains. */ export const GO_TOOL_CONTRACTS = [ diff --git a/src/contracts/gpu/gpuToolContracts.ts b/src/contracts/gpu/gpuToolContracts.ts index a924b03f21..77bf42f2b2 100644 --- a/src/contracts/gpu/gpuToolContracts.ts +++ b/src/contracts/gpu/gpuToolContracts.ts @@ -6,7 +6,7 @@ import { import { gpuCodeObjectsSummaryViewResultSchema, gpuCodeObjectsPageViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "../toolContractTypes.js"; import { toolContractMetadata } from "../toolEffects.js"; import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; diff --git a/src/contracts/il2cpp/il2cppToolContracts.ts b/src/contracts/il2cpp/il2cppToolContracts.ts index c25c7e31ed..66d26eb57c 100644 --- a/src/contracts/il2cpp/il2cppToolContracts.ts +++ b/src/contracts/il2cpp/il2cppToolContracts.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { il2cppMetadataSummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { il2cppMetadataSummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; import { il2cppMetadataSchema, inspectIl2cppMetadataInputSchema, diff --git a/src/contracts/javascript/electronToolContracts.ts b/src/contracts/javascript/electronToolContracts.ts index 32970aee36..2bd145aeeb 100644 --- a/src/contracts/javascript/electronToolContracts.ts +++ b/src/contracts/javascript/electronToolContracts.ts @@ -22,7 +22,7 @@ import { import { javaScriptRoutesViewResultSchema, javascriptApplicationSummaryViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import { analysisDetailOutputSchemaOf } from "../analysisDetail.js"; import { analyzeJavaScriptApplicationInputSchema, diff --git a/src/contracts/model/modelToolContracts.ts b/src/contracts/model/modelToolContracts.ts index c61d5dfb3d..2908d900a4 100644 --- a/src/contracts/model/modelToolContracts.ts +++ b/src/contracts/model/modelToolContracts.ts @@ -4,7 +4,7 @@ import { modelArtifactSchema } from "../../domain/model/modelArtifact.js"; import { modelArtifactPageViewResultSchema, modelArtifactSummaryViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "../toolContractTypes.js"; import { toolContractMetadata } from "../toolEffects.js"; import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; diff --git a/src/contracts/native/binaryDiagnosticsToolContracts.ts b/src/contracts/native/binaryDiagnosticsToolContracts.ts index 4e90729e6a..6b0d98ef2d 100644 --- a/src/contracts/native/binaryDiagnosticsToolContracts.ts +++ b/src/contracts/native/binaryDiagnosticsToolContracts.ts @@ -6,7 +6,7 @@ import { devicetreeSummaryViewResultSchema, binaryLayoutSummaryViewResultSchema, nativeExportsSummaryViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import { inspectPeLoadConfigInputSchema, peLoadConfigSchema, diff --git a/src/contracts/pcap/pcapToolContracts.ts b/src/contracts/pcap/pcapToolContracts.ts index 27fb02ed87..3940c1f252 100644 --- a/src/contracts/pcap/pcapToolContracts.ts +++ b/src/contracts/pcap/pcapToolContracts.ts @@ -9,7 +9,7 @@ import { analysisDetailSchema, analysisDetailOutputSchemaOf, } from "../analysisDetail.js"; -import { pcapCaptureSummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { pcapCaptureSummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; /** Packet-capture analysis served by caller-supplied tshark and capinfos. */ export const PCAP_TOOL_CONTRACTS = [ diff --git a/src/contracts/playwrightTraceToolContracts.ts b/src/contracts/playwrightTraceToolContracts.ts index 482def5ad0..b0275fe78a 100644 --- a/src/contracts/playwrightTraceToolContracts.ts +++ b/src/contracts/playwrightTraceToolContracts.ts @@ -4,7 +4,7 @@ import { inspectPlaywrightTraceInputSchema } from "../domain/playwrightTrace/pla import { playwrightTraceSummaryViewResultSchema, playwrightTracePageViewResultSchema, -} from "../domain/analysisView/analysisView.js"; +} from "../domain/analysisView/analysisViewResult.js"; import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js"; import { toolContractMetadata } from "./toolEffects.js"; import type { ToolContract } from "./toolContractTypes.js"; diff --git a/src/contracts/python/pythonPycToolContracts.ts b/src/contracts/python/pythonPycToolContracts.ts index 64d550f3e7..3056ccb70d 100644 --- a/src/contracts/python/pythonPycToolContracts.ts +++ b/src/contracts/python/pythonPycToolContracts.ts @@ -9,7 +9,7 @@ import { z } from "zod"; import { pythonPycSummaryViewResultSchema, pythonPycCodeObjectsViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import { pythonPycCompleteDeliveryShape, pythonPycSummaryDeliveryShape, diff --git a/src/contracts/reverseEngineeringToolContracts.ts b/src/contracts/reverseEngineeringToolContracts.ts index 73394aca40..9705c37dfa 100644 --- a/src/contracts/reverseEngineeringToolContracts.ts +++ b/src/contracts/reverseEngineeringToolContracts.ts @@ -1,5 +1,5 @@ import { commandObservationSchema } from "../domain/commandObservation.js"; -import { objdumpSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; +import { objdumpSummaryViewResultSchema } from "../domain/analysisView/analysisViewResult.js"; import { objdumpInputSchema, diff --git a/src/contracts/rust/rustToolContracts.ts b/src/contracts/rust/rustToolContracts.ts index 181f16dfd6..6358c5e00e 100644 --- a/src/contracts/rust/rustToolContracts.ts +++ b/src/contracts/rust/rustToolContracts.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { rustBinarySummaryViewResultSchema, rustBinaryPageViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "../toolContractTypes.js"; import { toolContractMetadata } from "../toolEffects.js"; import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; diff --git a/src/contracts/toolOutputSchemaGroups.ts b/src/contracts/toolOutputSchemaGroups.ts index 8100e805ae..5560f9e4b7 100644 --- a/src/contracts/toolOutputSchemaGroups.ts +++ b/src/contracts/toolOutputSchemaGroups.ts @@ -20,7 +20,7 @@ import { machoSummaryViewResultSchema, managedArtifactSummaryViewResultSchema, managedMemberSummaryViewResultSchema, -} from "../domain/analysisView/analysisView.js"; +} from "../domain/analysisView/analysisViewResult.js"; import { literalFeatureTraceSchema } from "../domain/native/literalFeatureTrace.js"; import { byteEntropySchema } from "../domain/native/byteEntropy.js"; import { nativeFunctionMatchingSchema } from "../domain/native/functionMatching.js"; diff --git a/src/contracts/wasm/wasmToolContracts.ts b/src/contracts/wasm/wasmToolContracts.ts index 3d72649ece..178b5e66e7 100644 --- a/src/contracts/wasm/wasmToolContracts.ts +++ b/src/contracts/wasm/wasmToolContracts.ts @@ -9,7 +9,7 @@ import { analysisDetailSchema, analysisDetailOutputSchemaOf, } from "../analysisDetail.js"; -import { wasmArtifactSummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { wasmArtifactSummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; /** Delivery selection does not change the provider input or original Evidence. */ export const wasmArtifactInspectionInputSchema = diff --git a/src/contracts/webNetworkCaptureToolContracts.ts b/src/contracts/webNetworkCaptureToolContracts.ts index 4104a3a3c3..0a5ada90bd 100644 --- a/src/contracts/webNetworkCaptureToolContracts.ts +++ b/src/contracts/webNetworkCaptureToolContracts.ts @@ -8,7 +8,7 @@ import { analysisDetailSchema, analysisDetailOutputSchemaOf, } from "./analysisDetail.js"; -import { webNetworkCaptureSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; +import { webNetworkCaptureSummaryViewResultSchema } from "../domain/analysisView/analysisViewResult.js"; /** Historical inspection is distinct from live browser capture and runtime authority. */ export const WEB_NETWORK_CAPTURE_TOOL_CONTRACTS = [ diff --git a/src/contracts/webScriptToolContracts.ts b/src/contracts/webScriptToolContracts.ts index dc53259a55..fbe318f8fa 100644 --- a/src/contracts/webScriptToolContracts.ts +++ b/src/contracts/webScriptToolContracts.ts @@ -8,7 +8,7 @@ import { webSourceLocationSummaryViewResultSchema, webSourceLocationMatchesViewResultSchema, webSourceLocationTextViewResultSchema, -} from "../domain/analysisView/analysisView.js"; +} from "../domain/analysisView/analysisViewResult.js"; import { exportWebScriptsInputSchema, webScriptExportResultSchema, diff --git a/src/cutter/CutterBridgeClient.ts b/src/cutter/CutterBridgeClient.ts index cef5949892..d26c714a03 100644 --- a/src/cutter/CutterBridgeClient.ts +++ b/src/cutter/CutterBridgeClient.ts @@ -50,10 +50,10 @@ import { BRIDGE_MAX_REQUEST_BYTES_SETTING, bridgeRequestLimitMismatchError, bridgeRequestOversizeError, - DEFAULT_BRIDGE_MAX_REQUEST_BYTES, parseBridgeMaxRequestBytes, type BridgeMaxRequestBytes, } from "../config/bridgeMaxRequestBytes.js"; +import { DEFAULT_BRIDGE_MAX_REQUEST_BYTES } from "../domain/analysisResourceLimits.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js"; import type { AnalysisSettingsReader } from "../domain/analysisSettings.js"; diff --git a/src/desktop/BuildComputerEnvironment.ts b/src/desktop/BuildComputerEnvironment.ts index 84ed1c541d..270671a436 100644 --- a/src/desktop/BuildComputerEnvironment.ts +++ b/src/desktop/BuildComputerEnvironment.ts @@ -8,9 +8,6 @@ import { PRODUCT_IDENTITY } from "../identity.js"; import { BUILD_COMPUTER_SETTINGS } from "../domain/desktop/buildComputerSettings.js"; import { resolveProgram } from "./ProgramLookup.js"; -/** The settings a build computer reads where the REA server starts. */ -export { BUILD_COMPUTER_SETTINGS }; - /** One of the build computer's settings. */ export type BuildComputerSetting = (typeof BUILD_COMPUTER_SETTINGS)[number]; diff --git a/src/desktop/DesktopBuildDoctorFacts.ts b/src/desktop/DesktopBuildDoctorFacts.ts index 3a2f4f9a55..23daea238d 100644 --- a/src/desktop/DesktopBuildDoctorFacts.ts +++ b/src/desktop/DesktopBuildDoctorFacts.ts @@ -21,12 +21,12 @@ import { execFileOutputFailure } from "../process/ExecFileOutput.js"; import { agentEnvInterpreter } from "./AgentEnvComputerHelper.js"; import { agentEnvReportedVersion, - BUILD_COMPUTER_SETTINGS, defaultBuildComputerArchive, programOnPath, resolveBuildComputerProgram, type BuildComputerSetting, } from "./BuildComputerEnvironment.js"; +import { BUILD_COMPUTER_SETTINGS } from "../domain/desktop/buildComputerSettings.js"; const DEFAULT_DOCKER_ROOT = "/var/lib/docker"; const CONFIG_BYTES = 1024 * 1024; diff --git a/src/desktop/DesktopBuildSetupHost.ts b/src/desktop/DesktopBuildSetupHost.ts index 94c6de0782..7e69512eff 100644 --- a/src/desktop/DesktopBuildSetupHost.ts +++ b/src/desktop/DesktopBuildSetupHost.ts @@ -23,11 +23,11 @@ import { } from "../process/ExecFileOutput.js"; import { agentEnvReportedVersion, - BUILD_COMPUTER_SETTINGS, buildComputerArchiveDirectory, defaultBuildComputerArchive, programOnPath, } from "./BuildComputerEnvironment.js"; +import { BUILD_COMPUTER_SETTINGS } from "../domain/desktop/buildComputerSettings.js"; import { desktopBuildOwnedCommandReason, runDesktopBuildOwnedCommand, diff --git a/src/domain/analysisView/analysisView.test.ts b/src/domain/analysisView/analysisView.test.ts index 9a87346563..69f1bf0dc3 100644 --- a/src/domain/analysisView/analysisView.test.ts +++ b/src/domain/analysisView/analysisView.test.ts @@ -27,12 +27,10 @@ import { } from "../javascript/javascriptSemanticGraph.js"; import { sourceMapReference } from "../javascript/javascriptApplicationGraph.fixture.js"; import { createArtifactInspection } from "../artifactInspection.js"; -import { - analysisViewResultSchema, - inspectAnalysisViewInputSchema, - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { inspectAnalysisViewInputSchema } from "./analysisViewRequest.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { analysisViewResultSchema } from "./analysisViewResult.js"; const layoutParent = (): AnalysisViewParent => { const evidence = analysisViewLayoutEvidence(); diff --git a/src/domain/analysisView/analysisView.ts b/src/domain/analysisView/analysisView.ts index ecc0634134..a37adea080 100644 --- a/src/domain/analysisView/analysisView.ts +++ b/src/domain/analysisView/analysisView.ts @@ -100,91 +100,8 @@ import { projectIl2cppMetadataView } from "./il2cppMetadataView.js"; import type { AnalysisViewParent } from "./analysisViewParent.js"; import type { GroupingViewKind } from "./analysisViewShape.js"; +import type { AnalysisViewRequest } from "./analysisViewRequest.js"; import { - inspectAnalysisViewInputSchema, - type AnalysisViewRequest, - type InspectAnalysisViewInput, -} from "./analysisViewRequest.js"; -import { - analysisViewResultSchema, - type AnalysisViewResult, - type UnsignedAnalysisView, -} from "./analysisViewResult.js"; -export { - inspectAnalysisViewInputSchema, - type AnalysisViewRequest, - type InspectAnalysisViewInput, - type AnalysisViewParent, -}; -export { - webSourceLocationSummaryViewResultSchema, - webSourceLocationMatchesViewResultSchema, - webSourceLocationTextViewResultSchema, - procedurePseudocodeSummaryViewResultSchema, - nativeObservationSummaryViewResultSchemas, - nativeFeatureTraceSummaryViewResultSchema, - byteEntropySummaryViewResultSchema, - nativeExportsSummaryViewResultSchema, - devicetreeSummaryViewResultSchema, - erlangBeamSummaryViewResultSchema, - erlangBeamTableViewResultSchema, - pythonPycSummaryViewResultSchema, - pythonPycCodeObjectsViewResultSchema, - gdbCommandSummaryViewResultSchema, - objdumpSummaryViewResultSchema, - fridaSessionStatusSummaryViewResultSchema, - goBinarySummaryViewResultSchema, - rustBinarySummaryViewResultSchema, - rustBinaryPageViewResultSchema, - binaryLayoutSummaryViewResultSchema, - javascriptApplicationSummaryViewResultSchema, - artifactInventorySummaryViewResultSchema, - desktopApplicationSummaryViewResultSchema, - wasmArtifactSummaryViewResultSchema, - machoSummaryViewResultSchema, - interfaceBuilderSummaryViewResultSchema, - evmInterfaceSummaryViewResultSchema, - managedArtifactSummaryViewResultSchema, - managedMemberSummaryViewResultSchema, - webNetworkCaptureSummaryViewResultSchema, - browserScenarioSummaryViewResultSchema, - androidPackageSummaryViewResultSchema, - androidJniSummaryViewResultSchema, - androidJniPageViewResultSchema, - androidClassSummaryViewResultSchema, - androidMethodSummaryViewResultSchema, - androidManifestViewResultSchema, - androidComponentsViewResultSchema, - androidComponentSummaryViewResultSchema, - pcapCaptureSummaryViewResultSchema, - dartAotSummaryViewResultSchema, - javaScriptRoutesViewResultSchema, - webBundleSummaryViewResultSchema, - webBundlePageViewResultSchema, - gpuCodeObjectsSummaryViewResultSchema, - gpuCodeObjectsPageViewResultSchema, - apkSignatureSummaryViewResultSchema, - apkSignaturePageViewResultSchema, - modelArtifactSummaryViewResultSchema, - modelArtifactPageViewResultSchema, - il2cppMetadataSummaryViewResultSchema, - nativeInventorySummaryViewResultSchema, - nativeProcedureReferencesSummaryViewResultSchema, - nativeLoadImageSummaryViewResultSchema, - nativeFunctionMatchingSummaryViewResultSchema, - nativeFunctionSummaryViewResultSchema, - sourceToBundleSummaryViewResultSchema, - sourceToBundlePageViewResultSchema, - endpointComparisonSummaryViewResultSchema, - apiContractSummaryViewResultSchema, - playwrightTraceSummaryViewResultSchema, - playwrightTracePageViewResultSchema, - apiContractPageViewResultSchema, - graphqlContractPageViewResultSchema, - endpointComparisonPageViewResultSchema, - featureTraceSummaryViewResultSchema, - batchDecompileSummaryViewResultSchema, - featureTraceViewResultSchema, analysisViewResultSchema, type AnalysisViewResult, type UnsignedAnalysisView, diff --git a/src/domain/analysisView/analysisViewParent.test.ts b/src/domain/analysisView/analysisViewParent.test.ts index b0d037f225..8eefab63a8 100644 --- a/src/domain/analysisView/analysisViewParent.test.ts +++ b/src/domain/analysisView/analysisViewParent.test.ts @@ -4,7 +4,7 @@ import { freezeOwnedJsonTreeSnapshot, isImmutableJsonSnapshot, } from "../immutableJson.js"; -import type { AnalysisViewParent } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; const parent = (normalizedResult: unknown): AnalysisViewParent => ({ diff --git a/src/domain/analysisView/analysisViewRequest.ts b/src/domain/analysisView/analysisViewRequest.ts index a041f33f45..ce0ecb9a94 100644 --- a/src/domain/analysisView/analysisViewRequest.ts +++ b/src/domain/analysisView/analysisViewRequest.ts @@ -40,7 +40,7 @@ import { javascriptModuleReferencesRequestSchema } from "./javascriptModuleRefer import { javascriptFactsViewRequestSchema } from "./javascriptFactsView.js"; import { evidenceSchema } from "../evidence.js"; import { declareEvidenceReferenceAlternative } from "../evidenceInputContract.js"; -import { apiSurfaceViewSchema } from "./webNetworkApiSurfaceView.js"; +import { apiSurfaceViewSchema } from "./apiSurfaceSchema.js"; import { evmViewRequestSchema } from "./evmInterfaceView.js"; import { artifactInventoryViewRequestSchema } from "./artifactInventoryView.js"; import { managedViewRequestSchema } from "./managedArtifactView.js"; @@ -54,7 +54,7 @@ import { environmentStateViewRequestSchema } from "./environmentStateView.js"; import { environmentCoverageViewRequestSchema } from "./environmentCoverageView.js"; import { pcapViewRequestSchema } from "./pcapCaptureView.js"; import { qlogViewRequestSchema } from "./qlogCaptureView.js"; -import { androidViewRequestSchema } from "./androidPackageView.js"; +import { androidViewRequestSchema } from "../android/androidPackageQuery.js"; import { androidJniViewRequestSchema } from "../android/androidJniQuery.js"; import { dartAotViewRequestSchema } from "./dartAotView.js"; import { il2cppMetadataViewRequestSchema } from "./il2cppMetadataView.js"; diff --git a/src/domain/analysisView/analysisViewResult.ts b/src/domain/analysisView/analysisViewResult.ts index 32188128cd..98918bf728 100644 --- a/src/domain/analysisView/analysisViewResult.ts +++ b/src/domain/analysisView/analysisViewResult.ts @@ -213,10 +213,7 @@ import { artifactGraphManifestSchema, artifactInventoryCoverageSchema, } from "../artifactGraph.js"; -import { - apiSurfaceFields, - apiSurfaceViewSchema, -} from "./webNetworkApiSurfaceView.js"; +import { apiSurfaceFields, apiSurfaceViewSchema } from "./apiSurfaceSchema.js"; import { evmViewRequestSchema, evmViewItemSchema, @@ -261,10 +258,8 @@ import { captureViewItemsSchema, captureSummarySchema, } from "./webNetworkCaptureView.js"; -import { - environmentManifestFields, - environmentManifestViewRequestSchema, -} from "./environmentManifestView.js"; +import { environmentManifestFields } from "./environmentManifestSchema.js"; +import { environmentManifestViewRequestSchema } from "./environmentManifestView.js"; import { environmentStateFields, environmentStateViewRequestSchema, @@ -286,13 +281,15 @@ import { import { androidPackageSummarySchema, androidManifestViewItemSchema, - androidManifestViewRequestSchema, - androidComponentsViewRequestSchema, - androidComponentSummaryViewRequestSchema, androidComponentViewRowSchema, androidComponentViewContextSchema, androidComponentSummarySchema, } from "./androidPackageView.js"; +import { + androidManifestViewRequestSchema, + androidComponentsViewRequestSchema, + androidComponentSummaryViewRequestSchema, +} from "../android/androidPackageQuery.js"; import { androidClassSummarySchema, androidMethodSummarySchema, diff --git a/src/domain/analysisView/androidPackageView.test.ts b/src/domain/analysisView/androidPackageView.test.ts index a82e5c65e3..53bb2f050a 100644 --- a/src/domain/analysisView/androidPackageView.test.ts +++ b/src/domain/analysisView/androidPackageView.test.ts @@ -2,12 +2,12 @@ import { expect, it } from "vitest"; import { createEvidence } from "../evidence.js"; import { projectAnalysisError } from "../analysisErrorProjection.js"; import type { JsonValue } from "../jsonValue.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { androidComponentsViewResultSchema, androidComponentSummaryViewResultSchema, - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +} from "./analysisViewResult.js"; import { analysisViewRequestSchema } from "./analysisViewRequest.js"; const component = (name: string, extra: Record = {}) => ({ diff --git a/src/domain/analysisView/androidPackageView.ts b/src/domain/analysisView/androidPackageView.ts index ec6809186d..db0cf0437b 100644 --- a/src/domain/analysisView/androidPackageView.ts +++ b/src/domain/analysisView/androidPackageView.ts @@ -32,13 +32,6 @@ import { analysisTextPageItemSchema, } from "./analysisViewText.js"; -export { - androidViewRequestSchema, - androidManifestViewRequestSchema, - androidComponentsViewRequestSchema, - androidComponentSummaryViewRequestSchema, -} from "../android/androidPackageQuery.js"; - const packageSchema = androidResultSchemas.inspect_android_package; const readReport = createAnalysisViewResultReader(packageSchema); const PAGE_BYTES = 1024 * 1024; diff --git a/src/domain/analysisView/androidSymbolView.test.ts b/src/domain/analysisView/androidSymbolView.test.ts index c0cbe5f530..6d923c1fd9 100644 --- a/src/domain/analysisView/androidSymbolView.test.ts +++ b/src/domain/analysisView/androidSymbolView.test.ts @@ -3,12 +3,12 @@ import { z } from "zod"; import { androidComponentsViewRequestSchema } from "../android/androidPackageQuery.js"; import { createEvidence } from "../evidence.js"; import { immutableJsonSnapshotMember } from "../immutableJson.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { androidClassSummaryViewResultSchema, androidMethodSummaryViewResultSchema, - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +} from "./analysisViewResult.js"; import type { AdmittedView } from "./analysisViewShape.js"; import { androidClassSummarySchema, diff --git a/src/domain/analysisView/artifactAvailabilityView.test.ts b/src/domain/analysisView/artifactAvailabilityView.test.ts index 485a9f5e09..233c14387b 100644 --- a/src/domain/analysisView/artifactAvailabilityView.test.ts +++ b/src/domain/analysisView/artifactAvailabilityView.test.ts @@ -2,10 +2,8 @@ import { expect, it } from "vitest"; import { createEvidence } from "../evidence.js"; import type { ErlangBeam } from "../erlang/beam.js"; import type { ModelArtifact } from "../model/modelArtifact.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; const parent = ( operation: string, diff --git a/src/domain/analysisView/artifactInventorySummaryProjection.ts b/src/domain/analysisView/artifactInventorySummaryProjection.ts index dc9b02e673..7ad2a10074 100644 --- a/src/domain/analysisView/artifactInventorySummaryProjection.ts +++ b/src/domain/analysisView/artifactInventorySummaryProjection.ts @@ -1,7 +1,5 @@ -import { - artifactInventorySummaryViewResultSchema, - sealAnalysisView, -} from "./analysisView.js"; +import { sealAnalysisView } from "./analysisView.js"; +import { artifactInventorySummaryViewResultSchema } from "./analysisViewResult.js"; import { ARTIFACT_INVENTORY_VIEW_CAVEATS } from "./artifactInventoryViewCaveats.js"; import { digestCanonicalValue } from "../canonicalDigest.js"; import { evidenceSemanticContent, type Evidence } from "../evidence.js"; diff --git a/src/domain/analysisView/artifactInventoryView.test.ts b/src/domain/analysisView/artifactInventoryView.test.ts index 1b334345a0..0e85b89b50 100644 --- a/src/domain/analysisView/artifactInventoryView.test.ts +++ b/src/domain/analysisView/artifactInventoryView.test.ts @@ -13,7 +13,7 @@ import { artifactInventoryViewRequestSchema, projectArtifactInventoryView, } from "./artifactInventoryView.js"; -import type { AnalysisViewParent } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; const carrier = ARTIFACT_COMPARISON_EXAMPLE.left; const freshInventory = (version: "current" | "historical" = "current") => { diff --git a/src/domain/analysisView/environmentCoverageView.test.ts b/src/domain/analysisView/environmentCoverageView.test.ts index 54f68dd646..c929e4f477 100644 --- a/src/domain/analysisView/environmentCoverageView.test.ts +++ b/src/domain/analysisView/environmentCoverageView.test.ts @@ -10,11 +10,9 @@ import { scenarioCaptureFixture, } from "../../../tests/fixtures/browserScenarioCapture.js"; import type { Evidence } from "../evidence.js"; -import { - projectAnalysisView, - type AnalysisViewParent, - type AnalysisViewResult, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import type { AnalysisViewResult } from "./analysisViewResult.js"; import { analysisViewRequestSchema } from "./analysisViewRequest.js"; import { WITHHELD_WRITE_HINTS, diff --git a/src/domain/analysisView/environmentManifestView.test.ts b/src/domain/analysisView/environmentManifestView.test.ts index 37b7767683..466c30a4bd 100644 --- a/src/domain/analysisView/environmentManifestView.test.ts +++ b/src/domain/analysisView/environmentManifestView.test.ts @@ -4,10 +4,8 @@ import { analysisViewLayoutEvidence, environmentManifestCaptureFixture, } from "../../../tests/fixtures/analysisView.js"; -import { - projectAnalysisView, - type AnalysisViewRequest, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewRequest } from "./analysisViewRequest.js"; import { analysisViewRequestSchema } from "./analysisViewRequest.js"; type ManifestView = Extract< diff --git a/src/domain/analysisView/environmentManifestView.ts b/src/domain/analysisView/environmentManifestView.ts index cabf1085d4..243131657f 100644 --- a/src/domain/analysisView/environmentManifestView.ts +++ b/src/domain/analysisView/environmentManifestView.ts @@ -16,10 +16,6 @@ import { admitScenarioCaptureParent, type ScenarioResponseBodyRetention, } from "./scenarioCaptureRecords.js"; -import { environmentManifestFields } from "./environmentManifestSchema.js"; - -export { environmentManifestFields }; - /** Opt-in template rules, each listed at most once; shared by every view that groups tools. */ export const templateRulesSchema = z .array(z.enum(TEMPLATE_RULES)) diff --git a/src/domain/analysisView/environmentStateView.test.ts b/src/domain/analysisView/environmentStateView.test.ts index 1e77daf221..c9b1a0bd95 100644 --- a/src/domain/analysisView/environmentStateView.test.ts +++ b/src/domain/analysisView/environmentStateView.test.ts @@ -14,12 +14,12 @@ import { import type { Evidence } from "../evidence.js"; import { ownedJsonObjectSchema, type JsonValue } from "../jsonValue.js"; import { webNetworkCaptureSchema } from "../webNetworkCapture.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { analysisViewResultSchema, - projectAnalysisView, - type AnalysisViewParent, type AnalysisViewResult, -} from "./analysisView.js"; +} from "./analysisViewResult.js"; import { EnvironmentStateAccumulator } from "./EnvironmentStateAccumulator.js"; import type { ObservedEntity } from "./environmentStateMerge.js"; import { diff --git a/src/domain/analysisView/goBinaryView.test.ts b/src/domain/analysisView/goBinaryView.test.ts index 239a33b324..751e3ba550 100644 --- a/src/domain/analysisView/goBinaryView.test.ts +++ b/src/domain/analysisView/goBinaryView.test.ts @@ -3,10 +3,8 @@ import { expect, it } from "vitest"; import { AnalysisInputError } from "../analysisErrorCore.js"; import { createEvidence } from "../evidence.js"; import type { GoBinary } from "../go/goBinary.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; const report: GoBinary = { artifact: { path: "/artifacts/program", sha256: "a".repeat(64), bytes: 512 }, diff --git a/src/domain/analysisView/gpuCodeObjectsView.test.ts b/src/domain/analysisView/gpuCodeObjectsView.test.ts index a800cee49c..4471ba416c 100644 --- a/src/domain/analysisView/gpuCodeObjectsView.test.ts +++ b/src/domain/analysisView/gpuCodeObjectsView.test.ts @@ -2,7 +2,7 @@ import { expect, it } from "vitest"; import { createEvidence } from "../evidence.js"; import { freezeOwnedJsonTreeSnapshot } from "../immutableJson.js"; import { gpuCodeObjectsSchema } from "../gpu/gpuCodeObjects.js"; -import type { AnalysisViewParent } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { gpuCodeObjectsItemsSchema, projectGpuCodeObjectsView, diff --git a/src/domain/analysisView/interfaceBuilderView.test.ts b/src/domain/analysisView/interfaceBuilderView.test.ts index 9c9921be59..c8e9562a7b 100644 --- a/src/domain/analysisView/interfaceBuilderView.test.ts +++ b/src/domain/analysisView/interfaceBuilderView.test.ts @@ -2,11 +2,9 @@ import { describe, expect, it, vi } from "vitest"; import { interfaceBuilderAnalysisSchema } from "../apple/interfaceBuilderGraph.js"; import { freezeJsonSnapshot } from "../immutableJson.js"; -import { - analysisViewResultSchema, - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { analysisViewResultSchema } from "./analysisViewResult.js"; const target = "a".repeat(64); const resourceRoot = { diff --git a/src/domain/analysisView/javascriptRoutesView.test.ts b/src/domain/analysisView/javascriptRoutesView.test.ts index d972591d35..1442c9785d 100644 --- a/src/domain/analysisView/javascriptRoutesView.test.ts +++ b/src/domain/analysisView/javascriptRoutesView.test.ts @@ -4,10 +4,8 @@ import { completeSemanticSteps } from "../javascript/javascriptSemanticTraversal import { buildJavaScriptRouteInventorySteps } from "../javascript/javascriptRouteInventory.js"; import { freezeJsonSnapshot } from "../immutableJson.js"; import { projectAnalysisError } from "../analysisErrorProjection.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { analysisViewRequestSchema } from "./analysisViewRequest.js"; const inventory = ( diff --git a/src/domain/analysisView/machoView.test.ts b/src/domain/analysisView/machoView.test.ts index 1c84e1da4d..a9818507df 100644 --- a/src/domain/analysisView/machoView.test.ts +++ b/src/domain/analysisView/machoView.test.ts @@ -1,11 +1,9 @@ import { describe, expect, it } from "vitest"; import { inspectMachoSchema } from "../native/nativeInspection.js"; -import { - analysisViewResultSchema, - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { analysisViewResultSchema } from "./analysisViewResult.js"; it("binds every Mach-O facet to its rows, including empty pages", () => { for (const facet of [ diff --git a/src/domain/analysisView/managedMemberView.test.ts b/src/domain/analysisView/managedMemberView.test.ts index 122f27640a..5f95aebc0b 100644 --- a/src/domain/analysisView/managedMemberView.test.ts +++ b/src/domain/analysisView/managedMemberView.test.ts @@ -2,7 +2,7 @@ import { expect, it } from "vitest"; import { MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE } from "../../contracts/managed/managedWorkflowExamples.js"; import { freezeOwnedJsonTreeSnapshot } from "../immutableJson.js"; import { managedMemberInspectionSchema } from "../managed/managedArtifact.js"; -import type { AnalysisViewParent } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { managedMembersViewRequestSchema, managedMembersViewPageSchema, diff --git a/src/domain/analysisView/nativeInventoryView.test.ts b/src/domain/analysisView/nativeInventoryView.test.ts index ae07597581..425f151632 100644 --- a/src/domain/analysisView/nativeInventoryView.test.ts +++ b/src/domain/analysisView/nativeInventoryView.test.ts @@ -1,10 +1,8 @@ import { afterEach, expect, it, vi } from "vitest"; import { freezeJsonSnapshot } from "../immutableJson.js"; import { nativeInventoryRowsSchemas } from "../native/nativeInventory.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { analysisViewRequestSchema } from "./analysisViewRequest.js"; afterEach(() => vi.restoreAllMocks()); diff --git a/src/domain/analysisView/pcapCaptureView.test.ts b/src/domain/analysisView/pcapCaptureView.test.ts index 1e16f2b02d..1f0152d4a9 100644 --- a/src/domain/analysisView/pcapCaptureView.test.ts +++ b/src/domain/analysisView/pcapCaptureView.test.ts @@ -1,9 +1,7 @@ import { expect, it } from "vitest"; import { createEvidence } from "../evidence.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; const capture = ( tsharkStderr: string, diff --git a/src/domain/analysisView/procedurePseudocodeView.test.ts b/src/domain/analysisView/procedurePseudocodeView.test.ts index e5a5fe1100..cac5124817 100644 --- a/src/domain/analysisView/procedurePseudocodeView.test.ts +++ b/src/domain/analysisView/procedurePseudocodeView.test.ts @@ -2,10 +2,8 @@ import { expect, it } from "vitest"; import { ANALYSIS_EVIDENCE_PREDICATE } from "../evidence.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; const parent = (normalizedResult: unknown): AnalysisViewParent => ({ evidenceId: `ev_${"a".repeat(64)}`, diff --git a/src/domain/analysisView/rustBinaryView.test.ts b/src/domain/analysisView/rustBinaryView.test.ts index 9cb8d8d898..8e47b109b1 100644 --- a/src/domain/analysisView/rustBinaryView.test.ts +++ b/src/domain/analysisView/rustBinaryView.test.ts @@ -2,10 +2,8 @@ import { expect, it } from "vitest"; import { createEvidence } from "../evidence.js"; import { freezeJsonSnapshot } from "../immutableJson.js"; import type { RustBinary } from "../rust/rustBinary.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; const report: RustBinary = { artifact: { path: "/artifacts/rust", sha256: "a".repeat(64), bytes: 512 }, diff --git a/src/domain/analysisView/webBundleView.test.ts b/src/domain/analysisView/webBundleView.test.ts index 25639df204..599b378b83 100644 --- a/src/domain/analysisView/webBundleView.test.ts +++ b/src/domain/analysisView/webBundleView.test.ts @@ -3,10 +3,8 @@ import type { WebBundleAnalysis } from "../webBundleAnalysis.js"; import { sampleWebBundleAnalysis } from "../../../tests/fixtures/webBundleAnalysis.js"; import { AnalysisInputError } from "../analysisErrorCore.js"; import { projectAnalysisError } from "../analysisErrorProjection.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { analysisViewRequestSchema } from "./analysisViewRequest.js"; const makeParent = ( diff --git a/src/domain/analysisView/webNetworkApiSurfaceView.ts b/src/domain/analysisView/webNetworkApiSurfaceView.ts index d1bd0224c7..29830efe0d 100644 --- a/src/domain/analysisView/webNetworkApiSurfaceView.ts +++ b/src/domain/analysisView/webNetworkApiSurfaceView.ts @@ -10,9 +10,7 @@ import { import { WebNetworkApiSurfaceAccumulator } from "./WebNetworkApiSurfaceAccumulator.js"; import { admitHarCaptureParent } from "./WebNetworkProjectionAccumulator.js"; -import { apiSurfaceFields, apiSurfaceViewSchema } from "./apiSurfaceSchema.js"; - -export { apiSurfaceFields, apiSurfaceViewSchema }; +import { apiSurfaceViewSchema } from "./apiSurfaceSchema.js"; /** Create the budget and query-name policy shared by admission and projection. */ export const createApiSurfaceAccumulator = ( diff --git a/src/domain/desktop/appBuild.ts b/src/domain/desktop/appBuild.ts index 02c8e4f4c5..5f3d067061 100644 --- a/src/domain/desktop/appBuild.ts +++ b/src/domain/desktop/appBuild.ts @@ -15,8 +15,8 @@ import { renderEnvToml, renderInstallScript, renderSmokeTask, - shellQuote, } from "./appExport.js"; +import { shellQuote } from "./shellQuote.js"; import { COMPUTER_ARCHITECTURES, DOWNLOAD_INSTALL_SCRIPT, diff --git a/src/domain/desktop/appExport.ts b/src/domain/desktop/appExport.ts index e2b1505425..01ddb6216f 100644 --- a/src/domain/desktop/appExport.ts +++ b/src/domain/desktop/appExport.ts @@ -44,7 +44,6 @@ import { capturedFileUrl, type LaunchDocument } from "./seedDocument.js"; import { posix } from "node:path"; import { roleFacts, stateDirectoryOf } from "./stateRoles.js"; import { shellQuote } from "./shellQuote.js"; -export { shellQuote } from "./shellQuote.js"; /** agentenv-framework release targeted by the exported class. */ export const DESKTOP_EXPORT_FRAMEWORK_VERSION = "0.9.1485"; diff --git a/src/domain/javascript/javascriptAstFingerprint.ts b/src/domain/javascript/javascriptAstFingerprint.ts index f7d4e5be44..b664fefd4b 100644 --- a/src/domain/javascript/javascriptAstFingerprint.ts +++ b/src/domain/javascript/javascriptAstFingerprint.ts @@ -3,7 +3,7 @@ import { createHash } from "node:crypto"; import * as t from "@babel/types"; import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; diff --git a/src/domain/javascript/javascriptBundledRequireOrigin.ts b/src/domain/javascript/javascriptBundledRequireOrigin.ts index e61c4e596b..88b1d53b48 100644 --- a/src/domain/javascript/javascriptBundledRequireOrigin.ts +++ b/src/domain/javascript/javascriptBundledRequireOrigin.ts @@ -12,10 +12,8 @@ import { import { collectJavaScriptIdentityWritesSteps } from "./javascriptElectronMemberWrites.js"; import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; import { createSemanticModuleInterop } from "./javascriptModuleInterop.js"; -import { - semanticRequireOriginCandidate, - type JavaScriptRequireOrigin, -} from "./javascriptSemanticRequireOrigin.js"; +import { semanticRequireOriginCandidate } from "./javascriptSemanticRequireOrigin.js"; +import type { JavaScriptRequireOrigin } from "./javascriptSemanticRequireOriginSlot.js"; import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js"; import { resolveSemanticBindingState, diff --git a/src/domain/javascript/javascriptCommonJsExportReceivers.ts b/src/domain/javascript/javascriptCommonJsExportReceivers.ts index 49d8146c8d..9b1c869210 100644 --- a/src/domain/javascript/javascriptCommonJsExportReceivers.ts +++ b/src/domain/javascript/javascriptCommonJsExportReceivers.ts @@ -11,7 +11,7 @@ import { semanticResolutionBlocked, type JavaScriptSemanticAnalysisState, } from "./javascriptSemanticState.js"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { traverseJavaScriptValueAstSteps } from "./javascriptSemanticTraversal.js"; import { semanticPropertyPathKeyMatches } from "./javascriptSemanticPropertyPaths.js"; import { createSemanticModuleInterop } from "./javascriptModuleInterop.js"; diff --git a/src/domain/javascript/javascriptNetworkRequests.ts b/src/domain/javascript/javascriptNetworkRequests.ts index a2b4cfc8ec..41dbe94c71 100644 --- a/src/domain/javascript/javascriptNetworkRequests.ts +++ b/src/domain/javascript/javascriptNetworkRequests.ts @@ -12,7 +12,7 @@ import { type JavaScriptSemanticAnalysisState, } from "./javascriptSemanticState.js"; import * as t from "@babel/types"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; export const isFetchCall = ( node: t.Node, diff --git a/src/domain/javascript/javascriptRouteScan.ts b/src/domain/javascript/javascriptRouteScan.ts index 175ea164b7..8766aca9e8 100644 --- a/src/domain/javascript/javascriptRouteScan.ts +++ b/src/domain/javascript/javascriptRouteScan.ts @@ -35,8 +35,8 @@ import { javaScriptRuntimeContext } from "./javascriptSourceParser.js"; import { semanticObjectPropertyReferences, semanticArrayPropertyReferences, - type JavaScriptSemanticObjectReference, } from "./javascriptSemanticObjectReferences.js"; +import type { JavaScriptSemanticObjectReference } from "./javascriptSemanticObjectReference.js"; import { classifyNextAppRoutePath, routePath } from "./javascriptRoutePaths.js"; import type { JavaScriptRouteCandidate, diff --git a/src/domain/javascript/javascriptRpcEndpoints.ts b/src/domain/javascript/javascriptRpcEndpoints.ts index 548bb4bba2..2b303467b9 100644 --- a/src/domain/javascript/javascriptRpcEndpoints.ts +++ b/src/domain/javascript/javascriptRpcEndpoints.ts @@ -14,8 +14,6 @@ import type { JavaScriptRpcScan, } from "./javascriptRpcEndpoint.js"; -export type { JavaScriptRpcScan } from "./javascriptRpcEndpoint.js"; - // tRPC proxies preserve router and procedure names as property names, so they // survive identifier minification. Method names follow @trpc/react-query and // @trpc/tanstack-react-query 11.19 documentation; cache-key helpers such as diff --git a/src/domain/javascript/javascriptSemanticAmbientWrites.ts b/src/domain/javascript/javascriptSemanticAmbientWrites.ts index 4482b9f2f3..ee6cabec83 100644 --- a/src/domain/javascript/javascriptSemanticAmbientWrites.ts +++ b/src/domain/javascript/javascriptSemanticAmbientWrites.ts @@ -21,10 +21,8 @@ import { semanticDefiniteEffectPrecedesReference, semanticEffectTiming, } from "./javascriptSemanticMutationInitializers.js"; -import { - semanticIterationSources, - type SemanticIterationSource, -} from "./javascriptSemanticIterationSources.js"; +import { semanticIterationSources } from "./javascriptSemanticIterationSources.js"; +import type { SemanticIterationSource } from "./javascriptSemanticIterationFacts.js"; import { joinSemanticPropertyPaths, semanticPropertyPathKeyMatches, @@ -42,11 +40,6 @@ import { } from "./javascriptSemanticReferenceSources.js"; import { semanticCopiedReferencePath } from "./javascriptSemanticCopiedReferencePaths.js"; -export { - isUnmodifiedGlobalReference, - isUnmodifiedImportMetaUrl, -} from "./javascriptSemanticGlobalReference.js"; - /** Global self references retain object identity, rather than adding a property. */ const canonicalAmbientPath = ( path: readonly (string | null)[], diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index c29f75c0a3..a671268f08 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -88,8 +88,8 @@ import { import { createAmbientPathResolver, indexJavaScriptAmbientWritesSteps, - isUnmodifiedGlobalReference, } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { hasUncertainTypeOnlyModuleSideEffect, TYPE_ONLY_MODULE_SIDE_EFFECT_LIMITATION, @@ -522,7 +522,6 @@ export function* analyzeParsedJavaScriptSemanticsSteps( * Receiver identity facts for the overloaded `.open` syntax, plus * `configuration` for `.get` reads of a `getConfiguration(...)` result. */ -export type { JavaScriptOpenReceiverFact } from "./javascriptSemanticReceiverFacts.js"; /** Lexical receiver identities that select static endpoint interpretations. */ export interface JavaScriptReceiverFacts { diff --git a/src/domain/javascript/javascriptSemanticAsyncEffects.ts b/src/domain/javascript/javascriptSemanticAsyncEffects.ts index 6fb46d96e5..10aec8401f 100644 --- a/src/domain/javascript/javascriptSemanticAsyncEffects.ts +++ b/src/domain/javascript/javascriptSemanticAsyncEffects.ts @@ -19,10 +19,8 @@ import { } from "./javascriptSemanticState.js"; import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js"; -import { - createGlobalReferenceReader, - isUnmodifiedGlobalReference, -} from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; +import { createGlobalReferenceReader } from "./javascriptSemanticAmbientWrites.js"; import { semanticOpaqueGlobalMayHaveChanged, semanticOpaqueNameMayHaveChanged, diff --git a/src/domain/javascript/javascriptSemanticDataEffects.ts b/src/domain/javascript/javascriptSemanticDataEffects.ts index f63782365a..9373b2b45c 100644 --- a/src/domain/javascript/javascriptSemanticDataEffects.ts +++ b/src/domain/javascript/javascriptSemanticDataEffects.ts @@ -38,7 +38,7 @@ import { traverseDataEffects as traverseWithContext, type DataEffectTraversalContext as TraversalContext, } from "./javascriptSemanticDataEffectHelpers.js"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { range } from "./javascriptStaticAnalysisHelpers.js"; import { semanticPrimitiveSet } from "./javascriptSemanticPrimitives.js"; import type { JavaScriptSemanticValue } from "./javascriptSemanticValueTypes.js"; diff --git a/src/domain/javascript/javascriptSemanticGetterResults.ts b/src/domain/javascript/javascriptSemanticGetterResults.ts index ab47ec150b..c6f24e8245 100644 --- a/src/domain/javascript/javascriptSemanticGetterResults.ts +++ b/src/domain/javascript/javascriptSemanticGetterResults.ts @@ -2,8 +2,8 @@ import * as t from "@babel/types"; import { semanticObjectPropertyReferences, semanticArrayPropertyReferences, - type JavaScriptSemanticObjectReference as Reference, } from "./javascriptSemanticObjectReferences.js"; +import type { JavaScriptSemanticObjectReference as Reference } from "./javascriptSemanticObjectReference.js"; import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js"; import { SemanticGetterQuery } from "./javascriptSemanticGetterQueries.js"; import { GetterResultWalker } from "./javascriptSemanticGetterResultWalker.js"; diff --git a/src/domain/javascript/javascriptSemanticIterationSources.ts b/src/domain/javascript/javascriptSemanticIterationSources.ts index a69e440f55..6b9d4c9052 100644 --- a/src/domain/javascript/javascriptSemanticIterationSources.ts +++ b/src/domain/javascript/javascriptSemanticIterationSources.ts @@ -20,11 +20,6 @@ import type { SemanticIterationSource, } from "./javascriptSemanticIterationFacts.js"; -export type { - IterationSourceValue, - SemanticIterationSource, -} from "./javascriptSemanticIterationFacts.js"; - /** A property value obtained by destructuring or an own-enumerable copy. */ export interface SemanticPropertyReadSource extends IterationSourceValue, SourceGuards { diff --git a/src/domain/javascript/javascriptSemanticMapOperations.ts b/src/domain/javascript/javascriptSemanticMapOperations.ts index ce559fe3ba..469ff7c04e 100644 --- a/src/domain/javascript/javascriptSemanticMapOperations.ts +++ b/src/domain/javascript/javascriptSemanticMapOperations.ts @@ -4,7 +4,7 @@ import { semanticStaticPropertyName, unwrapJavaScriptExpression, } from "./javascriptAstValues.js"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { semanticOpaqueGlobalMayHaveChanged } from "./javascriptSemanticOpaqueEffects.js"; import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; import { diff --git a/src/domain/javascript/javascriptSemanticMemberMutations.ts b/src/domain/javascript/javascriptSemanticMemberMutations.ts index b4bf18206f..bf12e3354a 100644 --- a/src/domain/javascript/javascriptSemanticMemberMutations.ts +++ b/src/domain/javascript/javascriptSemanticMemberMutations.ts @@ -52,14 +52,14 @@ import { semanticObjectPropertyReferences, semanticObjectAccessorReferences, semanticNativePrototypeChain, - type JavaScriptSemanticObjectReference, } from "./javascriptSemanticObjectReferences.js"; +import type { JavaScriptSemanticObjectReference } from "./javascriptSemanticObjectReference.js"; import { semanticIterationSources, semanticPropertyReadSources, - type SemanticIterationSource, type SemanticPropertyReadSource, } from "./javascriptSemanticIterationSources.js"; +import type { SemanticIterationSource } from "./javascriptSemanticIterationFacts.js"; import { semanticGetterReceiverReferences } from "./javascriptSemanticGetterResults.js"; import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js"; import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js"; diff --git a/src/domain/javascript/javascriptSemanticObjectReferences.ts b/src/domain/javascript/javascriptSemanticObjectReferences.ts index 9012dc8ef4..291c5cbb22 100644 --- a/src/domain/javascript/javascriptSemanticObjectReferences.ts +++ b/src/domain/javascript/javascriptSemanticObjectReferences.ts @@ -23,8 +23,6 @@ import { expandSemanticGetterResult } from "./javascriptSemanticGetterResultWalk import type { JavaScriptSemanticObjectReference } from "./javascriptSemanticObjectReference.js"; -export type { JavaScriptSemanticObjectReference } from "./javascriptSemanticObjectReference.js"; - export const semanticClassCandidates = ( root: t.Node, state: JavaScriptSemanticAnalysisState, diff --git a/src/domain/javascript/javascriptSemanticProjectedSources.ts b/src/domain/javascript/javascriptSemanticProjectedSources.ts index b2cf9c7db9..304658bee7 100644 --- a/src/domain/javascript/javascriptSemanticProjectedSources.ts +++ b/src/domain/javascript/javascriptSemanticProjectedSources.ts @@ -8,8 +8,8 @@ import { unwrapJavaScriptExpression } from "./javascriptAstValues.js"; import { semanticObjectPropertyReferences, semanticArrayPropertyReferences, - type JavaScriptSemanticObjectReference, } from "./javascriptSemanticObjectReferences.js"; +import type { JavaScriptSemanticObjectReference } from "./javascriptSemanticObjectReference.js"; import { semanticBindingAtRead, semanticDefiniteEffectPrecedesReference, diff --git a/src/domain/javascript/javascriptSemanticPromises.ts b/src/domain/javascript/javascriptSemanticPromises.ts index 80aae5e75d..b8767cfdc0 100644 --- a/src/domain/javascript/javascriptSemanticPromises.ts +++ b/src/domain/javascript/javascriptSemanticPromises.ts @@ -19,7 +19,7 @@ import { import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js"; import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { range } from "./javascriptStaticAnalysisHelpers.js"; import { assignedSemanticResultBindings, diff --git a/src/domain/javascript/javascriptSemanticRequireOrigin.ts b/src/domain/javascript/javascriptSemanticRequireOrigin.ts index 5a2dccebaf..ba7068aefe 100644 --- a/src/domain/javascript/javascriptSemanticRequireOrigin.ts +++ b/src/domain/javascript/javascriptSemanticRequireOrigin.ts @@ -63,12 +63,7 @@ import { import { isUnmodifiedGlobalReference, isUnmodifiedImportMetaUrl, -} from "./javascriptSemanticAmbientWrites.js"; - -export type { - JavaScriptRequireLoaderFact, - JavaScriptRequireOrigin, -} from "./javascriptSemanticRequireOriginSlot.js"; +} from "./javascriptSemanticGlobalReference.js"; const possibleCommonJsDelegateWriteStates = new WeakSet(); diff --git a/src/domain/javascript/javascriptSemanticResources.ts b/src/domain/javascript/javascriptSemanticResources.ts index 358d54c601..646023620d 100644 --- a/src/domain/javascript/javascriptSemanticResources.ts +++ b/src/domain/javascript/javascriptSemanticResources.ts @@ -20,7 +20,7 @@ import { staticObjectCopyPropertyReader, unwrapJavaScriptExpression, } from "./javascriptAstValues.js"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { semanticOpaqueBindingReadMayHaveChanged } from "./javascriptSemanticOpaqueEffects.js"; import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; import { diff --git a/src/domain/javascript/javascriptSemanticTypedArrays.ts b/src/domain/javascript/javascriptSemanticTypedArrays.ts index 5bda6960c1..7be386c6c3 100644 --- a/src/domain/javascript/javascriptSemanticTypedArrays.ts +++ b/src/domain/javascript/javascriptSemanticTypedArrays.ts @@ -1,7 +1,7 @@ import * as t from "@babel/types"; import { unwrapJavaScriptExpression } from "./javascriptAstValues.js"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { semanticOpaqueGlobalMayHaveChanged } from "./javascriptSemanticOpaqueEffects.js"; import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js"; diff --git a/src/domain/javascript/javascriptStaticAnalysisCalls.ts b/src/domain/javascript/javascriptStaticAnalysisCalls.ts index 4a25d8d23d..2edbc99c8b 100644 --- a/src/domain/javascript/javascriptStaticAnalysisCalls.ts +++ b/src/domain/javascript/javascriptStaticAnalysisCalls.ts @@ -51,7 +51,7 @@ import { import { isUnmodifiedGlobalReference, isUnmodifiedImportMetaUrl, -} from "./javascriptSemanticAmbientWrites.js"; +} from "./javascriptSemanticGlobalReference.js"; import { evaluateSemanticExpression, evaluateSemanticExpressionProvenance, diff --git a/src/domain/javascript/javascriptStaticAnalysisState.ts b/src/domain/javascript/javascriptStaticAnalysisState.ts index 3025fc01fd..099ef9f5a6 100644 --- a/src/domain/javascript/javascriptStaticAnalysisState.ts +++ b/src/domain/javascript/javascriptStaticAnalysisState.ts @@ -1,7 +1,7 @@ import type * as t from "@babel/types"; import type { JavaScriptBundlerFactoryRoles } from "./javascriptBundlerFactoryRoles.js"; import type { JavaScriptOpenReceiverFact } from "./javascriptSemanticReceiverFacts.js"; -import type { JavaScriptRpcScan } from "./javascriptRpcEndpoints.js"; +import type { JavaScriptRpcScan } from "./javascriptRpcEndpoint.js"; import type { JavaScriptStaticPathIntrinsicQuery } from "./javascriptStaticAnalysisHelpers.js"; import type { JavaScriptExpressionShapeResolver } from "./javascriptSemanticExpressionShapes.js"; diff --git a/src/domain/javascript/javascriptStaticPathIntrinsics.ts b/src/domain/javascript/javascriptStaticPathIntrinsics.ts index bc363bb4d0..61fc7dc8d8 100644 --- a/src/domain/javascript/javascriptStaticPathIntrinsics.ts +++ b/src/domain/javascript/javascriptStaticPathIntrinsics.ts @@ -7,7 +7,7 @@ import { evaluateSemanticExpressionProvenance } from "./javascriptSemanticValues import { isUnmodifiedGlobalReference, isUnmodifiedImportMetaUrl, -} from "./javascriptSemanticAmbientWrites.js"; +} from "./javascriptSemanticGlobalReference.js"; import { unwrapJavaScriptExpression } from "./javascriptAstValues.js"; import type { JavaScriptStaticPathIntrinsicQuery } from "./javascriptStaticAnalysisHelpers.js"; diff --git a/src/domain/referenceSourceRequireBindings.ts b/src/domain/referenceSourceRequireBindings.ts index 323dbd014a..3c81617003 100644 --- a/src/domain/referenceSourceRequireBindings.ts +++ b/src/domain/referenceSourceRequireBindings.ts @@ -6,7 +6,7 @@ import { semanticStaticPropertyKey, unwrapJavaScriptExpression, } from "./javascript/javascriptAstValues.js"; -import { isUnmodifiedGlobalReference } from "./javascript/javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascript/javascriptSemanticGlobalReference.js"; import { indexJavaScriptBindingsWithReceiversSteps, type JavaScriptReceiverIndex, diff --git a/src/domain/webBundleAnalyzerInspection.ts b/src/domain/webBundleAnalyzerInspection.ts index 30565524a9..776b68662b 100644 --- a/src/domain/webBundleAnalyzerInspection.ts +++ b/src/domain/webBundleAnalyzerInspection.ts @@ -20,7 +20,7 @@ import { completeSemanticSteps, } from "./javascript/javascriptSemanticTraversal.js"; import { indexJavaScriptBindingsWithReceiversSteps } from "./javascript/javascriptSemanticAnalysis.js"; -import type { JavaScriptOpenReceiverFact } from "./javascript/javascriptSemanticAnalysis.js"; +import type { JavaScriptOpenReceiverFact } from "./javascript/javascriptSemanticReceiverFacts.js"; import { isIntrinsicMapLookup } from "./javascript/javascriptSemanticMapOperations.js"; import { semanticStaticPropertyKey, diff --git a/src/domain/webSourceLocation.ts b/src/domain/webSourceLocation.ts index f22e9b64b2..c61deafb37 100644 --- a/src/domain/webSourceLocation.ts +++ b/src/domain/webSourceLocation.ts @@ -2,7 +2,6 @@ import { localSourceMapLimitsSchema, WEB_SOURCE_MAP_LIMITS, } from "./sourceMapLimits.js"; -export { WEB_SOURCE_MAP_LIMITS } from "./sourceMapLimits.js"; import { z } from "zod"; import { browserDeadlineIncrease, diff --git a/src/domain/windows/ntfsMft.ts b/src/domain/windows/ntfsMft.ts index 2cec504f92..182eda821d 100644 --- a/src/domain/windows/ntfsMft.ts +++ b/src/domain/windows/ntfsMft.ts @@ -8,7 +8,6 @@ import { MFT_INLINE_ATTRIBUTE_REFERENCE_LIMIT, type MftFileReference, } from "./ntfsMftAttributes.js"; -export type { MftFileReference } from "./ntfsMftAttributes.js"; import { err, ok, type Result } from "../result.js"; import type { RegionDecodeFailure, diff --git a/src/erlang/BeamProvider.ts b/src/erlang/BeamProvider.ts index 76419eec11..3e4d6867a0 100644 --- a/src/erlang/BeamProvider.ts +++ b/src/erlang/BeamProvider.ts @@ -1,7 +1,8 @@ import { ERLANG_BEAM_PROVIDER_IDENTITY } from "./ErlangBeamProviderIdentity.js"; import { + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; @@ -13,7 +14,6 @@ import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, artifactSelectedPathErrno, - artifactFileCancellationCause, } from "../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import type { BeamPort } from "../application/erlang/BeamPort.js"; @@ -21,8 +21,6 @@ import type { ExecutionOptions } from "../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../domain/artifactOperationError.js"; import { AnalysisAccessDeniedError, - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, } from "../domain/analysisErrorCore.js"; @@ -111,27 +109,12 @@ const beamFailurePrimary = ( input: InspectErlangBeamInput, ) => { const { cause, origin } = facts; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { - cause, - }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -159,17 +142,7 @@ const beamFailurePrimary = ( ); } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } if (cause instanceof Error && "code" in cause) { if (cause.code === "EACCES" || cause.code === "EPERM") diff --git a/src/firmware/devicetree/DevicetreeProvider.ts b/src/firmware/devicetree/DevicetreeProvider.ts index 385fe4820c..ee14aba535 100644 --- a/src/firmware/devicetree/DevicetreeProvider.ts +++ b/src/firmware/devicetree/DevicetreeProvider.ts @@ -1,7 +1,8 @@ import { + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -12,15 +13,12 @@ import { import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { DevicetreePort } from "../../application/binaryDiagnostics/DevicetreePort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -114,27 +112,12 @@ const devicetreeFailurePrimary = ( message: cause.message, }, ]); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { - cause, - }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -157,17 +140,7 @@ const devicetreeFailurePrimary = ( } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/go/GoBinaryProvider.ts b/src/go/GoBinaryProvider.ts index b658d3635b..3c028e5a5b 100644 --- a/src/go/GoBinaryProvider.ts +++ b/src/go/GoBinaryProvider.ts @@ -2,9 +2,10 @@ import { jsonValueByteLength } from "../domain/jsonValueByteLength.js"; import { GO_BINARY_PROVIDER_IDENTITY } from "./GoBinaryProviderIdentity.js"; import { projectSelectedArtifactFileFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, projectArtifactCloseFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, } from "../artifacts/StableArtifactFailure.js"; import { type StableArtifactSnapshot, @@ -14,7 +15,6 @@ import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, assertArtifactReadActive, - artifactFileCancellationCause, } from "../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; @@ -22,8 +22,6 @@ import type { ExecutionOptions } from "../application/AnalysisProvider.js"; import type { GoBinaryPort } from "../application/go/GoBinaryPort.js"; import { AnalysisError } from "../domain/analysisErrorBase.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisOutputError, AnalysisResourceConstraintError, @@ -66,22 +64,12 @@ const failureForPrimary = ( const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) return cause; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError(OPERATION, path, cause.message, { - cause, - }); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + path, + ); + if (identityFailure !== undefined) return identityFailure; if (cause instanceof BinaryResourceFailure) { const parameter = cause.boundary === "search" @@ -146,11 +134,7 @@ const failureForPrimary = ( }, ); if (cause.reason === "path") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/hopper/BridgeLauncher.ts b/src/hopper/BridgeLauncher.ts index 485c7f2687..110fb5a869 100644 --- a/src/hopper/BridgeLauncher.ts +++ b/src/hopper/BridgeLauncher.ts @@ -53,8 +53,8 @@ import type { OwnedLease } from "../process/OwnedLease.js"; import { MAXIMUM_BRIDGE_MAX_REQUEST_BYTES, MINIMUM_BRIDGE_MAX_REQUEST_BYTES, - type BridgeMaxRequestBytes, -} from "../config/bridgeMaxRequestBytes.js"; +} from "../domain/analysisResourceLimits.js"; +import type { BridgeMaxRequestBytes } from "../config/bridgeMaxRequestBytes.js"; import { HOPPER_SHUTDOWN_TIMEOUT_MS } from "./HopperSessionValues.js"; import { verifyMacHopperNativeHelper } from "./HopperMacPublisher.js"; import { inspectLinuxHopperLauncher } from "../application/LinuxHopper.js"; diff --git a/src/hopper/HopperSessionValues.test.ts b/src/hopper/HopperSessionValues.test.ts index b692085115..ae6d718785 100644 --- a/src/hopper/HopperSessionValues.test.ts +++ b/src/hopper/HopperSessionValues.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { DEFAULT_BRIDGE_MAX_REQUEST_BYTES } from "../config/bridgeMaxRequestBytes.js"; +import { DEFAULT_BRIDGE_MAX_REQUEST_BYTES } from "../domain/analysisResourceLimits.js"; import { parseHopperServerInfo } from "./HopperSessionValues.js"; const health = { diff --git a/src/hopper/HopperSessionValues.ts b/src/hopper/HopperSessionValues.ts index 477060081b..09768af56e 100644 --- a/src/hopper/HopperSessionValues.ts +++ b/src/hopper/HopperSessionValues.ts @@ -1,4 +1,4 @@ -import { DEFAULT_BRIDGE_MAX_REQUEST_BYTES } from "../config/bridgeMaxRequestBytes.js"; +import { DEFAULT_BRIDGE_MAX_REQUEST_BYTES } from "../domain/analysisResourceLimits.js"; import { HopperProtocolError } from "../domain/hopperErrors.js"; import type { JsonValue } from "../domain/jsonValue.js"; import { err, ok, type Result } from "../domain/result.js"; diff --git a/src/il2cpp/Il2cppMetadataProvider.ts b/src/il2cpp/Il2cppMetadataProvider.ts index 8ee9b18a73..1c489ff96d 100644 --- a/src/il2cpp/Il2cppMetadataProvider.ts +++ b/src/il2cpp/Il2cppMetadataProvider.ts @@ -15,15 +15,15 @@ import { import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; import { + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, } from "../artifacts/StableArtifactFailure.js"; import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, artifactFileCompletion, bindArtifactFileCleanup, - artifactFileCancellationCause, assertArtifactReadActive, } from "../artifacts/ArtifactFileOperation.js"; import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js"; @@ -35,7 +35,6 @@ import { createIl2cppMetadataAdmission } from "./Il2cppMetadataAdmission.js"; import { hexAddress as hex } from "./Il2cppBinaryImage.js"; import { jsonValueByteLength } from "../domain/jsonValueByteLength.js"; import { - AnalysisArtifactChangedError, AnalysisCancelledError, AnalysisCapabilityUnavailableError, AnalysisInputError, @@ -117,22 +116,12 @@ const failureForPrimary = ( }, ]); } - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError(OPERATION, path, cause.message, { - cause, - }); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -149,11 +138,7 @@ const failureForPrimary = ( }, ); if (cause.reason === "path") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: [field], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION, field); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/javascript/analysis/JavaScriptAnalysisTransfer.ts b/src/javascript/analysis/JavaScriptAnalysisTransfer.ts index 9b89cd5b80..5bd990be47 100644 --- a/src/javascript/analysis/JavaScriptAnalysisTransfer.ts +++ b/src/javascript/analysis/JavaScriptAnalysisTransfer.ts @@ -26,7 +26,6 @@ import { } from "./JavaScriptAnalysisWorkerSchemas.js"; // Budgets bound transfer representation, independently of source length. -export { JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES } from "../../domain/javascript/javascriptAnalysisResourceControls.js"; const FRAME_BYTES = JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES; const TRANSFER_BYTES = 512 * 1024 * 1024; const MAX_TRANSFER_BYTES = 2 * 1024 * 1024 * 1024; diff --git a/src/javascript/analysis/JavaScriptAnalysisWorker.ts b/src/javascript/analysis/JavaScriptAnalysisWorker.ts index 837294d91b..dc4af6c852 100644 --- a/src/javascript/analysis/JavaScriptAnalysisWorker.ts +++ b/src/javascript/analysis/JavaScriptAnalysisWorker.ts @@ -67,11 +67,11 @@ import { } from "../../process/ProviderDeadline.js"; import { readJavaScriptAnalysisTransfer, - JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES, javaScriptForwardedTransferRemediation, javaScriptTransferRetentionBytes, } from "./JavaScriptAnalysisTransfer.js"; import { + JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES, JAVASCRIPT_PARENT_CHECKPOINT_HEAP_REMEDIATION, JAVASCRIPT_PARENT_RESULT_BUDGET_REMEDIATION, JAVASCRIPT_WORKER_HEAP_MIB_MAXIMUM, diff --git a/src/javascript/sourceMaps/SourceMapCommand.test.ts b/src/javascript/sourceMaps/SourceMapCommand.test.ts index 02cdc5aeab..7206f65bf8 100644 --- a/src/javascript/sourceMaps/SourceMapCommand.test.ts +++ b/src/javascript/sourceMaps/SourceMapCommand.test.ts @@ -17,10 +17,8 @@ import { ProviderProcessSupervisor, spawnOwnedProviderProcess, } from "../../process/ProviderProcess.js"; -import { - WEB_SOURCE_MAP_LIMITS, - type WebSourceMapReport, -} from "../../domain/webSourceLocation.js"; +import { WEB_SOURCE_MAP_LIMITS } from "../../domain/sourceMapLimits.js"; +import type { WebSourceMapReport } from "../../domain/webSourceLocation.js"; import { traceSourceMap } from "./TraceSourceMap.js"; import { runSourceMapCommand, diff --git a/src/javascript/sourceMaps/SourceMapCommand.ts b/src/javascript/sourceMaps/SourceMapCommand.ts index a1810bafb2..137ac06f5b 100644 --- a/src/javascript/sourceMaps/SourceMapCommand.ts +++ b/src/javascript/sourceMaps/SourceMapCommand.ts @@ -1,4 +1,5 @@ import { + WEB_SOURCE_MAP_LIMITS, sourceMapLimitSelection, sourceMapLimitRecovery, type SourceMapLimits, @@ -23,7 +24,6 @@ import type { JsonValue } from "../../domain/jsonValue.js"; import { err, type Result } from "../../domain/result.js"; import { DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS, - WEB_SOURCE_MAP_LIMITS, webSourceMapDecodeTimeoutRecovery, type WebSourceMapReport, } from "../../domain/webSourceLocation.js"; diff --git a/src/native/apple/AppleIpsProvider.ts b/src/native/apple/AppleIpsProvider.ts index d917fc52b5..dcbb999dda 100644 --- a/src/native/apple/AppleIpsProvider.ts +++ b/src/native/apple/AppleIpsProvider.ts @@ -14,9 +14,10 @@ import { } from "../../domain/native/appleIps.js"; import type { StableArtifactSnapshot } from "../../artifacts/readStableArtifact.js"; import { + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -25,15 +26,12 @@ import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, assertArtifactReadActive, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { AppleIpsPort } from "../../application/binaryDiagnostics/AppleIpsPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -262,25 +260,12 @@ const appleIpsFailurePrimary = ( const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) return cause; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if (cause instanceof AppleIpsResourceFailure) return new AnalysisResourceConstraintError( OPERATION, @@ -325,11 +310,7 @@ const appleIpsFailurePrimary = ( }, ); if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: [field], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION, field); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/elf/ElfNotesProvider.ts b/src/native/elf/ElfNotesProvider.ts index dbc875b738..4994990b0a 100644 --- a/src/native/elf/ElfNotesProvider.ts +++ b/src/native/elf/ElfNotesProvider.ts @@ -1,7 +1,8 @@ import { + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -12,15 +13,12 @@ import { import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { ElfNotesPort } from "../../application/binaryDiagnostics/ElfNotesPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -144,27 +142,12 @@ const elfNotesFailurePrimary = ( message: cause.message, }, ]); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { - cause, - }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -199,17 +182,7 @@ const elfNotesFailurePrimary = ( ); } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/elf/ElfUnwindRangesProvider.ts b/src/native/elf/ElfUnwindRangesProvider.ts index 86044d8dad..4a68a87e48 100644 --- a/src/native/elf/ElfUnwindRangesProvider.ts +++ b/src/native/elf/ElfUnwindRangesProvider.ts @@ -1,7 +1,8 @@ import { + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -9,15 +10,12 @@ import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { ElfUnwindRangesPort } from "../../application/binaryDiagnostics/ElfUnwindRangesPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -149,25 +147,12 @@ const elfUnwindFailurePrimary = ( message: cause.message, }, ]); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -202,17 +187,7 @@ const elfUnwindFailurePrimary = ( ); } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/elf/KernelBuildConfigProvider.ts b/src/native/elf/KernelBuildConfigProvider.ts index 0181da8151..49f3a74f6b 100644 --- a/src/native/elf/KernelBuildConfigProvider.ts +++ b/src/native/elf/KernelBuildConfigProvider.ts @@ -1,7 +1,8 @@ import { + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -9,15 +10,12 @@ import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { KernelBuildConfigPort } from "../../application/binaryDiagnostics/KernelBuildConfigPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, } from "../../domain/analysisErrorCore.js"; @@ -136,25 +134,12 @@ const kernelBuildConfigFailurePrimary = ( input: InspectKernelBuildConfigInput, ) => { const { cause, origin } = facts; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -186,17 +171,7 @@ const kernelBuildConfigFailurePrimary = ( ); } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/elf/KernelModuleProvider.ts b/src/native/elf/KernelModuleProvider.ts index 4902614fdf..f13e1beebf 100644 --- a/src/native/elf/KernelModuleProvider.ts +++ b/src/native/elf/KernelModuleProvider.ts @@ -1,7 +1,8 @@ import { + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -12,15 +13,12 @@ import { import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { KernelModulePort } from "../../application/binaryDiagnostics/KernelModulePort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -136,27 +134,12 @@ const kernelModuleFailurePrimary = ( message: cause.message, }, ]); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { - cause, - }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -191,17 +174,7 @@ const kernelModuleFailurePrimary = ( ); } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/exports/NativeExportsProvider.ts b/src/native/exports/NativeExportsProvider.ts index 7f6b0d2762..a8efd05bc9 100644 --- a/src/native/exports/NativeExportsProvider.ts +++ b/src/native/exports/NativeExportsProvider.ts @@ -1,8 +1,9 @@ import { imageFileSizeRemediation, projectSelectedArtifactFileFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -14,15 +15,12 @@ import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, assertArtifactReadActive, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { NativeExportsPort } from "../../application/binaryDiagnostics/NativeExportsPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -93,25 +91,12 @@ const nativeExportsFailurePrimary = ( const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) return cause; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if (cause instanceof BinaryResourceFailure) return new AnalysisResourceConstraintError( OPERATION, @@ -163,11 +148,7 @@ const nativeExportsFailurePrimary = ( }, ); if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/exports/NativeExportsView.test.ts b/src/native/exports/NativeExportsView.test.ts index a23c434138..6623493d6d 100644 --- a/src/native/exports/NativeExportsView.test.ts +++ b/src/native/exports/NativeExportsView.test.ts @@ -6,10 +6,8 @@ import { } from "../../../tests/fixtures/binaryDiagnostics/nativeExports.js"; import { freezeJsonSnapshot } from "../../domain/immutableJson.js"; import { digestCanonicalValue } from "../../domain/canonicalDigest.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "../../domain/analysisView/analysisView.js"; +import { projectAnalysisView } from "../../domain/analysisView/analysisView.js"; +import type { AnalysisViewParent } from "../../domain/analysisView/analysisViewParent.js"; import { analysisViewRequestSchema } from "../../domain/analysisView/analysisViewRequest.js"; const artifact = { diff --git a/src/native/hardening/BinaryHardeningProvider.ts b/src/native/hardening/BinaryHardeningProvider.ts index 9e798c3932..d2ac7d2d74 100644 --- a/src/native/hardening/BinaryHardeningProvider.ts +++ b/src/native/hardening/BinaryHardeningProvider.ts @@ -1,7 +1,8 @@ import { imageFileSizeRemediation, + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectStableArtifactFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; @@ -14,15 +15,12 @@ import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, assertArtifactReadActive, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { BinaryHardeningPort } from "../../application/binaryDiagnostics/BinaryHardeningPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -139,25 +137,12 @@ const binaryHardeningFailurePrimary = ( const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) return cause; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if (cause instanceof BinaryResourceFailure) return new AnalysisResourceConstraintError( OPERATION, @@ -207,11 +192,7 @@ const binaryHardeningFailurePrimary = ( }, ); if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/initializers/NativeInitializersProvider.ts b/src/native/initializers/NativeInitializersProvider.ts index db1e4c06c5..8761b9ad2a 100644 --- a/src/native/initializers/NativeInitializersProvider.ts +++ b/src/native/initializers/NativeInitializersProvider.ts @@ -1,14 +1,14 @@ import { imageFileSizeRemediation, projectSelectedArtifactFileFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; import { - artifactFileCancellationCause, artifactFilePrimaryFacts, assertArtifactReadActive, type ArtifactFilePrimaryFacts, @@ -18,8 +18,6 @@ import type { NativeInitializersPort } from "../../application/binaryDiagnostics import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -192,25 +190,12 @@ const failurePrimary = ( const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) return cause; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( cause instanceof ElfPointerRelocationFailure && cause.reason === "native-index-capacity" @@ -277,11 +262,7 @@ const failurePrimary = ( }, ); if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/pe/PeBinaryFailure.ts b/src/native/pe/PeBinaryFailure.ts index a111f3cdcd..8b0b99c4dd 100644 --- a/src/native/pe/PeBinaryFailure.ts +++ b/src/native/pe/PeBinaryFailure.ts @@ -1,6 +1,6 @@ import { projectSelectedArtifactFileFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectStableArtifactFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -107,11 +107,7 @@ const peBinaryFailurePrimary = ( }, ); if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - operation, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, operation); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/pe/PeCertificatesProvider.ts b/src/native/pe/PeCertificatesProvider.ts index 67df806ce1..d0eac0fcfe 100644 --- a/src/native/pe/PeCertificatesProvider.ts +++ b/src/native/pe/PeCertificatesProvider.ts @@ -1,8 +1,9 @@ import { BinaryFormatFailure } from "../metadata/BinaryContainer.js"; import { projectSelectedArtifactFileFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -13,15 +14,12 @@ import { import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { PeCertificatesPort } from "../../application/binaryDiagnostics/PeCertificatesPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -152,27 +150,12 @@ const peCertificatesFailurePrimary = ( { cause }, ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { - cause, - }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof PeCertificateAsn1Failure && @@ -206,17 +189,7 @@ const peCertificatesFailurePrimary = ( }, ); if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/pe/PeResourcesProvider.ts b/src/native/pe/PeResourcesProvider.ts index 4de6c1b626..784bab490c 100644 --- a/src/native/pe/PeResourcesProvider.ts +++ b/src/native/pe/PeResourcesProvider.ts @@ -2,8 +2,9 @@ import { BinaryFormatFailure } from "../metadata/BinaryContainer.js"; import { imageFileSizeRemediation, projectSelectedArtifactFileFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { fixedNativeAllowance } from "../../domain/native/nativeDeadlines.js"; @@ -15,15 +16,12 @@ import { import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { PeResourcesPort } from "../../application/binaryDiagnostics/PeResourcesPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -116,25 +114,12 @@ const peResourcesFailurePrimary = ( { cause }, ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - operation, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(operation, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + operation, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && @@ -178,17 +163,7 @@ const peResourcesFailurePrimary = ( ); } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - operation, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, operation); } const fileFailure = projectSelectedArtifactFileFailure( diff --git a/src/native/pwntools/PwntoolsDecoder.ts b/src/native/pwntools/PwntoolsDecoder.ts index 574c0bcd2d..378d22b353 100644 --- a/src/native/pwntools/PwntoolsDecoder.ts +++ b/src/native/pwntools/PwntoolsDecoder.ts @@ -323,12 +323,11 @@ export class PwntoolsDecoder< let replyFinalization: AnalysisError | undefined; let phase: "configuration" | "artifact-read" | "decoder" = "configuration"; const executablePath = environment.REA_PWNTOOLS_PYTHON ?? ""; - let selectedPath = executablePath; const failure = (cause: unknown) => pwntoolsDecoderFailure( cause, phase, - selectedPath, + phase === "configuration" ? executablePath : input.path, executablePath, {}, context, @@ -350,20 +349,20 @@ export class PwntoolsDecoder< { userMessage: requirement }, ); } - if (!isAbsolute(selectedPath)) + if (!isAbsolute(executablePath)) throw pwntoolsUnavailable( "Set REA_PWNTOOLS_PYTHON to an absolute caller-supplied Python executable with pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2; REA never installs Python or packages.", - selectedPath, + executablePath, undefined, undefined, context, ); - await access(selectedPath, constants.X_OK); - if (!(await stat(selectedPath)).isFile()) + await access(executablePath, constants.X_OK); + if (!(await stat(executablePath)).isFile()) throw pwntoolsUnavailable( "Selected Python executable must be a regular executable file, not a directory: " + - selectedPath, - selectedPath, + executablePath, + executablePath, undefined, undefined, context, @@ -379,7 +378,7 @@ export class PwntoolsDecoder< if (!inputLimit.configured) throw pwntoolsUnavailable( inputLimit.error, - selectedPath, + executablePath, undefined, undefined, { @@ -393,7 +392,6 @@ export class PwntoolsDecoder< ...resolvePwntoolsLimits(selectedLimits.data, inputLimit.bytes), command_timeout_ms: timeoutMs, }; - selectedPath = input.path; const snapshot = await readStableArtifact( input.path, inputLimit.bytes, @@ -684,7 +682,7 @@ export class PwntoolsDecoder< const primary = pwntoolsDecoderFailure( cause, phase, - selectedPath, + phase === "configuration" ? executablePath : input.path, executablePath, evidence, context, diff --git a/src/native/windows/WindowsShortcutProvider.ts b/src/native/windows/WindowsShortcutProvider.ts index 76a8cde9ff..f51c8a6ab0 100644 --- a/src/native/windows/WindowsShortcutProvider.ts +++ b/src/native/windows/WindowsShortcutProvider.ts @@ -1,6 +1,7 @@ import { lstat, open } from "node:fs/promises"; import { projectSelectedArtifactFileFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; @@ -15,15 +16,12 @@ import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, assertArtifactReadActive, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { WindowsShortcutPort } from "../../application/binaryDiagnostics/WindowsShortcutPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -281,25 +279,12 @@ const windowsShortcutFailurePrimary = ( const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) return cause; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if (cause instanceof WindowsShortcutResourceFailure) return new AnalysisResourceConstraintError( OPERATION, diff --git a/src/python/PythonPycProvider.ts b/src/python/PythonPycProvider.ts index 30b2da8fc6..842715a92d 100644 --- a/src/python/PythonPycProvider.ts +++ b/src/python/PythonPycProvider.ts @@ -1,12 +1,12 @@ import { PYTHON_PYC_PROVIDER_IDENTITY } from "./PythonPycProviderIdentity.js"; import { projectSelectedArtifactFileFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../artifacts/StableArtifactFailure.js"; import { - artifactFileCancellationCause, artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, } from "../artifacts/ArtifactFileOperation.js"; @@ -20,8 +20,6 @@ import type { PythonPycPort } from "../application/python/PythonPycPort.js"; import type { ExecutionOptions } from "../application/AnalysisProvider.js"; import { AnalysisError } from "../domain/analysisErrorBase.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -71,25 +69,12 @@ const failureForPrimary = ( const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) return cause; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if (cause instanceof PythonPycResourceFailure) return new AnalysisResourceConstraintError( OPERATION, @@ -129,17 +114,7 @@ const failureForPrimary = ( }, ); if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/rust/RustBinaryProvider.ts b/src/rust/RustBinaryProvider.ts index 08a33cb150..3bf7347783 100644 --- a/src/rust/RustBinaryProvider.ts +++ b/src/rust/RustBinaryProvider.ts @@ -2,7 +2,7 @@ import { RUST_BINARY_PROVIDER_IDENTITY } from "./RustBinaryProviderIdentity.js"; import { projectSelectedArtifactFileFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../artifacts/StableArtifactFailure.js"; import { @@ -436,11 +436,7 @@ const artifactReaderFailure = ( }, ); if (cause.reason === "path") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION); return new ProviderAdapterError(RUST_BINARY_PROVIDER_IDENTITY.id, OPERATION, { cause, diagnostics: { path, reason: cause.message }, diff --git a/src/server/contractToolHandler.ts b/src/server/contractToolHandler.ts index 6662ca77bd..c9bdedf869 100644 --- a/src/server/contractToolHandler.ts +++ b/src/server/contractToolHandler.ts @@ -17,7 +17,7 @@ import { requireAnalysisViewRetention, type AnalysisViewRetention, } from "../application/analysisView/AnalysisViewService.js"; -import type { AnalysisViewRequest } from "../domain/analysisView/analysisView.js"; +import type { AnalysisViewRequest } from "../domain/analysisView/analysisViewRequest.js"; import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import type { ToolContract } from "../contracts/toolContractTypes.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; diff --git a/src/server/registerApplicationTools/helpers.ts b/src/server/registerApplicationTools/helpers.ts index 9313a45220..dbe2419b54 100644 --- a/src/server/registerApplicationTools/helpers.ts +++ b/src/server/registerApplicationTools/helpers.ts @@ -5,7 +5,7 @@ import { APPLICATION_TOOL_CONTRACTS } from "../../contracts/applicationToolContr import { inspectAnalysisViewValidated } from "../../application/analysisView/AnalysisViewService.js"; import { recordEvidenceSources } from "../../application/investigation/recordEvidenceSources.js"; import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js"; -import type { AnalysisViewRequest } from "../../domain/analysisView/analysisView.js"; +import type { AnalysisViewRequest } from "../../domain/analysisView/analysisViewRequest.js"; import type { Evidence } from "../../domain/evidence.js"; import { jsonValueSchema } from "../../domain/jsonValue.js"; import { ok } from "../../domain/result.js"; diff --git a/src/server/toolResult.ts b/src/server/toolResult.ts index 459b051776..90933004d1 100644 --- a/src/server/toolResult.ts +++ b/src/server/toolResult.ts @@ -3,7 +3,7 @@ import { isNativeInventoryOperation } from "../domain/native/nativeInventory.js" import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server"; import { ToolResultDelivery } from "./ToolResultDelivery.js"; import { INSPECT_ANALYSIS_VIEW_OPERATION } from "../domain/analysisView/analysisViewErrors.js"; -import { inspectAnalysisViewInputSchema } from "../domain/analysisView/analysisView.js"; +import { inspectAnalysisViewInputSchema } from "../domain/analysisView/analysisViewRequest.js"; import { smallerBatchDecompileSelection } from "../domain/analysisView/batchDecompileView.js"; import type { Evidence } from "../domain/evidence.js"; import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js"; diff --git a/src/sqlite/SqliteDatabaseFailures.ts b/src/sqlite/SqliteDatabaseFailures.ts index da6220b6e4..b6e0f9e917 100644 --- a/src/sqlite/SqliteDatabaseFailures.ts +++ b/src/sqlite/SqliteDatabaseFailures.ts @@ -1,5 +1,5 @@ import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; -import { stableArtifactInputRecovery } from "../artifacts/StableArtifactFailure.js"; +import { stableArtifactInputFailure } from "../artifacts/StableArtifactFailure.js"; import { ArtifactFileCleanupFailure, bindArtifactFileCleanup, @@ -169,17 +169,7 @@ export const sqliteDatabaseFailure = ( return new AnalysisCancelledError(operation, { cause: artifactFileCancellationCause(cause), }); - return new AnalysisInputError( - operation, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, operation); } if (cause instanceof Error && cause.name === "AbortError") return new AnalysisCancelledError(operation, { diff --git a/src/v8/V8CodeCacheProvider.ts b/src/v8/V8CodeCacheProvider.ts index 37c60c8551..20622f3d0d 100644 --- a/src/v8/V8CodeCacheProvider.ts +++ b/src/v8/V8CodeCacheProvider.ts @@ -5,22 +5,20 @@ import { readStableArtifact, } from "../artifacts/readStableArtifact.js"; import { - artifactFileCancellationCause, artifactFilePrimaryFacts, artifactSelectedPathErrno, type ArtifactFilePrimaryFacts, } from "../artifacts/ArtifactFileOperation.js"; import { projectArtifactCloseFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, } from "../artifacts/StableArtifactFailure.js"; import type { ExecutionOptions } from "../application/AnalysisProvider.js"; import type { V8CodeCachePort } from "../application/v8CodeCache/V8CodeCachePort.js"; import { AnalysisAccessDeniedError, - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -200,27 +198,12 @@ const v8CodeCacheFailurePrimary = ( message: cause.message, }, ]); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { - cause, - }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -244,17 +227,7 @@ const v8CodeCacheFailurePrimary = ( ); } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } if (cause instanceof Error && "code" in cause) { if (cause.code === "EACCES" || cause.code === "EPERM") diff --git a/tests/acceptance/analysisViewCallerPath.test.ts b/tests/acceptance/analysisViewCallerPath.test.ts index ef19e38abf..20a06db34b 100644 --- a/tests/acceptance/analysisViewCallerPath.test.ts +++ b/tests/acceptance/analysisViewCallerPath.test.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { expect } from "vitest"; import { toolContract } from "../../src/contracts/toolContracts.js"; import { parseEvidence } from "../../src/domain/evidence.js"; -import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; import { analysisViewJavaScriptEvidence, analysisViewLayoutEvidence, diff --git a/tests/acceptance/apkSignatureLimitsCliMcp.test.ts b/tests/acceptance/apkSignatureLimitsCliMcp.test.ts index ef85387154..8537dad4e1 100644 --- a/tests/acceptance/apkSignatureLimitsCliMcp.test.ts +++ b/tests/acceptance/apkSignatureLimitsCliMcp.test.ts @@ -5,7 +5,7 @@ import { expect } from "vitest"; import { parseEvidence } from "../../src/domain/evidence.js"; import { analysisErrorProjectionSchema } from "../../src/contracts/errorSchemas.js"; -import { apkSignatureSummaryViewResultSchema } from "../../src/domain/analysisView/analysisView.js"; +import { apkSignatureSummaryViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; import { parseMcpToolError } from "../fixtures/mcpToolError.js"; import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; diff --git a/tests/acceptance/apkSignatureViewsCliMcp.test.ts b/tests/acceptance/apkSignatureViewsCliMcp.test.ts index ba60352800..cc508281a7 100644 --- a/tests/acceptance/apkSignatureViewsCliMcp.test.ts +++ b/tests/acceptance/apkSignatureViewsCliMcp.test.ts @@ -6,7 +6,7 @@ import { parseEvidence } from "../../src/domain/evidence.js"; import { apkSignaturePageViewResultSchema, apkSignatureSummaryViewResultSchema, -} from "../../src/domain/analysisView/analysisView.js"; +} from "../../src/domain/analysisView/analysisViewResult.js"; import { apkDigestRowSchema, type apkSignatureViewRequestSchema, diff --git a/tests/acceptance/applications/apiContract.test.ts b/tests/acceptance/applications/apiContract.test.ts index 0302a05e2d..d5b69828cb 100644 --- a/tests/acceptance/applications/apiContract.test.ts +++ b/tests/acceptance/applications/apiContract.test.ts @@ -12,7 +12,7 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js"; import { apiContractSchema } from "../../../src/domain/apiContract/apiContract.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; type McpCall = Awaited>["call"]; const fixture = new URL( diff --git a/tests/acceptance/applications/erlangBeamViews.test.ts b/tests/acceptance/applications/erlangBeamViews.test.ts index 02bb9ea33a..2f70934a90 100644 --- a/tests/acceptance/applications/erlangBeamViews.test.ts +++ b/tests/acceptance/applications/erlangBeamViews.test.ts @@ -7,7 +7,7 @@ import { toolContract } from "../../../src/contracts/toolContracts.js"; import { erlangBeamSchema } from "../../../src/domain/erlang/beam.js"; import { evidenceSchema } from "../../../src/domain/evidence.js"; import { digestCanonicalValue } from "../../../src/domain/canonicalDigest.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { erlangBeamFixture, erlangBeamChunksFixture, diff --git a/tests/acceptance/applications/graphqlContract.test.ts b/tests/acceptance/applications/graphqlContract.test.ts index cc21b4c963..0813b95bb7 100644 --- a/tests/acceptance/applications/graphqlContract.test.ts +++ b/tests/acceptance/applications/graphqlContract.test.ts @@ -11,7 +11,7 @@ import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js"; import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js"; import { graphqlContractSchema } from "../../../src/domain/apiContract/graphqlContract.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; const fixture = new URL( "../../fixtures/graphql/swapi.graphql.txt", diff --git a/tests/acceptance/applications/javascriptContentDeduplicatedModulePaths.test.ts b/tests/acceptance/applications/javascriptContentDeduplicatedModulePaths.test.ts index ae95a675b2..6d67e7e740 100644 --- a/tests/acceptance/applications/javascriptContentDeduplicatedModulePaths.test.ts +++ b/tests/acceptance/applications/javascriptContentDeduplicatedModulePaths.test.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { expect } from "vitest"; import { parseEvidence } from "../../../src/domain/evidence.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; diff --git a/tests/acceptance/applications/javascriptExportBindingModuleView.test.ts b/tests/acceptance/applications/javascriptExportBindingModuleView.test.ts index f496b35277..42ab01bb52 100644 --- a/tests/acceptance/applications/javascriptExportBindingModuleView.test.ts +++ b/tests/acceptance/applications/javascriptExportBindingModuleView.test.ts @@ -3,7 +3,7 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; diff --git a/tests/acceptance/applications/javascriptWindowOptionsCli.test.ts b/tests/acceptance/applications/javascriptWindowOptionsCli.test.ts index 2024250681..847c77693d 100644 --- a/tests/acceptance/applications/javascriptWindowOptionsCli.test.ts +++ b/tests/acceptance/applications/javascriptWindowOptionsCli.test.ts @@ -5,7 +5,7 @@ import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; cliTest( "retains constant window preferences and inferred source provenance through CLI, MCP and fact views", diff --git a/tests/acceptance/applications/nativeExportsViews.test.ts b/tests/acceptance/applications/nativeExportsViews.test.ts index d249d36a9c..ed828cca72 100644 --- a/tests/acceptance/applications/nativeExportsViews.test.ts +++ b/tests/acceptance/applications/nativeExportsViews.test.ts @@ -6,7 +6,7 @@ import { readEvidenceBundle } from "../../../src/application/EvidenceBundleFiles import { toolContract } from "../../../src/contracts/toolContracts.js"; import { evidenceSchema } from "../../../src/domain/evidence.js"; import { digestCanonicalValue } from "../../../src/domain/canonicalDigest.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; import { elfExportImage, diff --git a/tests/acceptance/applications/playwrightTrace.test.ts b/tests/acceptance/applications/playwrightTrace.test.ts index 10a5b21e82..9c3b72ae25 100644 --- a/tests/acceptance/applications/playwrightTrace.test.ts +++ b/tests/acceptance/applications/playwrightTrace.test.ts @@ -12,7 +12,7 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js"; import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; import { playwrightTraceSchema } from "../../../src/domain/playwrightTrace/playwrightTrace.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js"; type McpCall = Awaited>["call"]; diff --git a/tests/acceptance/applications/progressiveAnalysisViews.test.ts b/tests/acceptance/applications/progressiveAnalysisViews.test.ts index a6c84d772c..e217fb5c3e 100644 --- a/tests/acceptance/applications/progressiveAnalysisViews.test.ts +++ b/tests/acceptance/applications/progressiveAnalysisViews.test.ts @@ -6,7 +6,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js"; import { apiContractSchema } from "../../../src/domain/apiContract/apiContract.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import { toolOutputSchemaWithMetadata } from "../../../src/contracts/toolSchemaMetadata.js"; diff --git a/tests/acceptance/applications/pythonPycViews.test.ts b/tests/acceptance/applications/pythonPycViews.test.ts index 486420d166..aaf094c9b0 100644 --- a/tests/acceptance/applications/pythonPycViews.test.ts +++ b/tests/acceptance/applications/pythonPycViews.test.ts @@ -7,7 +7,7 @@ import { toolContract } from "../../../src/contracts/toolContracts.js"; import { evidenceSchema } from "../../../src/domain/evidence.js"; import { pythonPycSchema } from "../../../src/domain/python/pythonPyc.js"; import { digestCanonicalValue } from "../../../src/domain/canonicalDigest.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { marshalCode, pythonPycHeader, diff --git a/tests/acceptance/applications/staticObservedEndpoints.test.ts b/tests/acceptance/applications/staticObservedEndpoints.test.ts index b5581fc496..9905ce3b2a 100644 --- a/tests/acceptance/applications/staticObservedEndpoints.test.ts +++ b/tests/acceptance/applications/staticObservedEndpoints.test.ts @@ -20,7 +20,7 @@ import { staticObservedEndpointComparisonSchema, type StaticObservedEndpointComparison, } from "../../../src/domain/javascript/staticObservedEndpointSchemas.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { webNetworkCaptureSchema } from "../../../src/domain/webNetworkCapture.js"; const prepareInputs = async (cli: TestCli) => { diff --git a/tests/acceptance/artifactComparisonEspImage.test.ts b/tests/acceptance/artifactComparisonEspImage.test.ts index ee566a1c75..2c297871b8 100644 --- a/tests/acceptance/artifactComparisonEspImage.test.ts +++ b/tests/acceptance/artifactComparisonEspImage.test.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { expect, it } from "vitest"; import { ESP32C3_APP_SEGMENTS, espImage } from "../fixtures/espImage.js"; -import { artifactInventorySummaryViewResultSchema } from "../../src/domain/analysisView/analysisView.js"; +import { artifactInventorySummaryViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; import { artifactComparisonResultSchema } from "../../src/domain/artifactComparison.js"; import { artifactInspectionResultSchema } from "../../src/domain/artifactInspection.js"; import { artifactInventoryResultSchema } from "../../src/domain/artifactGraph.js"; diff --git a/tests/acceptance/artifactComparisonHistoricalSummary.test.ts b/tests/acceptance/artifactComparisonHistoricalSummary.test.ts index a86c668946..f3860482a3 100644 --- a/tests/acceptance/artifactComparisonHistoricalSummary.test.ts +++ b/tests/acceptance/artifactComparisonHistoricalSummary.test.ts @@ -2,10 +2,8 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it } from "vitest"; -import { - artifactInventorySummaryViewResultSchema, - sealAnalysisView, -} from "../../src/domain/analysisView/analysisView.js"; +import { sealAnalysisView } from "../../src/domain/analysisView/analysisView.js"; +import { artifactInventorySummaryViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; import { artifactComparisonResultSchema } from "../../src/domain/artifactComparison.js"; import { digestCanonicalValue } from "../../src/domain/canonicalDigest.js"; import { diff --git a/tests/acceptance/dotnetBundleJourney.test.ts b/tests/acceptance/dotnetBundleJourney.test.ts index f448357943..2e946e94bf 100644 --- a/tests/acceptance/dotnetBundleJourney.test.ts +++ b/tests/acceptance/dotnetBundleJourney.test.ts @@ -14,7 +14,7 @@ import { parseEvidence } from "../../src/domain/evidence.js"; import { analysisViewResultSchema, artifactInventorySummaryViewResultSchema, -} from "../../src/domain/analysisView/analysisView.js"; +} from "../../src/domain/analysisView/analysisViewResult.js"; import { workspaceCliTest as test } from "../support/cli/workspaceCliFixture.js"; import { createMcpSchemaValidation } from "../fixtures/mcpSchemaValidation.js"; diff --git a/tests/acceptance/godotPck.test.ts b/tests/acceptance/godotPck.test.ts index bb0d56e9f9..32c4a030af 100644 --- a/tests/acceptance/godotPck.test.ts +++ b/tests/acceptance/godotPck.test.ts @@ -12,7 +12,7 @@ import { } from "../fixtures/godotPck.js"; import { elf } from "../../src/domain/binaryTarget.fixture.js"; import { readEvidenceBundle } from "../../src/application/EvidenceBundleFiles.js"; -import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../src/domain/evidence.js"; import { artifactInspectionResultSchema } from "../../src/domain/artifactInspection.js"; import { diff --git a/tests/acceptance/gpuCodeObjectsCli.test.ts b/tests/acceptance/gpuCodeObjectsCli.test.ts index 2967daf08f..c713630779 100644 --- a/tests/acceptance/gpuCodeObjectsCli.test.ts +++ b/tests/acceptance/gpuCodeObjectsCli.test.ts @@ -8,7 +8,7 @@ import { gpuCodeObjectsSchema } from "../../src/domain/gpu/gpuCodeObjects.js"; import { gpuCodeObjectsPageViewResultSchema, gpuCodeObjectsSummaryViewResultSchema, -} from "../../src/domain/analysisView/analysisView.js"; +} from "../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence, createEvidence } from "../../src/domain/evidence.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js"; diff --git a/tests/acceptance/il2cppMetadataCli.test.ts b/tests/acceptance/il2cppMetadataCli.test.ts index a90187d1ed..9e162c60cd 100644 --- a/tests/acceptance/il2cppMetadataCli.test.ts +++ b/tests/acceptance/il2cppMetadataCli.test.ts @@ -15,7 +15,7 @@ import { toolContract } from "../../src/contracts/toolContracts.js"; import { analysisViewResultSchema, il2cppMetadataSummaryViewResultSchema, -} from "../../src/domain/analysisView/analysisView.js"; +} from "../../src/domain/analysisView/analysisViewResult.js"; import { createEvidence, parseEvidence } from "../../src/domain/evidence.js"; import { createEvidenceBundle, diff --git a/tests/acceptance/inesContainer.test.ts b/tests/acceptance/inesContainer.test.ts index 589d4d63cc..74e82be581 100644 --- a/tests/acceptance/inesContainer.test.ts +++ b/tests/acceptance/inesContainer.test.ts @@ -7,7 +7,7 @@ import { artifactInventoryResultSchema, artifactExtractionResultSchema, } from "../../src/domain/artifactGraph.js"; -import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../src/domain/evidence.js"; import { cliTest } from "../support/cli/cliFixture.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; diff --git a/tests/acceptance/javascriptRoutesCli.test.ts b/tests/acceptance/javascriptRoutesCli.test.ts index 786ff5253c..dfb744b66e 100644 --- a/tests/acceptance/javascriptRoutesCli.test.ts +++ b/tests/acceptance/javascriptRoutesCli.test.ts @@ -7,7 +7,7 @@ import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; import { cliTest } from "../support/cli/cliFixture.js"; import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js"; import { parseEvidence } from "../../src/domain/evidence.js"; -import { javaScriptRoutesViewResultSchema } from "../../src/domain/analysisView/analysisView.js"; +import { javaScriptRoutesViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; const routeViewFromEvidence = (evidence: unknown) => javaScriptRoutesViewResultSchema.parse( diff --git a/tests/acceptance/managedMemberRecovery.test.ts b/tests/acceptance/managedMemberRecovery.test.ts index 88d31f562d..904039c4b0 100644 --- a/tests/acceptance/managedMemberRecovery.test.ts +++ b/tests/acceptance/managedMemberRecovery.test.ts @@ -13,7 +13,7 @@ import { managedMemberInspectionSchema } from "../../src/domain/managed/managedA import { analysisViewResultSchema, managedMemberSummaryViewResultSchema, -} from "../../src/domain/analysisView/analysisView.js"; +} from "../../src/domain/analysisView/analysisViewResult.js"; import { buildManagedMemberCapacityFixture } from "../fixtures/managedMemberCapacity.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; import { cliTest } from "../support/cli/cliFixture.js"; diff --git a/tests/acceptance/modelArtifactCli.test.ts b/tests/acceptance/modelArtifactCli.test.ts index d6d39fc5fd..fcf58c8c25 100644 --- a/tests/acceptance/modelArtifactCli.test.ts +++ b/tests/acceptance/modelArtifactCli.test.ts @@ -10,7 +10,7 @@ import { import { modelArtifactSummaryViewResultSchema, analysisViewResultSchema, -} from "../../src/domain/analysisView/analysisView.js"; +} from "../../src/domain/analysisView/analysisViewResult.js"; import { createEvidence, parseEvidence } from "../../src/domain/evidence.js"; import { readEvidenceBundle } from "../../src/application/EvidenceBundleFiles.js"; import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js"; diff --git a/tests/acceptance/rustDependencyViews.test.ts b/tests/acceptance/rustDependencyViews.test.ts index 612d5d2aff..a5f68bda09 100644 --- a/tests/acceptance/rustDependencyViews.test.ts +++ b/tests/acceptance/rustDependencyViews.test.ts @@ -4,7 +4,7 @@ import { expect } from "vitest"; import { parseEvidence, createEvidence } from "../../src/domain/evidence.js"; import { createEvidenceBundle } from "../../src/domain/evidenceBundle.js"; import { rustBinarySchema } from "../../src/domain/rust/rustBinary.js"; -import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; import { readEvidenceBundle } from "../../src/application/EvidenceBundleFiles.js"; import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js"; import { parseMcpToolError } from "../fixtures/mcpToolError.js"; diff --git a/tests/acceptance/tools/protobufCapture.test.ts b/tests/acceptance/tools/protobufCapture.test.ts index c161b36352..f072d19179 100644 --- a/tests/acceptance/tools/protobufCapture.test.ts +++ b/tests/acceptance/tools/protobufCapture.test.ts @@ -24,7 +24,7 @@ import { inspectWebNetworkCaptureInputSchema, } from "../../../src/domain/webNetworkCapture.js"; import { readEvidenceBundle } from "../../../src/application/EvidenceBundleFiles.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; diff --git a/tests/acceptance/tools/protobufDescriptorFiles.test.ts b/tests/acceptance/tools/protobufDescriptorFiles.test.ts index f37f1fead7..b65b98ea3a 100644 --- a/tests/acceptance/tools/protobufDescriptorFiles.test.ts +++ b/tests/acceptance/tools/protobufDescriptorFiles.test.ts @@ -13,7 +13,7 @@ import { join } from "node:path"; import { expect } from "vitest"; import { z } from "zod"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { webNetworkCaptureSchema } from "../../../src/domain/webNetworkCapture.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; diff --git a/tests/acceptance/tools/protobufDescriptorIdentity.test.ts b/tests/acceptance/tools/protobufDescriptorIdentity.test.ts index d57476ad7e..0fb0fc5b99 100644 --- a/tests/acceptance/tools/protobufDescriptorIdentity.test.ts +++ b/tests/acceptance/tools/protobufDescriptorIdentity.test.ts @@ -10,7 +10,7 @@ import { join } from "node:path"; import { expect } from "vitest"; import { z } from "zod"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { webNetworkCaptureSchema } from "../../../src/domain/webNetworkCapture.js"; import { protobufHar } from "../../fixtures/protobufCapture.js"; diff --git a/tests/acceptance/tools/qlogCapture.test.ts b/tests/acceptance/tools/qlogCapture.test.ts index 4bda480872..eea8eedca1 100644 --- a/tests/acceptance/tools/qlogCapture.test.ts +++ b/tests/acceptance/tools/qlogCapture.test.ts @@ -8,7 +8,7 @@ import { CAPTURE_DECODER_HAS_MEMORY_BOUND as hasMemoryBound, CAPTURE_RUNTIME_SUPPORTED as runtimeSupported, } from "../../fixtures/captureDecoderLimits.js"; -import { webNetworkCaptureSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { webNetworkCaptureSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { webNetworkCaptureSchema, diff --git a/tests/acceptance/tools/unavailableNativeCollectionViews.test.ts b/tests/acceptance/tools/unavailableNativeCollectionViews.test.ts index 245048f02e..c44d47d826 100644 --- a/tests/acceptance/tools/unavailableNativeCollectionViews.test.ts +++ b/tests/acceptance/tools/unavailableNativeCollectionViews.test.ts @@ -2,7 +2,7 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { nativeLoadImageObservationFixture, diff --git a/tests/boundary/browser/webSourceMapHttpStatus.test.ts b/tests/boundary/browser/webSourceMapHttpStatus.test.ts index 13ec2a408e..9339acddab 100644 --- a/tests/boundary/browser/webSourceMapHttpStatus.test.ts +++ b/tests/boundary/browser/webSourceMapHttpStatus.test.ts @@ -9,7 +9,7 @@ import { analyzeWebBundleInputSchema, webSourceMapsSchema, } from "../../../src/domain/webBundleAnalysis.js"; -import { WEB_SOURCE_MAP_LIMITS } from "../../../src/domain/webSourceLocation.js"; +import { WEB_SOURCE_MAP_LIMITS } from "../../../src/domain/sourceMapLimits.js"; it.each([ { status: 300, followsRedirect: false }, diff --git a/tests/boundary/cli/electronWrappedCallPaths.test.ts b/tests/boundary/cli/electronWrappedCallPaths.test.ts index 40909faeb3..943246ac6c 100644 --- a/tests/boundary/cli/electronWrappedCallPaths.test.ts +++ b/tests/boundary/cli/electronWrappedCallPaths.test.ts @@ -14,7 +14,7 @@ import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplicat import { analysisViewResultSchema, javascriptApplicationSummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; const sourcePositionSchema = z.object({ line: z.number(), column: z.number() }); const callPathConditionSchema = z.object({ diff --git a/tests/boundary/cli/fridaStatusViewCli.test.ts b/tests/boundary/cli/fridaStatusViewCli.test.ts index 67fb617c5f..501367d585 100644 --- a/tests/boundary/cli/fridaStatusViewCli.test.ts +++ b/tests/boundary/cli/fridaStatusViewCli.test.ts @@ -2,7 +2,7 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { fridaSessionStatusSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { fridaSessionStatusSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; diff --git a/tests/boundary/cli/managedMemberSummaryCli.test.ts b/tests/boundary/cli/managedMemberSummaryCli.test.ts index 5f74d5118d..8a571c1438 100644 --- a/tests/boundary/cli/managedMemberSummaryCli.test.ts +++ b/tests/boundary/cli/managedMemberSummaryCli.test.ts @@ -19,7 +19,7 @@ import { import { analysisViewResultSchema, managedMemberSummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; import { parseEvidenceBundle } from "../../../src/domain/evidenceBundle.js"; import { diff --git a/tests/boundary/filesystem/ciChangeScope.test.ts b/tests/boundary/filesystem/ciChangeScope.test.ts index 01bec4bd68..425baf87e2 100644 --- a/tests/boundary/filesystem/ciChangeScope.test.ts +++ b/tests/boundary/filesystem/ciChangeScope.test.ts @@ -106,7 +106,7 @@ async function classify( it.each([ ["bridge/native/rea_lldb_tracer.py", "python"], ["bridge/pwndbg/test_core_context.py", "python"], - ["tests/support/analysis-efficiency/test_signal_oracle.py", "python"], + ["tests/support/analysis-efficiency/test_catalog_eval.py", "python"], ["scripts/run-python-tests.py", "python"], ["scripts/verify-browser-request.mjs", "browser"], ["scripts/lib/browser-request-e2e.mjs", "browser"], diff --git a/tests/boundary/filesystem/hermesClientConfiguration.test.ts b/tests/boundary/filesystem/hermesClientConfiguration.test.ts index 19aa865c92..3ab316f37c 100644 --- a/tests/boundary/filesystem/hermesClientConfiguration.test.ts +++ b/tests/boundary/filesystem/hermesClientConfiguration.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { userInfo } from "node:os"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { parse as parseYaml } from "yaml"; +import { isMap, parse as parseYaml, parseDocument } from "yaml"; import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js"; @@ -42,7 +42,257 @@ const hermesPath = ( ({ name }) => name === "hermes", ); +describe("Hermes registration source retention", () => { + it("round-trips setup and removal while other servers remain inherited", async () => { + const home = await createTestTempDirectory( + "rea-hermes-inherited-lifecycle-", + ); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = + "shared: &shared\n mcp_servers:\n other: {command: other-server}\n<<: *shared\n"; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await configureClientConfiguration(client, {}, command), + ).toMatchObject({ + status: "configured", + }); + expect( + await configureClientConfiguration(client, {}, command), + ).toMatchObject({ + status: "unchanged", + }); + expect(await systemUninstallHost(home).removeClient(client)).toMatchObject({ + status: "removed", + }); + const written = await readFile(path, "utf8"); + const document = parseYaml(written, { version: "1.1" }) as { + mcp_servers: Record; + }; + expect(document.mcp_servers).toEqual({ + other: { command: "other-server" }, + }); + expect( + parseYaml(written.replace("other-server", "new-server"), { + version: "1.1", + }), + ).toMatchObject({ + mcp_servers: { other: { command: "new-server" } }, + }); + }); + + it("keeps other servers inherited when setup updates REA", async () => { + const home = await createTestTempDirectory("rea-hermes-inherited-setup-"); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = + "shared: &shared\n mcp_servers:\n rea: {command: rea, args: [mcp]}\n other: {command: other-server}\n<<: *shared\n"; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await configureClientConfiguration(client, { CUSTOM: "value" }, command), + ).toMatchObject({ status: "configured" }); + const written = await readFile(path, "utf8"); + expect(parseYaml(written, { version: "1.1" })).toMatchObject({ + mcp_servers: { + rea: { env: { CUSTOM: "value" } }, + other: { command: "other-server" }, + }, + }); + expect( + parseYaml(written.replace("other-server", "new-server"), { + version: "1.1", + }), + ).toMatchObject({ mcp_servers: { other: { command: "new-server" } } }); + expect(await readFile(`${path}.rea.backup`, "utf8")).toBe(original); + }); +}); + +describe("Hermes inherited registration removal", () => { + it("does not remove an override if that would reactivate an inherited registration", async () => { + const home = await createTestTempDirectory( + "rea-hermes-inherited-override-", + ); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = + "shared: &shared\n mcp_servers:\n rea: {command: rea, args: [mcp]}\n other: {command: other-server}\n<<: *shared\n"; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await configureClientConfiguration(client, { CUSTOM: "value" }, command), + ).toMatchObject({ status: "configured" }); + const configured = await readFile(path, "utf8"); + expect(await systemUninstallHost(home).removeClient(client)).toMatchObject({ + status: "failed", + detail: expect.stringContaining("inherited from a YAML merge"), + }); + expect(await readFile(path, "utf8")).toBe(configured); + }); + + it.each([ + "shared: &shared\n rea: {command: rea, args: [mcp]}\nmcp_servers:\n <<: *shared\n", + "shared: &shared\n mcp_servers:\n rea: {command: rea, args: [mcp]}\n<<: *shared\n", + ])( + "reports an inherited registration without writing its shared source", + async (original) => { + const home = await createTestTempDirectory( + "rea-hermes-inherited-server-", + ); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await systemUninstallHost(home).removeClient(client), + ).toMatchObject({ + status: "failed", + detail: expect.stringContaining("inherited from a YAML merge"), + }); + expect(await readFile(path, "utf8")).toBe(original); + await expect(readFile(`${path}.rea.backup`, "utf8")).rejects.toThrow(); + }, + ); + + it("updates a registration selected through an aliased YAML key", async () => { + const home = await createTestTempDirectory("rea-hermes-key-alias-"); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = + "server_name: &server_name rea\nmcp_servers:\n ? *server_name\n :\n command: rea\n args: [mcp]\n marker: retained\n"; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await configureClientConfiguration(client, { CUSTOM: "value" }, command), + ).toMatchObject({ status: "configured" }); + const written = await readFile(path, "utf8"); + expect(written).toContain("*server_name"); + const servers = parseDocument(written, { version: "1.1" }).get( + "mcp_servers", + true, + ); + expect(isMap(servers) && servers.items).toHaveLength(1); + expect(parseYaml(written, { version: "1.1" })).toMatchObject({ + mcp_servers: { rea: { marker: "retained", env: { CUSTOM: "value" } } }, + }); + }); + + it("retains inherited registration settings through setup", async () => { + const home = await createTestTempDirectory("rea-hermes-merged-fields-"); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = + "defaults: &defaults\n metadata: {marker: old}\nmcp_servers:\n rea:\n <<: *defaults\n command: rea\n args: [mcp]\n"; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await configureClientConfiguration(client, { CUSTOM: "value" }, command), + ).toMatchObject({ status: "configured" }); + const written = await readFile(path, "utf8"); + expect(written).toContain("<<: *defaults"); + const changed = written.replace("marker: old", "marker: new"); + expect(parseYaml(changed, { version: "1.1" })).toMatchObject({ + mcp_servers: { rea: { metadata: { marker: "new" } } }, + }); + expect(await readFile(`${path}.rea.backup`, "utf8")).toBe(original); + }); + + it("preserves distinct keys beside a changed launcher field", async () => { + const home = await createTestTempDirectory("rea-hermes-mixed-fields-"); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = + 'mcp_servers:\n rea:\n command: rea\n args: [mcp]\n 1: numeric\n "1": string\n'; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await configureClientConfiguration(client, { CUSTOM: "value" }, command), + ).toMatchObject({ status: "configured" }); + const written = await readFile(path, "utf8"); + expect(written).toContain(' 1: numeric\n "1": string'); + }); + + it.each([ + ' 1: numeric\n "1": string', + " 2026-10-14T00:00:00.123456Z: first\n 2026-10-14T00:00:00.123457Z: second", + ])( + "retains distinct YAML keys in an existing registration: %s", + async (keys) => { + const home = await createTestTempDirectory("rea-hermes-mixed-keys-"); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = `mcp_servers:\n rea:\n command: rea\n args: [mcp]\n metadata:\n${keys}\n`; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + const result = await configureClientConfiguration( + client, + { CUSTOM: "value" }, + command, + ); + expect(result.status, JSON.stringify(result)).toBe("configured"); + const written = await readFile(path, "utf8"); + expect(written).toContain(keys); + expect(await readFile(`${path}.rea.backup`, "utf8")).toBe(original); + }, + ); +}); + describe("Hermes configuration paths", () => { + it("preserves unrelated distinct timestamp keys during registration", async () => { + const home = await createTestTempDirectory("rea-hermes-timestamp-keys-"); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = + "history:\n 2026-10-14T00:00:00.123456Z: first\n 2026-10-14T00:00:00.123457Z: second\n"; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await configureClientConfiguration(client, {}, command), + ).toMatchObject({ + status: "configured", + }); + const written = await readFile(path, "utf8"); + expect(written).toContain(original.trimEnd()); + expect(await readFile(`${path}.rea.backup`, "utf8")).toBe(original); + }); + it.skipIf(process.platform === "win32")( "configures the filesystem-selected Hermes home through symlink parent traversal", async () => { diff --git a/tests/boundary/filesystem/kimiClientConfiguration.test.ts b/tests/boundary/filesystem/kimiClientConfiguration.test.ts index 2602171063..925e5212ad 100644 --- a/tests/boundary/filesystem/kimiClientConfiguration.test.ts +++ b/tests/boundary/filesystem/kimiClientConfiguration.test.ts @@ -474,15 +474,18 @@ describe("Kimi Code transport validation", () => { }) ).find(({ client: name }) => name === "kimi"), ).toMatchObject({ state: "aligned", command }); + // The explicit selector already chooses stdio, so setup keeps it and + // has nothing to rewrite. + expect(await configureClientConfiguration(client, {}, command)).toEqual({ + status: "unchanged", + }); + expect(await configureClientConfiguration(client, {}, command)).toEqual({ + status: "unchanged", + }); expect( - (await configureClientConfiguration(client, {}, command)).status, - ).toBe("configured"); - expect(await configureClientConfiguration(client, {}, command)).toEqual({ - status: "unchanged", - }); - expect(await configureClientConfiguration(client, {}, command)).toEqual({ - status: "unchanged", - }); + JSON.parse(await readFile(client.configPath, "utf8")).mcpServers.rea + .transport, + ).toBe("stdio"); expect( ( await readClientRegistrationStatuses(home, undefined, { diff --git a/tests/boundary/filesystem/webSourceLocationArtifacts.test.ts b/tests/boundary/filesystem/webSourceLocationArtifacts.test.ts index c443a88aca..cc90bebce4 100644 --- a/tests/boundary/filesystem/webSourceLocationArtifacts.test.ts +++ b/tests/boundary/filesystem/webSourceLocationArtifacts.test.ts @@ -5,7 +5,7 @@ import { expect, it } from "vitest"; import { LocalWebSourceLocationArtifacts } from "../../../src/browser/assets/WebSourceLocationArtifacts.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { ok } from "../../../src/domain/result.js"; -import { WEB_SOURCE_MAP_LIMITS } from "../../../src/domain/webSourceLocation.js"; +import { WEB_SOURCE_MAP_LIMITS } from "../../../src/domain/sourceMapLimits.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { webSourceLocationArgs, diff --git a/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts b/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts index f49bee07cb..273163d6dd 100644 --- a/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts +++ b/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts @@ -1,6 +1,6 @@ import { cliTest } from "../../support/cli/cliFixture.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; -import { byteEntropySummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { byteEntropySummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; diff --git a/tests/boundary/mcp/analysisViewMcp.test.ts b/tests/boundary/mcp/analysisViewMcp.test.ts index fe2e4f4d08..dd6561c597 100644 --- a/tests/boundary/mcp/analysisViewMcp.test.ts +++ b/tests/boundary/mcp/analysisViewMcp.test.ts @@ -59,7 +59,7 @@ import { import { analysisViewResultSchema, il2cppMetadataSummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { BINARY_LAYOUT_TEST_PROVIDER, binaryLayoutFixture, diff --git a/tests/boundary/mcp/androidPackageSummaryMcp.test.ts b/tests/boundary/mcp/androidPackageSummaryMcp.test.ts index b9eb64f367..9503e75870 100644 --- a/tests/boundary/mcp/androidPackageSummaryMcp.test.ts +++ b/tests/boundary/mcp/androidPackageSummaryMcp.test.ts @@ -21,7 +21,7 @@ import { androidJniSummaryViewResultSchema, androidJniPageViewResultSchema, analysisViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { createEvidence, parseEvidence, diff --git a/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts b/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts index aa1f277ff6..2c19a30ad7 100644 --- a/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts +++ b/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts @@ -10,7 +10,7 @@ import type { OnTestFinishedHandler } from "vitest"; import { machoImage } from "../../../src/artifacts/apple/MachoImage.fixture.js"; import { createArtifactBinarySession } from "../../../src/composition/binary.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { createServer } from "../../../src/server/createServer.js"; diff --git a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts index 1208ff60de..bf58eed860 100644 --- a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts +++ b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts @@ -27,7 +27,7 @@ import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph import { analysisViewResultSchema, type AnalysisViewResult, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; const schemaValidation = createMcpSchemaValidation(); afterAll(() => schemaValidation.clear()); diff --git a/tests/boundary/mcp/batchDecompileViewsMcp.test.ts b/tests/boundary/mcp/batchDecompileViewsMcp.test.ts index 30847dfd1c..b3709b2837 100644 --- a/tests/boundary/mcp/batchDecompileViewsMcp.test.ts +++ b/tests/boundary/mcp/batchDecompileViewsMcp.test.ts @@ -5,7 +5,7 @@ import type { CallToolResult } from "@modelcontextprotocol/server"; import { afterAll, expect, it, onTestFinished, vi } from "vitest"; import { z } from "zod"; import { ANALYSIS_VIEW_TOOL_CONTRACTS } from "../../../src/contracts/analysisViewToolContracts.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { AnalysisOutputError } from "../../../src/domain/analysisErrorCore.js"; import { createEvidence, diff --git a/tests/boundary/mcp/bunStandaloneMcp.test.ts b/tests/boundary/mcp/bunStandaloneMcp.test.ts index 892250b021..a2201897c2 100644 --- a/tests/boundary/mcp/bunStandaloneMcp.test.ts +++ b/tests/boundary/mcp/bunStandaloneMcp.test.ts @@ -7,7 +7,7 @@ import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js"; import { createServer } from "../../../src/server/createServer.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; -import { artifactInventorySummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { artifactInventorySummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { bunModuleGraphPayload, elfAppendedBunPayload, diff --git a/tests/boundary/mcp/dartAotSummaryMcp.test.ts b/tests/boundary/mcp/dartAotSummaryMcp.test.ts index 70fa9fd11c..5e88924df4 100644 --- a/tests/boundary/mcp/dartAotSummaryMcp.test.ts +++ b/tests/boundary/mcp/dartAotSummaryMcp.test.ts @@ -9,7 +9,7 @@ import { createFlutterBuildAnalysisProvider } from "../../../src/composition/flu import { analysisViewResultSchema, dartAotSummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { flutterResultSchemas } from "../../../src/domain/flutter/flutterBuildAnalysis.js"; import type { JsonValue } from "../../../src/domain/jsonValue.js"; diff --git a/tests/boundary/mcp/deferredRuntime.test.ts b/tests/boundary/mcp/deferredRuntime.test.ts index a0735b1054..2025a6f4db 100644 --- a/tests/boundary/mcp/deferredRuntime.test.ts +++ b/tests/boundary/mcp/deferredRuntime.test.ts @@ -20,7 +20,7 @@ import { GENERATED_MCP_TOOL_CATALOG } from "../../fixtures/mcpToolCatalog.js"; import { PROMPT_CONTRACTS } from "../../../src/contracts/promptContracts.js"; import { TOOL_FAMILY_DEFINITIONS } from "../../../src/contracts/toolFamilies.js"; import { evidenceSchema } from "../../../src/domain/evidence.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { SERVER_START_FAILED } from "../../../src/main/messages.js"; import { HOT_TOOL_NAMES } from "../../../src/catalogDiscovery.js"; import { DatabaseSync } from "node:sqlite"; diff --git a/tests/boundary/mcp/denoStandaloneMcp.test.ts b/tests/boundary/mcp/denoStandaloneMcp.test.ts index 80f86fc1a3..73d7af8e57 100644 --- a/tests/boundary/mcp/denoStandaloneMcp.test.ts +++ b/tests/boundary/mcp/denoStandaloneMcp.test.ts @@ -7,7 +7,7 @@ import { expect, it } from "vitest"; import { artifactMcpCall } from "../../../scripts/lib/artifact-e2e.mjs"; import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js"; import { denoElf } from "../../../src/artifacts/deno/DenoStandalone.fixture.js"; -import { artifactInventorySummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { artifactInventorySummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; diff --git a/tests/boundary/mcp/desktopApplicationMcp.test.ts b/tests/boundary/mcp/desktopApplicationMcp.test.ts index 3dc9937e77..9d99d5b6df 100644 --- a/tests/boundary/mcp/desktopApplicationMcp.test.ts +++ b/tests/boundary/mcp/desktopApplicationMcp.test.ts @@ -8,7 +8,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { desktopApplicationSchema } from "../../../src/domain/desktopApplication.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { buildNativePeFixture } from "../../../src/dotnet/ManagedPe.fixture.js"; import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js"; diff --git a/tests/boundary/mcp/espImageMcp.test.ts b/tests/boundary/mcp/espImageMcp.test.ts index bc15db92ea..1d1082bf7e 100644 --- a/tests/boundary/mcp/espImageMcp.test.ts +++ b/tests/boundary/mcp/espImageMcp.test.ts @@ -14,7 +14,7 @@ import { import { ESP8266_REFUSAL } from "../../../src/domain/firmware/espImage.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { artifactExtractionResultSchema } from "../../../src/domain/artifactGraph.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; // The compiled stdio fixture checks advertised schemas against their dialect and // validates actual tool responses, avoiding a second source server module graph. diff --git a/tests/boundary/mcp/evmInterfaceMcp.test.ts b/tests/boundary/mcp/evmInterfaceMcp.test.ts index 808742e557..240b3bc0d0 100644 --- a/tests/boundary/mcp/evmInterfaceMcp.test.ts +++ b/tests/boundary/mcp/evmInterfaceMcp.test.ts @@ -14,7 +14,7 @@ import { AnalysisCapabilityUnavailableError, } from "../../../src/domain/analysisErrorCore.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { evmInterfaceSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { evmInterfaceSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { inspectSolidityMetadata } from "../../../src/evm/SolidityMetadata.js"; import { err, ok } from "../../../src/domain/result.js"; import { silentLogger } from "../../../src/logger.js"; diff --git a/tests/boundary/mcp/featureTraceViewsMcp.test.ts b/tests/boundary/mcp/featureTraceViewsMcp.test.ts index 70f12810ea..98190b91b4 100644 --- a/tests/boundary/mcp/featureTraceViewsMcp.test.ts +++ b/tests/boundary/mcp/featureTraceViewsMcp.test.ts @@ -5,7 +5,7 @@ import { afterAll, expect, it, onTestFinished } from "vitest"; import { z } from "zod"; import { JAVASCRIPT_FEATURE_TRACE_EXAMPLE } from "../../../src/contracts/javascript/javascriptApplicationWorkflowExamples.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; import { applicationFeatureTraceResultSchema } from "../../../src/domain/javascript/javascriptFeatureTraceSchemas.js"; import { jsonObjectSchema } from "../../../src/domain/jsonValue.js"; diff --git a/tests/boundary/mcp/fitImageMcp.test.ts b/tests/boundary/mcp/fitImageMcp.test.ts index d0e31d33c3..6d41900333 100644 --- a/tests/boundary/mcp/fitImageMcp.test.ts +++ b/tests/boundary/mcp/fitImageMcp.test.ts @@ -11,7 +11,7 @@ import { } from "../../../src/artifacts/fit/FitImage.fixture.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { artifactExtractionResultSchema } from "../../../src/domain/artifactGraph.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; // The compiled stdio fixture checks advertised schemas against their dialect and // validates actual tool responses, avoiding a second source server module graph. diff --git a/tests/boundary/mcp/fridaMcp.test.ts b/tests/boundary/mcp/fridaMcp.test.ts index 6bdc8f6203..49ec795df4 100644 --- a/tests/boundary/mcp/fridaMcp.test.ts +++ b/tests/boundary/mcp/fridaMcp.test.ts @@ -10,7 +10,7 @@ import { expandToolAvailability } from "../../../src/contracts/toolOutputSchemaP import { FridaInstrumentationManager } from "../../../src/frida/FridaInstrumentationManager.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; -import { fridaSessionStatusSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { fridaSessionStatusSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { fridaSessionStatusSchema } from "../../../src/domain/fridaSessionStatus.js"; import { evidenceBundleSummarySchema } from "../../../src/domain/evidenceRecordSummary.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; diff --git a/tests/boundary/mcp/ghidraNativeApiMcp.test.ts b/tests/boundary/mcp/ghidraNativeApiMcp.test.ts index 39c78d2dc7..fdfeb8f423 100644 --- a/tests/boundary/mcp/ghidraNativeApiMcp.test.ts +++ b/tests/boundary/mcp/ghidraNativeApiMcp.test.ts @@ -15,7 +15,7 @@ import { ok } from "../../../src/domain/result.js"; import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import { Ajv2020 } from "ajv/dist/2020.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; it("retains complete load-image facts and pages selected relocations through the advertised SDK contracts", async () => { const fixture = fixtureDosOverlapLoadImage(); diff --git a/tests/boundary/mcp/goBinaryViews.test.ts b/tests/boundary/mcp/goBinaryViews.test.ts index 78cbd5d7a7..b2a2e65579 100644 --- a/tests/boundary/mcp/goBinaryViews.test.ts +++ b/tests/boundary/mcp/goBinaryViews.test.ts @@ -11,7 +11,7 @@ import { goBinarySchema } from "../../../src/domain/go/goBinary.js"; import { analysisViewResultSchema, goBinarySummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { diff --git a/tests/boundary/mcp/godotPckMcp.test.ts b/tests/boundary/mcp/godotPckMcp.test.ts index bff61f7029..35b85033b8 100644 --- a/tests/boundary/mcp/godotPckMcp.test.ts +++ b/tests/boundary/mcp/godotPckMcp.test.ts @@ -19,7 +19,7 @@ import type { ArtifactOccurrence } from "../../../src/domain/artifactGraph.js"; import { artifactInventorySummaryViewResultSchema, analysisViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { canBorrowParsedJsonSnapshot, immutableJsonSnapshotMember, diff --git a/tests/boundary/mcp/hexRecordsMcp.test.ts b/tests/boundary/mcp/hexRecordsMcp.test.ts index 388a9369c4..0f481def0e 100644 --- a/tests/boundary/mcp/hexRecordsMcp.test.ts +++ b/tests/boundary/mcp/hexRecordsMcp.test.ts @@ -8,7 +8,7 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { SREC_S37 } from "../../../src/domain/firmware/hexRecords.fixture.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { artifactExtractionResultSchema } from "../../../src/domain/artifactGraph.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; // The compiled stdio fixture checks advertised schemas against their dialect and // validates actual tool responses, avoiding a second source server module graph. diff --git a/tests/boundary/mcp/javascriptFactsViewMcp.test.ts b/tests/boundary/mcp/javascriptFactsViewMcp.test.ts index cdf36533bf..44ca4d2125 100644 --- a/tests/boundary/mcp/javascriptFactsViewMcp.test.ts +++ b/tests/boundary/mcp/javascriptFactsViewMcp.test.ts @@ -17,7 +17,7 @@ import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; import { analysisViewResultSchema, type AnalysisViewResult, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { createJavaScriptApplicationGraph, createJavaScriptApplicationNode, diff --git a/tests/boundary/mcp/javascriptNativeHandoffIdentity.test.ts b/tests/boundary/mcp/javascriptNativeHandoffIdentity.test.ts index 29613f874d..7decd73b54 100644 --- a/tests/boundary/mcp/javascriptNativeHandoffIdentity.test.ts +++ b/tests/boundary/mcp/javascriptNativeHandoffIdentity.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { expect } from "vitest"; import { toolContract } from "../../../src/contracts/toolContracts.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createEvidence } from "../../../src/domain/evidence.js"; import { applicationFeatureTraceResultSchema, diff --git a/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts b/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts index 8ecd613779..43fa6d342c 100644 --- a/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts +++ b/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts @@ -11,7 +11,7 @@ import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { managedArtifactSummaryViewResultSchema, analysisViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { managedArtifactInspectionSchema } from "../../../src/domain/managed/managedArtifact.js"; import { createServer } from "../../../src/server/createServer.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; diff --git a/tests/boundary/mcp/managedMemberSummaryMcp.test.ts b/tests/boundary/mcp/managedMemberSummaryMcp.test.ts index 43092a482f..4ead74b429 100644 --- a/tests/boundary/mcp/managedMemberSummaryMcp.test.ts +++ b/tests/boundary/mcp/managedMemberSummaryMcp.test.ts @@ -15,7 +15,7 @@ import { import { analysisViewResultSchema, managedMemberSummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { managedMemberInspectionSchema, type ManagedMemberInspection, diff --git a/tests/boundary/mcp/nativeFeatureTraceViews.test.ts b/tests/boundary/mcp/nativeFeatureTraceViews.test.ts index 07885b3533..42df465c91 100644 --- a/tests/boundary/mcp/nativeFeatureTraceViews.test.ts +++ b/tests/boundary/mcp/nativeFeatureTraceViews.test.ts @@ -1,7 +1,7 @@ import { join } from "node:path"; import { expect, it } from "vitest"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/mcp/nativeInstructionReferencesView.test.ts b/tests/boundary/mcp/nativeInstructionReferencesView.test.ts index aa5858e5e7..45b3456725 100644 --- a/tests/boundary/mcp/nativeInstructionReferencesView.test.ts +++ b/tests/boundary/mcp/nativeInstructionReferencesView.test.ts @@ -6,10 +6,8 @@ import { createAnalysisExecution } from "../../../src/application/AnalysisProvid import { createServer } from "../../../src/server/createServer.js"; import { createToolResultDelivery } from "../../../src/server/toolResult.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { - analysisViewResultSchema, - inspectAnalysisViewInputSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +import { inspectAnalysisViewInputSchema } from "../../../src/domain/analysisView/analysisViewRequest.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { ok } from "../../../src/domain/result.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/mcp/nativeInventoryViewsMcp.test.ts b/tests/boundary/mcp/nativeInventoryViewsMcp.test.ts index 70e9027a11..e01f23dd6b 100644 --- a/tests/boundary/mcp/nativeInventoryViewsMcp.test.ts +++ b/tests/boundary/mcp/nativeInventoryViewsMcp.test.ts @@ -4,7 +4,7 @@ import { expect, it, onTestFinished } from "vitest"; import { z } from "zod"; import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; -import { inspectAnalysisViewInputSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { inspectAnalysisViewInputSchema } from "../../../src/domain/analysisView/analysisViewRequest.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { NATIVE_INVENTORY_OPERATIONS } from "../../../src/domain/native/nativeInventory.js"; import { ok } from "../../../src/domain/result.js"; diff --git a/tests/boundary/mcp/nativeLoadImageViewsMcp.test.ts b/tests/boundary/mcp/nativeLoadImageViewsMcp.test.ts index d52a2cb33e..e822344c30 100644 --- a/tests/boundary/mcp/nativeLoadImageViewsMcp.test.ts +++ b/tests/boundary/mcp/nativeLoadImageViewsMcp.test.ts @@ -6,7 +6,7 @@ import { ok } from "../../../src/domain/result.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { nativeDolLoadImageFixture, nativeRawLoadImageFixture, diff --git a/tests/boundary/mcp/nativeObservationViewsMcp.test.ts b/tests/boundary/mcp/nativeObservationViewsMcp.test.ts index 9d83777bc8..d7ce1ca7c7 100644 --- a/tests/boundary/mcp/nativeObservationViewsMcp.test.ts +++ b/tests/boundary/mcp/nativeObservationViewsMcp.test.ts @@ -8,10 +8,8 @@ import { ok } from "../../../src/domain/result.js"; import { z } from "zod"; import { afterAll, expect, it } from "vitest"; import { parseEvidence } from "../../../src/domain/evidence.js"; -import { - analysisViewResultSchema, - inspectAnalysisViewInputSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +import { inspectAnalysisViewInputSchema } from "../../../src/domain/analysisView/analysisViewRequest.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { NATIVE_PRIMITIVE_OBSERVATION_OPERATIONS } from "../../../src/domain/native/nativeObservation.js"; import { connectGhidraMcp } from "./ghidraMcpHarness.js"; import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js"; diff --git a/tests/boundary/mcp/nativeProcedureReferenceViews.test.ts b/tests/boundary/mcp/nativeProcedureReferenceViews.test.ts index 56726093b2..c8a55f72d1 100644 --- a/tests/boundary/mcp/nativeProcedureReferenceViews.test.ts +++ b/tests/boundary/mcp/nativeProcedureReferenceViews.test.ts @@ -1,7 +1,7 @@ import { join } from "node:path"; import { expect, it } from "vitest"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/mcp/nodeSeaMcp.test.ts b/tests/boundary/mcp/nodeSeaMcp.test.ts index 31472f4c4a..6db7aef411 100644 --- a/tests/boundary/mcp/nodeSeaMcp.test.ts +++ b/tests/boundary/mcp/nodeSeaMcp.test.ts @@ -14,7 +14,7 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { analysisViewResultSchema, artifactInventorySummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; const expectBlobAdmissionFailure = ( reply: Parameters[0], diff --git a/tests/boundary/mcp/pyinstallerArtifactMcp.test.ts b/tests/boundary/mcp/pyinstallerArtifactMcp.test.ts index eab462af30..8e5e40d738 100644 --- a/tests/boundary/mcp/pyinstallerArtifactMcp.test.ts +++ b/tests/boundary/mcp/pyinstallerArtifactMcp.test.ts @@ -18,7 +18,7 @@ import { import { artifactExtractionResultSchema } from "../../../src/domain/artifactGraph.js"; import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js"; import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts b/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts index 627f0b7a6d..78ef80c96f 100644 --- a/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts +++ b/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts @@ -5,7 +5,7 @@ import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { sourceToBundleComparisonResultSchema } from "../../../src/domain/javascript/sourceToBundleComparisonSchemas.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { importReferenceSource } from "../../support/referenceSourceResourceScope.js"; diff --git a/tests/boundary/mcp/wasmArtifactMcp.test.ts b/tests/boundary/mcp/wasmArtifactMcp.test.ts index 85314d2890..ace97422b5 100644 --- a/tests/boundary/mcp/wasmArtifactMcp.test.ts +++ b/tests/boundary/mcp/wasmArtifactMcp.test.ts @@ -10,7 +10,7 @@ import { readFile, realpath, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { WasmArtifactService } from "../../../src/application/wasm/WasmArtifactService.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { wasmArtifactSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { wasmArtifactSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createHash } from "node:crypto"; import { z } from "zod"; import { createServer } from "../../../src/server/createServer.js"; diff --git a/tests/boundary/mcp/webBundleViewsMcp.test.ts b/tests/boundary/mcp/webBundleViewsMcp.test.ts index 867258559e..79554ca932 100644 --- a/tests/boundary/mcp/webBundleViewsMcp.test.ts +++ b/tests/boundary/mcp/webBundleViewsMcp.test.ts @@ -6,7 +6,7 @@ import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { webBundlePageViewResultSchema, webBundleSummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { sampleWebBundleAnalysis } from "../../fixtures/webBundleAnalysis.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/process/androidMappingMcp.test.ts b/tests/boundary/process/androidMappingMcp.test.ts index 1f40065c36..a073fa01f4 100644 --- a/tests/boundary/process/androidMappingMcp.test.ts +++ b/tests/boundary/process/androidMappingMcp.test.ts @@ -11,7 +11,7 @@ import { androidResultSchemas } from "../../../src/domain/android/androidAnalysi import { androidClassSummaryViewResultSchema, androidMethodSummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; diff --git a/tests/boundary/process/hopperDeadlineCleanup.test.ts b/tests/boundary/process/hopperDeadlineCleanup.test.ts new file mode 100644 index 0000000000..f27283346f --- /dev/null +++ b/tests/boundary/process/hopperDeadlineCleanup.test.ts @@ -0,0 +1,81 @@ +import { access } from "node:fs/promises"; +import { expect, it } from "vitest"; +import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; +import { HopperDeadlineCleanup } from "../../../scripts/lib/hopper-deadline-cleanup.mjs"; +import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +it("retains the workspace and exact cleanup owner until failed profile cleanup succeeds", async () => { + const workspace = await createTestTempDirectory( + "rea-hopper-deadline-cleanup-", + ); + const profile = new ArtifactResourceScope(); + const denial = new Error("profile close denied"); + let attempts = 0; + profile.addCloseHook(async () => { + if (++attempts === 1) throw denial; + }); + const owner = new HopperDeadlineCleanup(workspace, profile); + let failure: unknown; + try { + await owner.close(); + } catch (cause) { + failure = cause; + } + expect(failure).toBeInstanceOf(AggregateError); + expect(failure).toMatchObject({ cleanupOwner: owner, errors: [denial] }); + await expect(access(workspace)).resolves.toBeUndefined(); + await owner.close(); + expect(attempts).toBe(2); + await expect(access(workspace)).rejects.toMatchObject({ code: "ENOENT" }); +}); + +it("attempts both owners and retries them after cancellation during cleanup", async () => { + const workspace = await createTestTempDirectory("rea-hopper-deadline-mixed-"); + const profile = new ArtifactResourceScope(); + const controller = new AbortController(); + const clientFailure = new Error("client close denied"); + const profileFailure = new Error("profile close interrupted"); + let release: (() => void) | undefined; + let entered: (() => void) | undefined; + const started = new Promise((resolve) => { + entered = resolve; + }); + const gate = new Promise((resolve) => { + release = resolve; + }); + let profileAttempts = 0; + profile.addCloseHook(async () => { + if (++profileAttempts === 1) { + entered?.(); + await gate; + if (controller.signal.aborted) throw profileFailure; + } + }); + let clientAttempts = 0; + const owner = new HopperDeadlineCleanup(workspace, profile); + owner.setClient({ + close: async () => + ++clientAttempts === 1 + ? { ok: false, error: clientFailure } + : { ok: true }, + }); + const first = owner.close(); + try { + expect(owner.close()).toBe(first); + await awaitAcknowledgement(started, first, "Hopper profile close hook"); + controller.abort(); + } finally { + // Release and join the parked cleanup even when an assertion fails. + release?.(); + await first.catch(() => undefined); + } + await expect(first).rejects.toMatchObject({ + cleanupOwner: owner, + errors: [clientFailure, profileFailure], + }); + await expect(access(workspace)).resolves.toBeUndefined(); + await owner.close(); + expect([clientAttempts, profileAttempts]).toEqual([2, 2]); + await expect(access(workspace)).rejects.toMatchObject({ code: "ENOENT" }); +}); diff --git a/tests/boundary/process/http3VerifierNodeTests.test.ts b/tests/boundary/process/http3VerifierNodeTests.test.ts new file mode 100644 index 0000000000..0e7be7fa6e --- /dev/null +++ b/tests/boundary/process/http3VerifierNodeTests.test.ts @@ -0,0 +1,31 @@ +import { randomUUID } from "node:crypto"; +import { expect, it } from "vitest"; +import { runOwnedCommand } from "../../../src/process/OwnedCommand.js"; + +// These verifier tests use Node's test runner and exercise the actual script +// modules. Run them in the built lane because the lifecycle module imports dist. +const runNodeTests = async (file: string): Promise => { + const result = await runOwnedCommand( + { + command: process.execPath, + arguments: ["--test", file], + cwd: process.cwd(), + hostEnvironment: process.env, + runId: `rea-http3-node-tests-${randomUUID()}`, + }, + { timeoutMs: 30_000, diagnosticBytes: 64 * 1024, acceptNonZeroExit: true }, + ); + expect(result.exitCode, result.stdout.text + result.stderr.text).toBe(0); +}; + +it("runs HTTP/3 capture assertion regressions", async () => { + await runNodeTests("scripts/lib/http3-capture-assertions.test.mjs"); +}, 60_000); + +it.skipIf(process.platform === "win32")( + "runs POSIX HTTP/3 lifecycle regressions", + async () => { + await runNodeTests("scripts/lib/http3-verifier-lifecycle.test.mjs"); + }, + 60_000, +); diff --git a/tests/boundary/process/javascriptVerifierBoundedEvidence.test.ts b/tests/boundary/process/javascriptVerifierBoundedEvidence.test.ts index 6837842304..7baac6f168 100644 --- a/tests/boundary/process/javascriptVerifierBoundedEvidence.test.ts +++ b/tests/boundary/process/javascriptVerifierBoundedEvidence.test.ts @@ -88,16 +88,12 @@ process.on('exit', () => fs.writeSync(2, 'Evidence close attempts: ' + attempts throw new Error("Missing verifier diagnostics"); if (denial === "always") { expect(result.stderr).toContain("authored Evidence close denial"); - expect(result.stderr).toContain( - "AggregateError: Verifier cleanup failed", - ); + expect(result.stderr).toContain("Verifier cleanup failed"); expect(result.stderr).toContain("cleanupLeases"); } else { expect(result.stderr).toContain("authored Evidence read failure"); expect(result.stderr).not.toContain("authored Evidence close denial"); - expect(result.stderr).not.toContain( - "AggregateError: Verifier cleanup failed", - ); + expect(result.stderr).not.toContain("Verifier cleanup failed"); } }, ); diff --git a/tests/boundary/process/verifierArtifactScope.test.ts b/tests/boundary/process/verifierArtifactScope.test.ts new file mode 100644 index 0000000000..68f1ad829c --- /dev/null +++ b/tests/boundary/process/verifierArtifactScope.test.ts @@ -0,0 +1,107 @@ +import { expect, it } from "vitest"; +import { + VerifierArtifactCleanupFailure, + withVerifierArtifactScope, +} from "../../../scripts/lib/verifier-artifact-scope.mjs"; +import type { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; +import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js"; + +it("retains the exact scope for retry and preserves the operation failure", async () => { + const primary = new Error("analysis failed"); + const cleanup = new Error("worker close failed"); + let attempts = 0; + let scope: ArtifactResourceScope | undefined; + let failure: unknown; + try { + await withVerifierArtifactScope(async (owner) => { + scope = owner; + owner.addCloseHook(async () => { + if (++attempts === 1) throw cleanup; + }); + throw primary; + }); + } catch (cause) { + failure = cause; + } + expect(failure).toBeInstanceOf(VerifierArtifactCleanupFailure); + const aggregate = failure as VerifierArtifactCleanupFailure; + expect(aggregate.errors).toEqual([primary, cleanup]); + expect(aggregate.cause).toBe(primary); + expect(aggregate.cleanupOwner).toBe(scope); + await aggregate.cleanupOwner.close(); + expect(attempts).toBe(2); +}); + +it("retains cleanup authority when the operation succeeds", async () => { + let attempts = 0; + let failure: unknown; + try { + await withVerifierArtifactScope(async (scope) => { + scope.addCloseHook(async () => { + if (++attempts === 1) throw new Error("close failed"); + }); + return "result"; + }); + } catch (cause) { + failure = cause; + } + expect(failure).toBeInstanceOf(VerifierArtifactCleanupFailure); + const aggregate = failure as VerifierArtifactCleanupFailure; + expect(aggregate.errors).toHaveLength(1); + await aggregate.cleanupOwner.close(); + expect(attempts).toBe(2); +}); + +it("finishes every close hook when cancellation arrives during final cleanup", async () => { + const controller = new AbortController(); + const cleanup = new Error("first close failed"); + let release: (() => void) | undefined; + let entered: (() => void) | undefined; + const started = new Promise((resolve) => { + entered = resolve; + }); + const gate = new Promise((resolve) => { + release = resolve; + }); + const calls: string[] = []; + let attempts = 0; + const operation = withVerifierArtifactScope(async (scope) => { + scope.addCloseHook(async () => { + calls.push("first"); + if (++attempts === 1) { + entered?.(); + await gate; + if (controller.signal.aborted) throw cleanup; + } + }); + scope.addCloseHook(async () => { + calls.push("second"); + }); + return "finished"; + }); + try { + await awaitAcknowledgement( + started, + operation, + "verifier artifact close hook", + ); + controller.abort(); + } finally { + // Release and join the parked cleanup even when the boundary is missed. + release?.(); + await operation.catch(() => undefined); + } + let failure: unknown; + try { + await operation; + } catch (cause) { + failure = cause; + } + expect(controller.signal.aborted).toBe(true); + expect(failure).toBeInstanceOf(VerifierArtifactCleanupFailure); + expect(calls).toEqual(["first", "second"]); + const aggregate = failure as VerifierArtifactCleanupFailure; + expect(aggregate.errors).toEqual([cleanup]); + await aggregate.cleanupOwner.close(); + expect(calls).toEqual(["first", "second", "first", "second"]); +}); diff --git a/tests/boundary/providers/hopper/hopperVerificationCleanup.test.ts b/tests/boundary/providers/hopper/hopperVerificationCleanup.test.ts index 2678c57026..704672da28 100644 --- a/tests/boundary/providers/hopper/hopperVerificationCleanup.test.ts +++ b/tests/boundary/providers/hopper/hopperVerificationCleanup.test.ts @@ -47,16 +47,18 @@ it.skipIf(process.platform === "win32")( try { const children = await readdir(directory); expect(children).toHaveLength(1); - expect(await snapshotHopperRuntime(parent, directory)).toEqual( + const snapshot = await snapshotHopperRuntime(parent, directory); + expect(snapshot.paths).toEqual( new Set(children.map((name) => join(directory, name))), ); + expect(snapshot.confirmedSessions.size).toBe(0); } finally { await acquired.lease.release(); } }, ); -it("reports retained sessions belonging to observed MCP processes", async () => { +it("reports sessions with unresolved identity without calling them foreign", async () => { const parent = await createTestTempDirectory("rea-cleanup-retained-session-"); const directory = join(parent, "rea-hopper-501"); const previous = join(parent, "rea-existing-session"); @@ -65,7 +67,10 @@ it("reports retained sessions belonging to observed MCP processes", async () => const retained = join(parent, "rea-retained-session"); const unrelated = join(parent, "rea-unrelated-session"); const unknown = join(parent, "rea-no-ownership-record"); - for (const path of [retained, unrelated, unknown]) await mkdir(path); + const malformed = join(parent, "rea-invalid-ownership-record"); + const invalidJson = join(parent, "rea-invalid-ownership-json"); + for (const path of [retained, unrelated, unknown, malformed, invalidJson]) + await mkdir(path); await writeFile(join(retained, "bootstrap.py"), "source-owned fixture"); await writeFile( join(retained, "ownership.json"), @@ -75,13 +80,18 @@ it("reports retained sessions belonging to observed MCP processes", async () => join(unrelated, "ownership.json"), JSON.stringify({ parent_pid: process.pid + 1 }), ); - const after = await snapshotHopperRuntime( - parent, - directory, - new Set([process.pid]), - ); + await writeFile(join(malformed, "ownership.json"), "{}"); + await writeFile(join(invalidJson, "ownership.json"), "{"); + const after = await snapshotHopperRuntime(parent, directory); - expect([...after].filter((path) => !before.has(path))).toEqual([retained]); + expect( + [...after.paths].filter((path) => !before.paths.has(path)).sort(), + ).toEqual([invalidJson, malformed, unknown, unrelated, retained].sort()); + expect( + [...after.unresolvedSessions] + .filter((path) => !before.paths.has(path)) + .sort(), + ).toEqual([invalidJson, malformed, unknown, unrelated, retained].sort()); }); it("reports an unexpected file at the shared lease root", async () => { @@ -89,11 +99,40 @@ it("reports an unexpected file at the shared lease root", async () => { const directory = join(parent, "rea-hopper-501"); await writeFile(directory, "unexpected lease root file"); - expect(await snapshotHopperRuntime(parent, directory)).toEqual( + expect((await snapshotHopperRuntime(parent, directory)).paths).toEqual( new Set([directory]), ); }); +it("requires the selected run identity before treating a session as identified", async () => { + const parent = await createTestTempDirectory("rea-hopper-run-identity-"); + const leases = join(parent, "rea-hopper-501"); + const selected = join(parent, "rea-selected"); + const reusedPid = join(parent, "rea-other-run"); + const missing = join(parent, "rea-missing-run"); + for (const path of [selected, reusedPid, missing]) await mkdir(path); + await writeFile( + join(selected, "ownership.json"), + JSON.stringify({ parent_pid: process.pid, run_id: "selected-run" }), + ); + await writeFile( + join(reusedPid, "ownership.json"), + JSON.stringify({ parent_pid: process.pid, run_id: "other-run" }), + ); + await writeFile( + join(missing, "ownership.json"), + JSON.stringify({ parent_pid: process.pid }), + ); + const observed = await snapshotHopperRuntime(parent, leases, "selected-run"); + expect([...observed.paths].sort()).toEqual( + [selected, reusedPid, missing].sort(), + ); + expect([...observed.confirmedSessions]).toEqual([selected]); + expect([...observed.unresolvedSessions].sort()).toEqual( + [reusedPid, missing].sort(), + ); +}); + const createLeaseWorkspace = async (): Promise => { // Unix socket paths must fit the host sockaddr_un limit, independently of // the test runner's potentially long redirected temporary directory. diff --git a/tests/evaluation/importCycleGuard.test.ts b/tests/evaluation/importCycleGuard.test.ts new file mode 100644 index 0000000000..dd3f8d4dc1 --- /dev/null +++ b/tests/evaluation/importCycleGuard.test.ts @@ -0,0 +1,31 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it } from "vitest"; +import { expectImportCycles } from "../fixtures/importCycles.js"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; + +it.each(["./self.js", "./%73elf.js?view=1#fragment"])( + "detects self-import %s and tracks its allowed edge", + async (specifier) => { + const root = await createTestTempDirectory("rea-import-cycle-"); + await writeFile( + join(root, "self.ts"), + `import ${JSON.stringify(specifier)};\n`, + ); + expect(() => expectImportCycles(root, [], [])).toThrow(); + expectImportCycles(root, [["self.ts"]], ["self.ts -> self.ts"]); + expect(() => expectImportCycles(root, [["self.ts"]], [])).toThrow(); + }, +); + +it("does not include cycles in test-only fixtures", async () => { + const root = await createTestTempDirectory("rea-import-cycle-"); + await writeFile( + join(root, "sample.fixture.ts"), + 'import "./sample.fixture.js";\n', + ); + await writeFile(join(root, "sample.test.ts"), 'import "./sample.test.js";\n'); + await writeFile(join(root, "sample.spec.ts"), 'import "./sample.spec.js";\n'); + await writeFile(join(root, "production.ts"), "export const value = 1;\n"); + expectImportCycles(root, [], []); +}); diff --git a/tests/evaluation/moduleBoundaries.test.ts b/tests/evaluation/moduleBoundaries.test.ts index 75f0bd4f4c..deacf4f5bb 100644 --- a/tests/evaluation/moduleBoundaries.test.ts +++ b/tests/evaluation/moduleBoundaries.test.ts @@ -1,7 +1,7 @@ import { execFile, spawnSync } from "node:child_process"; import { mkdir, rename, writeFile } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; -import { pathToFileURL } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; @@ -176,6 +176,28 @@ describe("live capability constructor ownership", () => { const execute = promisify(execFile); const verifier = resolve("scripts/verify-module-boundaries.mjs"); +const resolvePackageImport = ( + root: string, + file: string, + specifier: string, +) => { + const result = spawnSync( + process.execPath, + [ + "--experimental-import-meta-resolve", + "--input-type=module", + "-e", + "console.log(import.meta.resolve(process.argv[1], process.argv[2]))", + specifier, + pathToFileURL(join(root, file)).href, + ], + { encoding: "utf8" }, + ); + if (result.error !== undefined) throw result.error; + expect(result.status, result.stderr).toBe(0); + return fileURLToPath(result.stdout.trim()); +}; + describe("incremental module import boundaries", () => { it.each([ ["src/domain/probe.ts", "../application/Workflow.js", false], @@ -284,6 +306,72 @@ describe("incremental module import boundaries", () => { }); }); +describe("package-map module ownership", () => { + it.each([ + ["./dist/composition/worker.js"], + ["./dist/%63omposition/worker.js"], + ["./%64ist/composition/worker.js"], + ])("checks the package-map source owner for %s", async (target) => { + const root = await createTestTempDirectory("rea-module-package-"); + await mkdir(join(root, "src/application"), { recursive: true }); + await writeFile( + join(root, "package.json"), + JSON.stringify({ imports: { "#worker": target } }), + ); + const file = "src/application/probe.ts"; + await writeFile(join(root, file), 'import "#worker";\n'); + expect(resolvePackageImport(root, file, "#worker")).toBe( + join(root, "dist/composition/worker.js"), + ); + const checked = spawnSync(process.execPath, [verifier, file], { + cwd: root, + encoding: "utf8", + }); + if (checked.error !== undefined) throw checked.error; + expect(checked.status).toBe(1); + expect(checked.stderr).toContain( + "application-composition: #worker resolves to src/composition/worker.ts", + ); + }); + + it.each([ + [ + "#worker", + { + "#worker": { + import: "./dist/composition/worker.js", + default: "./dist/composition/other.js", + }, + }, + ], + ["#worker", { "#worker": ["./dist/composition/worker.js"] }], + ["#workers/worker", { "#workers/*": "./dist/composition/*.js" }], + ])( + "fails closed for unsupported package map %s", + async (specifier, imports) => { + const root = await createTestTempDirectory("rea-module-package-"); + await mkdir(join(root, "src/domain"), { recursive: true }); + await writeFile(join(root, "package.json"), JSON.stringify({ imports })); + const file = "src/domain/probe.ts"; + await writeFile( + join(root, file), + `import ${JSON.stringify(specifier)};\n`, + ); + expect(resolvePackageImport(root, file, specifier)).toBe( + join(root, "dist/composition/worker.js"), + ); + const checked = spawnSync(process.execPath, [verifier, file], { + cwd: root, + encoding: "utf8", + }); + if (checked.error !== undefined) throw checked.error; + expect(checked.status).toBe(1); + expect(checked.stderr).toContain("Cannot resolve package import"); + expect(checked.stderr).toContain("relative source import"); + }, + ); +}); + describe("module import syntax and source discovery", () => { it.each([ 'import { value } from "../android/Provider.js";', diff --git a/tests/evaluation/noBabelVisitorKeysRule.test.ts b/tests/evaluation/noBabelVisitorKeysRule.test.ts new file mode 100644 index 0000000000..c650a69395 --- /dev/null +++ b/tests/evaluation/noBabelVisitorKeysRule.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from "vitest"; +import { spawnSync } from "node:child_process"; +import { mkdir, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { RuleTester } from "oxlint/plugins-dev"; +import rule from "../../scripts/lint/rules/no-babel-visitor-keys.mjs"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; +import { writeRepositoryOxlintConfiguration } from "../fixtures/oxlintConfiguration.js"; + +RuleTester.describe = describe; +RuleTester.it = it; +new RuleTester({ languageOptions: { parserOptions: { lang: "ts" } } }).run( + "no-babel-visitor-keys", + rule, + { + valid: [ + 'const text = "VISITOR_KEYS";', + "const config = { VISITOR_KEYS: 1 }; config.VISITOR_KEYS;", + 'import * as types from "@babel/types"; types.isIdentifier(node);', + 'import { VISITOR_KEYS } from "unrelated";', + 'import * as types from "@babel/types"; function f(types: { VISITOR_KEYS: number }) { return types.VISITOR_KEYS; }', + ], + invalid: [ + { + code: 'import { VISITOR_KEYS as keys } from "@babel/types";', + errors: [{ messageId: "owner" }], + }, + { + code: 'export { VISITOR_KEYS as keys } from "@babel/types";', + errors: [{ messageId: "owner" }], + }, + { + code: 'import * as t from "@babel/types"; t.VISITOR_KEYS;', + errors: [{ messageId: "owner" }], + }, + { + code: 'import * as t from "@babel/types"; t["VISITOR_KEYS"];', + errors: [{ messageId: "owner" }], + }, + { + code: 'import * as t from "@babel/types"; const { VISITOR_KEYS: keys } = t;', + errors: [{ messageId: "owner" }], + }, + ], + }, +); + +it("limits the shared Babel child expansion exception to its owner", async () => { + const root = await createTestTempDirectory("rea-visitor-lint-"); + const config = await writeRepositoryOxlintConfiguration(root); + await mkdir(join(root, "src/domain/javascript"), { recursive: true }); + const source = + 'import * as t from "@babel/types"; export const keys = t.VISITOR_KEYS;\n'; + await writeFile( + join(root, "src/domain/javascript/javascriptSemanticTraversal.ts"), + source, + ); + await writeFile(join(root, "src/domain/javascript/other.ts"), source); + await writeFile(join(root, "src/domain/javascript/other.spec.ts"), source); + const result = spawnSync( + process.execPath, + [ + resolve("node_modules/oxlint/bin/oxlint"), + "-c", + config, + "--format", + "json", + "src/domain/javascript/javascriptSemanticTraversal.ts", + "src/domain/javascript/other.ts", + "src/domain/javascript/other.spec.ts", + ], + { cwd: root, encoding: "utf8" }, + ); + if (result.error !== undefined) throw result.error; + expect(result.status, result.stderr).toBe(1); + const report = JSON.parse(result.stdout) as { + diagnostics: Array<{ filename: string; code: string }>; + }; + expect( + report.diagnostics + .filter(({ code }) => code.includes("no-babel-visitor-keys")) + .map(({ filename }) => filename.replaceAll("\\", "/")), + ).toEqual(["src/domain/javascript/other.ts"]); +}); diff --git a/tests/evaluation/noLocaleCompareRule.test.ts b/tests/evaluation/noLocaleCompareRule.test.ts new file mode 100644 index 0000000000..ec89a3005c --- /dev/null +++ b/tests/evaluation/noLocaleCompareRule.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import { spawnSync } from "node:child_process"; +import { mkdir, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { RuleTester } from "oxlint/plugins-dev"; +import rule from "../../scripts/lint/rules/no-locale-compare.mjs"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; +import { writeRepositoryOxlintConfiguration } from "../fixtures/oxlintConfiguration.js"; + +RuleTester.describe = describe; +RuleTester.it = it; + +new RuleTester({ languageOptions: { parserOptions: { lang: "ts" } } }).run( + "no-locale-compare", + rule, + { + valid: [ + 'const example = "a.localeCompare(b)";', + "// a.localeCompare(b)\nconst x = a < b;", + "compareUnicodeCodePoints(a, b)", + ], + invalid: [ + { code: "a.localeCompare(b)", errors: [{ messageId: "locale" }] }, + { code: "a.localeCompare (b)", errors: [{ messageId: "locale" }] }, + { code: 'a["localeCompare"](b)', errors: [{ messageId: "locale" }] }, + { code: "a[`localeCompare`](b)", errors: [{ messageId: "locale" }] }, + { code: "a?.localeCompare(b)", errors: [{ messageId: "locale" }] }, + ], + }, +); + +it("applies the ordering rule to domain production files and excludes tests", async () => { + const root = await createTestTempDirectory("rea-ordering-lint-"); + const config = await writeRepositoryOxlintConfiguration(root); + await mkdir(join(root, "src/domain"), { recursive: true }); + const source = + "export const compare = (a: string, b: string) => a.localeCompare(b);\n"; + await writeFile(join(root, "src/domain/ordering.ts"), source); + await writeFile(join(root, "src/domain/ordering.test.ts"), source); + await writeFile(join(root, "src/domain/ordering.spec.ts"), source); + const result = spawnSync( + process.execPath, + [ + resolve("node_modules/oxlint/bin/oxlint"), + "-c", + config, + "--format", + "json", + "src/domain/ordering.ts", + "src/domain/ordering.test.ts", + "src/domain/ordering.spec.ts", + ], + { cwd: root, encoding: "utf8" }, + ); + if (result.error !== undefined) throw result.error; + expect(result.status, result.stderr).toBe(1); + const report = JSON.parse(result.stdout) as { + diagnostics: Array<{ filename: string; code: string }>; + }; + expect( + report.diagnostics + .filter(({ code }) => code.includes("no-locale-compare")) + .map(({ filename }) => filename.replaceAll("\\", "/")), + ).toEqual(["src/domain/ordering.ts"]); +}); diff --git a/tests/evaluation/numericLimitCheck.test.ts b/tests/evaluation/numericLimitCheck.test.ts index 9ff48209e7..3d08c61145 100644 --- a/tests/evaluation/numericLimitCheck.test.ts +++ b/tests/evaluation/numericLimitCheck.test.ts @@ -282,8 +282,16 @@ it("reports each mutable alias once while keeping dynamic reads unresolved", () (issue) => issue.reason === "mutable setting-key or value alias", ), ).toEqual([ - expect.objectContaining({ offset: source.indexOf("key =") }), - expect.objectContaining({ offset: source.indexOf("other =") }), + expect.objectContaining({ + offset: source.indexOf("key ="), + line: 2, + column: 9, + }), + expect.objectContaining({ + offset: source.indexOf("other ="), + line: 6, + column: 9, + }), ]); expect(discovered.unresolved).toContainEqual( expect.objectContaining({ @@ -292,6 +300,22 @@ it("reports each mutable alias once while keeping dynamic reads unresolved", () ); }); +it("retains uncertainty for mutable suffixless settings passed to other functions", () => { + const discovered = discoverNumericLimitUses([ + { + path: "src/aliases.ts", + source: ` + let size = process.env.REA_SIZE; + consume(size); + `, + }, + ]); + const mutable = discovered.unresolved.filter( + (issue) => issue.reason === "mutable setting-key or value alias", + ); + expect(mutable.map((issue) => issue.line)).toEqual([2]); +}); + it("does not attribute an unrelated same-named local to a numeric limit", () => { const discovered = discoverNumericLimitUses([ { diff --git a/tests/evaluation/packetWorkflowSemantics.test.ts b/tests/evaluation/packetWorkflowSemantics.test.ts deleted file mode 100644 index f79076904f..0000000000 --- a/tests/evaluation/packetWorkflowSemantics.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import assert from "node:assert/strict"; -import { spawnSync } from "node:child_process"; -import { test } from "vitest"; - -test("packet page verification rejects skipped matches with unchanged counts and monotone frames", () => { - const moduleUrl = new URL( - "../../tests/support/analysis-efficiency/packet-entry.mjs", - import.meta.url, - ).href; - const source = ` - import assert from 'node:assert/strict'; - import {verifyPacketPage} from ${JSON.stringify(moduleUrl)}; - const rows = Array.from({length:36}, (_,i)=>({frame:i+1, source_port:1000,destination_port:5432,flags:16})); - const sha256 = 'a'.repeat(64); - const report = (selected)=>({artifact:{sha256},output_window:{matching_packets:36},packets:selected.map(row=>({ - 'frame.number':[String(row.frame)],'tcp.srcport':['1000'],'tcp.dstport':['5432'],'tcp.flags':['0x0010'] - }))}); - const oracle={sha256,matching_packets:36,rows:rows.slice(0,24)}; - const first=report(rows.slice(0,12)); - verifyPacketPage(first,report(rows.slice(12,24)),oracle); - assert.throws(()=>verifyPacketPage(first,report(rows.slice(24,36)),oracle),/independently parsed/); - assert.throws(()=>verifyPacketPage(first,report(rows.slice(12,24).reverse()),oracle),/independently parsed/); - assert.throws(()=>verifyPacketPage(first,{...report(rows.slice(12,24)),artifact:{sha256:'b'.repeat(64)}},oracle),/independently parsed/); - `; - // Semantic checker only; synthetic rows do not establish provider behavior. - const result = spawnSync(process.execPath, ["--input-type=module", "-"], { - input: source, - encoding: "utf8", - }); - assert.ifError(result.error); - assert.equal(result.status, 0, result.stderr); -}); diff --git a/tests/evaluation/sazWorkflowSemantics.test.ts b/tests/evaluation/sazWorkflowSemantics.test.ts deleted file mode 100644 index f0098caa79..0000000000 --- a/tests/evaluation/sazWorkflowSemantics.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import assert from "node:assert/strict"; -import { spawnSync } from "node:child_process"; -import { test } from "vitest"; - -test("SAZ verification preserves source identities, duplicate headers and encoded empty bodies", () => { - const moduleUrl = new URL( - "../../tests/support/analysis-efficiency/saz-entry.mjs", - import.meta.url, - ).href; - const source = ` - import assert from 'node:assert/strict'; - import {verifySazRecords,verifySazOracleIdentity,sazRequest,sazEvidenceParameters} from ${JSON.stringify(moduleUrl)}; - const omitted=sazRequest('xbox-clear_nomp.saz','/tmp/sample.saz','baseline'); - assert.equal(Object.hasOwn(omitted,'record_ordinals'),false); - assert.deepEqual(sazEvidenceParameters(omitted,3),{capture_path:'/tmp/sample.saz',format:'saz',record_ordinals:[0,1,2],sensitive_value_count:0}); - assert.deepEqual(sazEvidenceParameters(sazRequest('xbox-clear_nomp.saz','/tmp/sample.saz','focused'),13).record_ordinals,[12,0,6]); - const empty={bytes:0,sha256:'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',content_base64:'',text:''}; - const raw=[12,0,6].map(ordinal=>{ - const session_id=String(ordinal+1).padStart(2,'0'); - return {ordinal,session_id,members:['raw/'+session_id+'_c.txt','raw/'+session_id+'_s.txt'], - request:{member:'raw/'+session_id+'_c.txt',method:'GET',url:'/',http_version:'HTTP/1.1',headers:[],body:empty}, - response:{member:'raw/'+session_id+'_s.txt',status:'200',reason:'OK',http_version:'HTTP/1.1',headers:[{name:'Content-Type',value:' text/plain'},{name:'content-type',value:''}],body:empty}}; - }); - const oracle={kind:'saz',artifact:'xbox-clear_nomp.saz',sha256:'3e808663a99cbb9160186f7419633248200fbc6863d2c897f5d4d9972dde0375',total_records:13,record_ordinals:[12,0,6],records:raw}; - const rows=raw.map(row=>({ordinal:row.ordinal,location:{kind:'archive-members',members:row.members}, - reported:{session_id:row.session_id,request:{...row.request,body:''},response:{...row.response,body:''}}, - binary_fields:['request','response'].map(side=>({pointer:'/'+side+'/body',representation:'producer-bytes',state:'retained',...empty}))})); - verifySazOracleIdentity('xbox-clear_nomp.saz',oracle); - const facts=verifySazRecords(rows,oracle);assert.deepEqual(facts.map(row=>row.ordinal),[12,0,6]); - assert.equal(facts[0].response.content_types.length,2); - assert.throws(()=>verifySazRecords(rows.slice(1),oracle),/coverage/); - assert.throws(()=>verifySazRecords(rows.toReversed(),oracle),/order/); - const changed=mutate=>{const copy=structuredClone(rows);mutate(copy);return copy;}; - assert.throws(()=>verifySazRecords(changed(copy=>copy[1].reported.session_id='1'),oracle),/identity/); - assert.throws(()=>verifySazRecords(changed(copy=>copy[0].reported.response.headers.pop()),oracle),/Content-Type/); - assert.throws(()=>verifySazRecords(changed(copy=>copy[0].binary_fields[1].content_base64='WA=='),oracle),/encoded body/); - assert.throws(()=>verifySazRecords(changed(copy=>copy[0].reported.response=null),oracle)); - assert.throws(()=>verifySazRecords(changed(copy=>copy[0].location.members=['raw/other_s.txt']),oracle),/coordinates/); - assert.throws(()=>verifySazOracleIdentity('xbox-clear_nomp.saz',{...oracle,sha256:'a'.repeat(64)}),/corpus/); - assert.throws(()=>sazRequest('__proto__','/tmp/sample.saz','focused'),/pinned/); - `; - // Checker-only synthesized rows; actual-provider verification is separate. - const result = spawnSync(process.execPath, ["--input-type=module", "-"], { - input: source, - encoding: "utf8", - }); - assert.ifError(result.error); - assert.equal(result.status, 0, result.stderr); -}); diff --git a/tests/evaluation/signalWorkflowSemantics.test.ts b/tests/evaluation/signalWorkflowSemantics.test.ts deleted file mode 100644 index 33d6f1f6de..0000000000 --- a/tests/evaluation/signalWorkflowSemantics.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import assert from "node:assert/strict"; -import { spawnSync } from "node:child_process"; -import { test } from "vitest"; - -test("signal verification rejects skipped identical UART bytes and reordered I2C samples", () => { - const moduleUrl = new URL( - "../../tests/support/analysis-efficiency/signal-entry.mjs", - import.meta.url, - ).href; - const source = ` - import assert from 'node:assert/strict'; - import {verifySignalRows,signalDataRows,verifySignalOracleIdentity} from ${JSON.stringify(moduleUrl)}; - const sha256='a'.repeat(64); - const rows=Array.from({length:8},(_,i)=>({start:i*100+10,end:i*100+80,byte:72})); - const uart={kind:'uart',sha256,total_samples:2000,rows:rows.map((_,i)=>({start:i*100,end_exclusive:i*100+90,first_data_sample:i*100+15,last_data_sample:i*100+75,byte:72}))}; - verifySignalRows('uart',rows,uart,sha256); - verifySignalRows('uart',rows.map((row,i)=>({...row,start:i*100+15,end:i*100+75})),uart,sha256); - assert.throws(()=>verifySignalRows('uart',[{...rows[0],start:0,end:0},...rows.slice(1)],uart,sha256),/interval/); - assert.throws(()=>verifySignalRows('uart',[{...rows[0],end:74},...rows.slice(1)],uart,sha256),/interval/); - assert.throws(()=>verifySignalRows('uart',[{...rows[0],start:16},...rows.slice(1)],uart,sha256),/interval/); - assert.throws(()=>verifySignalRows('uart',rows,{...uart,rows:uart.rows.map(({first_data_sample,...row})=>row)},sha256),/interval/); - assert.throws(()=>verifySignalRows('uart',rows.slice(1).concat({start:810,end:880,byte:72}),uart,sha256),/interval/); - assert.throws(()=>verifySignalRows('uart',rows.slice(1),uart,sha256),/coverage/); - assert.throws(()=>verifySignalRows('uart',rows,uart,'b'.repeat(64)),/identity/); - const data=Array.from({length:18},(_,i)=>({start:i*100+10,end:i*100+90,byte:117})); - const raw=data.map((_,i)=>({byte:117,first_clock:i*100+20,last_clock:i*100+80})); - const i2c={kind:'i2c',sha256,total_samples:2000,reads:[{bytes:[{byte:165,last_clock:0},...raw],stop:1900}]}; - verifySignalRows('i2c',data,i2c,sha256); - assert.throws(()=>verifySignalRows('i2c',data.toReversed(),i2c,sha256),/interval/); - assert.throws(()=>verifySignalRows('i2c',[{...data[0],byte:118},...data.slice(1)],i2c,sha256),/sample bits/); - assert.throws(()=>verifySignalRows('i2c',data.map((row,i)=>({...row,start:i,end:1999})),i2c,sha256),/interval/); - assert.throws(()=>verifySignalRows('i2c',[{...data[0],start:0},...data.slice(1)],i2c,sha256),/interval/); - assert.throws(()=>verifySignalRows('i2c',[...data.slice(0,-1),{...data.at(-1),end:1900}],i2c,sha256),/interval/); - verifySignalOracleIdentity('sigrok-uart-max3232e.sr',{kind:'uart',sha256:'f797d60d6b8e12d2a1549b9a5a499ef389f44cb9e870e8610af26b57c3b2be76'}); - assert.throws(()=>verifySignalOracleIdentity('sigrok-uart-max3232e.sr',uart),/pinned signal corpus/); - assert.throws(()=>verifySignalOracleIdentity('sigrok-i2c-nunchuk.sr',{kind:'uart',sha256:'0fa6adb8297d7f3fe5ba19c94ee655c9b0f952a8b5e48a17aaa499a2f6d10d70'}),/pinned signal corpus/); - assert.throws(()=>signalDataRows('uart','9007199254740993-9007199254740994 uart-1: 48'),/sample interval/); - `; - // Checker semantics only; synthesized rows do not establish provider behavior. - const result = spawnSync(process.execPath, ["--input-type=module", "-"], { - input: source, - encoding: "utf8", - }); - assert.ifError(result.error); - assert.equal(result.status, 0, result.stderr); -}); diff --git a/tests/fixtures/environmentStateWrites.ts b/tests/fixtures/environmentStateWrites.ts index 12f8d092d1..c6afe38595 100644 --- a/tests/fixtures/environmentStateWrites.ts +++ b/tests/fixtures/environmentStateWrites.ts @@ -1,7 +1,5 @@ -import { - projectAnalysisView, - type AnalysisViewResult, -} from "../../src/domain/analysisView/analysisView.js"; +import { projectAnalysisView } from "../../src/domain/analysisView/analysisView.js"; +import type { AnalysisViewResult } from "../../src/domain/analysisView/analysisViewResult.js"; import type { TemplateRule } from "../../src/domain/analysisView/environmentManifestRules.js"; import type { Evidence } from "../../src/domain/evidence.js"; import type { JsonValue } from "../../src/domain/jsonValue.js"; diff --git a/tests/fixtures/importCycles.ts b/tests/fixtures/importCycles.ts index 51ba28ed3a..d177d87fd4 100644 --- a/tests/fixtures/importCycles.ts +++ b/tests/fixtures/importCycles.ts @@ -1,5 +1,6 @@ import { readdirSync, readFileSync, statSync } from "node:fs"; -import { dirname, join, relative, resolve } from "node:path"; +import { join, relative } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { expect } from "vitest"; import { readImportSpecifiers } from "../../scripts/lib/import-specifiers.mjs"; @@ -13,7 +14,12 @@ const productionFiles = (directory: string): string[] => { for (const name of readdirSync(directory)) { const path = join(directory, name); if (statSync(path).isDirectory()) files.push(...productionFiles(path)); - else if (name.endsWith(".ts") && !name.includes(".test.")) files.push(path); + else if ( + name.endsWith(".ts") && + !/\.(?:test|spec)\.ts$/u.test(name) && + !name.endsWith(".fixture.ts") + ) + files.push(path); } return files; }; @@ -27,10 +33,9 @@ const importGraph = (root: string): Map> => { const source = readFileSync(join(root, file), "utf8"); const edges = new Set(); for (const spec of relativeSpecifiers(source)) { - const resolved = resolve(dirname(join(root, file)), spec).replace( - /\.js$/, - ".ts", - ); + const resolved = fileURLToPath( + new URL(spec, pathToFileURL(join(root, file))), + ).replace(/\.js$/, ".ts"); const target = relative(root, resolved); if (files.has(target)) edges.add(target); } @@ -73,7 +78,8 @@ const stronglyConnected = ( onStack.delete(member); component.push(member); } - if (component.length > 1) components.push(component.sort()); + if (component.length > 1 || graph.get(node)?.has(node) === true) + components.push(component.sort()); } }; for (const node of [...graph.keys()].sort()) { diff --git a/tests/support/analysis-efficiency/README.md b/tests/support/analysis-efficiency/README.md index 1970ab7074..0d4e3d049a 100644 --- a/tests/support/analysis-efficiency/README.md +++ b/tests/support/analysis-efficiency/README.md @@ -5,8 +5,7 @@ real local Codex CLI and the retained resource/agent runners. See [MCP catalog evaluation](../../../docs/mcp-catalog-evaluation.md) for its matrix, observed-token accounting, isolation and completed-task limits. -These files support the existing comparison, independent-oracle, receipt and -cleanup regressions. The TypeScript consumers live in `tests/boundary/`, +These files support the existing comparison, receipt and cleanup regressions. The TypeScript consumers live in `tests/boundary/`, `tests/evaluation/` and `tests/conformance/`; `npm run test:python` discovers the Python checks here. Build first for checks that use the compiled runtime. diff --git a/tests/support/analysis-efficiency/compare.mjs b/tests/support/analysis-efficiency/compare.mjs index 633057fdd5..74784ecc61 100644 --- a/tests/support/analysis-efficiency/compare.mjs +++ b/tests/support/analysis-efficiency/compare.mjs @@ -1,8 +1,16 @@ import { isDeepStrictEqual, parseArgs } from "node:util"; import { readFile, writeFile } from "node:fs/promises"; import { resolve } from "node:path"; -import { PACKET_ARTIFACTS } from "./packet-entry.mjs"; -import { SIGNAL_ARTIFACTS } from "./signal-entry.mjs"; + +const packetArtifacts = new Set([ + "wireshark-pgsql-jdbc.pcap", + "wireshark-dns.cap", + "wireshark-dhcp.pcap", +]); +const signalArtifacts = new Set([ + "sigrok-uart-max3232e.sr", + "sigrok-i2c-nunchuk.sr", +]); const { values } = parseArgs({ options: { @@ -81,8 +89,8 @@ for (const [side, answer] of [ // coverage. Repeated equivalent deliveries must also match every observation // field except the receipt and parent IDs, which identify their own invocations. if ( - Object.hasOwn(PACKET_ARTIFACTS, left.artifact) || - Object.hasOwn(SIGNAL_ARTIFACTS, left.artifact) || + packetArtifacts.has(left.artifact) || + signalArtifacts.has(left.artifact) || ["wireshark-http.cap", "wireshark-http_redirects.pcapng"].includes( left.artifact, ) @@ -103,9 +111,9 @@ if ( }); if ( left.delivery === right.delivery && - (!Object.hasOwn(PACKET_ARTIFACTS, left.artifact) || + (!packetArtifacts.has(left.artifact) || left.packet_selection === right.packet_selection) && - (!Object.hasOwn(SIGNAL_ARTIFACTS, left.artifact) || + (!signalArtifacts.has(left.artifact) || left.signal_selection === right.signal_selection) ) { answer.delivery_observations = answer.delivery_observations.map( diff --git a/tests/support/analysis-efficiency/native-entry.mjs b/tests/support/analysis-efficiency/native-entry.mjs index 84bc934ff6..caaf6a802f 100644 --- a/tests/support/analysis-efficiency/native-entry.mjs +++ b/tests/support/analysis-efficiency/native-entry.mjs @@ -1,7 +1,5 @@ -import { open, readFile } from "node:fs/promises"; import { isDeepStrictEqual } from "node:util"; -export const NATIVE_ARTIFACTS = ["jq", "sharex.exe", "freedos.edlin.exe"]; const normalized = (value) => value.normalized_result; const retainedEntryViews = async ( @@ -233,40 +231,3 @@ export const measureNativeEntry = async ({ await call("close_binary", {}); } }; - -export const nativeEntrySeed = async (artifact, path) => { - if (artifact === "jq") { - const file = await open(path, "r"); - let header; - try { - header = (await file.read(Buffer.alloc(32), 0, 32, 0)).buffer; - } finally { - await file.close(); - } - if (header.subarray(0, 6).toString("hex") !== "7f454c460201") - throw new Error("Expected ELF64 little endian entry seed"); - return `0x${header.readBigUInt64LE(24).toString(16)}`; - } - if (artifact === "sharex.exe") { - const bytes = await readFile(path); - const pe = bytes.readUInt32LE(0x3c); - const optional = pe + 24; - if ( - bytes.subarray(pe, pe + 4).toString("hex") !== "50450000" || - bytes.readUInt16LE(pe + 4) !== 0x8664 || - bytes.readUInt16LE(optional) !== 0x20b || - bytes.readUInt32LE(optional + 108) <= 14 || - bytes.readUInt32LE(optional + 112 + 14 * 8) !== 0 || - bytes.readUInt32LE(optional + 116 + 14 * 8) !== 0 - ) - throw new Error( - "Expected the pinned native PE32+ entry seed without CLR metadata", - ); - const address = - bytes.readBigUInt64LE(optional + 24) + - BigInt(bytes.readUInt32LE(optional + 16)); - return `0x${address.toString(16)}`; - } - if (artifact === "freedos.edlin.exe") return undefined; - throw new Error("Select a pinned native artifact"); -}; diff --git a/tests/support/analysis-efficiency/packet-entry.mjs b/tests/support/analysis-efficiency/packet-entry.mjs deleted file mode 100644 index 23505b702a..0000000000 --- a/tests/support/analysis-efficiency/packet-entry.mjs +++ /dev/null @@ -1,314 +0,0 @@ -import { isDeepStrictEqual, promisify } from "node:util"; -import { resolve } from "node:path"; -import { pathToFileURL, fileURLToPath } from "node:url"; -import { execFile } from "node:child_process"; - -export const verifyPacketPage = (first, next, oracle) => { - if ( - first.artifact.sha256 !== oracle.sha256 || - next.artifact.sha256 !== oracle.sha256 || - first.output_window.matching_packets !== oracle.matching_packets || - next.output_window.matching_packets !== oracle.matching_packets || - !isDeepStrictEqual( - packetFacts("postgres", first).rows, - oracle.rows.slice(0, 12), - ) || - !isDeepStrictEqual( - packetFacts("postgres", next).rows, - oracle.rows.slice(12, 24), - ) - ) - throw new Error( - "Packet pages differ from independently parsed original matches", - ); -}; - -export const PACKET_ARTIFACTS = { - "wireshark-pgsql-jdbc.pcap": { - kind: "postgres", - fields: ["tcp.srcport", "tcp.dstport", "tcp.flags"], - extra: ["tcp.len"], - protocol: "tcp", - query: "tcp.flags", - discovered: "tcp.flags", - filter: "tcp.port == 5432", - }, - "wireshark-dns.cap": { - kind: "dns", - fields: [ - "dns.id", - "dns.flags.response", - "dns.flags.rcode", - "dns.qry.name", - "dns.qry.type", - ], - extra: ["dns.resp.name", "dns.resp.ttl", "udp.srcport", "udp.dstport"], - protocol: "dns", - query: "dns.flags.rcode", - discovered: "dns.flags.rcode", - filter: "dns.flags.response == 1 && dns.flags.rcode != 0", - }, - "wireshark-dhcp.pcap": { - kind: "dhcp", - fields: [ - "dhcp.id", - "dhcp.option.dhcp", - "dhcp.ip.your", - "dhcp.option.dhcp_server_id", - ], - extra: [ - "dhcp.type", - "dhcp.ip.client", - "dhcp.hw.mac_addr", - "udp.srcport", - "udp.dstport", - ], - protocol: "dhcp", - query: "Option 53:", - discovered: "dhcp.option.dhcp", - filter: "dhcp", - }, -}; - -export const packetRequest = (artifact, path, selection) => { - const descriptor = PACKET_ARTIFACTS[artifact]; - if (!descriptor) throw new Error("Unknown pinned packet question"); - const baseline = selection === "baseline"; - return { - path, - fields: baseline - ? [...descriptor.fields, ...descriptor.extra] - : descriptor.fields, - display_filter: - descriptor.kind === "postgres" && baseline - ? `${descriptor.filter} && frame.number <= 2000` - : descriptor.kind === "dns" && baseline - ? "dns" - : descriptor.filter, - ...(descriptor.kind === "postgres" && !baseline - ? { packet_limit: 2000, match_limit: 12 } - : {}), - }; -}; - -const number = (row, field) => { - const value = row[field]; - if ( - !Array.isArray(value) || - value.length !== 1 || - !/^(?:[0-9]+|0x[0-9a-f]+)$/iu.test(value[0]) - ) - throw new Error(`Expected one recorded integer for ${field}`); - const parsed = Number(value[0]); - if (!Number.isSafeInteger(parsed)) - throw new Error(`Unsafe recorded integer for ${field}`); - return parsed; -}; - -export const packetFacts = (kind, report) => { - let packets = report.packets; - if (kind === "postgres") packets = packets.slice(0, 12); - if (kind === "dns") - packets = packets.filter( - (row) => - number(row, "dns.flags.response") === 1 && - number(row, "dns.flags.rcode") !== 0, - ); - return { - kind, - matching_packets: - kind === "dns" ? packets.length : report.output_window.matching_packets, - rows: packets.map((row) => { - const frame = number(row, "frame.number"); - if (kind === "postgres") - return { - frame, - source_port: number(row, "tcp.srcport"), - destination_port: number(row, "tcp.dstport"), - flags: number(row, "tcp.flags"), - }; - if (kind === "dns") { - const names = row["dns.qry.name"]; - const types = row["dns.qry.type"]; - if ( - !Array.isArray(names) || - names.length === 0 || - !Array.isArray(types) || - types.length !== names.length - ) - throw new Error( - "The fixed DNS question requires recorded names and types; missing fields are unknown", - ); - return { - frame, - id: number(row, "dns.id"), - rcode: number(row, "dns.flags.rcode"), - names, - types: types.map((value) => number({ type: [value] }, "type")), - }; - } - const server = row["dhcp.option.dhcp_server_id"]; - const address = row["dhcp.ip.your"]; - if ( - !Array.isArray(address) || - address.length !== 1 || - (server !== undefined && - (!Array.isArray(server) || server.length !== 1)) - ) - throw new Error("Expected recorded DHCP address scalar"); - return { - frame, - id: number(row, "dhcp.id"), - message_type: number(row, "dhcp.option.dhcp"), - your_address: address[0], - ...(server === undefined ? {} : { server_identifier: server[0] }), - }; - }), - }; -}; - -export const measurePacketEntry = async ({ - values, - call, - inputSha, - catalogDefinitions, -}) => { - const descriptor = PACKET_ARTIFACTS[values.artifact]; - const { parseEvidence } = await import( - pathToFileURL(resolve(values["runtime-root"], "dist/domain/evidence.js")) - .href - ); - const { inspectPacketCaptureInputSchema, packetCaptureSchema } = await import( - pathToFileURL( - resolve(values["runtime-root"], "dist/domain/captures/packetCapture.js"), - ).href - ); - const { inspectPacketFieldsInputSchema, packetFieldsCatalogSchema } = - await import( - pathToFileURL( - resolve(values["runtime-root"], "dist/domain/captures/packetFields.js"), - ).href - ); - const observations = []; - const selection = values["packet-selection"]; - const checked = (evidence, operation, input, schema) => { - parseEvidence(evidence); - const report = schema.parse(evidence.normalized_result); - if ( - evidence.operation !== operation || - !isDeepStrictEqual(evidence.parameters, input) || - !isDeepStrictEqual(report.selection, input) - ) - throw new Error( - "Packet Evidence operation or choices differs from the request", - ); - if ( - operation === "inspect_packet_capture" && - (evidence.subject?.local_path !== values.path || - evidence.subject?.digest?.sha256 !== inputSha || - report.artifact.path !== values.path || - report.artifact.sha256 !== inputSha) - ) - throw new Error("Packet Evidence identifies different capture bytes"); - if ( - operation === "inspect_packet_fields" && - (evidence.subject?.local_path !== report.tool.path || - evidence.subject?.digest?.sha256 !== report.tool.sha256) - ) - throw new Error("Field discovery identifies a different executable"); - observations.push({ - evidence_id: evidence.evidence_id, - operation, - selection: report.selection, - output_window: report.output_window, - read_scope: report.read_scope ?? null, - limitations: evidence.limitations, - }); - return report; - }; - if (selection === "discovery") { - const input = inspectPacketFieldsInputSchema.parse({ - kind: "fields", - protocol: descriptor.protocol, - query: descriptor.query, - limit: 1, - }); - const found = checked( - await call("inspect_packet_fields", input), - "inspect_packet_fields", - input, - packetFieldsCatalogSchema, - ); - if ( - !found.records.some((row) => row.abbreviation === descriptor.discovered) - ) - throw new Error( - "Selected installation does not advertise the required protocol field", - ); - } - const input = inspectPacketCaptureInputSchema.parse( - packetRequest(values.artifact, values.path, selection), - ); - for (const name of [ - "inspect_packet_capture", - ...(selection === "discovery" ? ["inspect_packet_fields"] : []), - ]) - if (!catalogDefinitions.has(name)) - throw new Error(`Missing live packet contract ${name}`); - const report = checked( - await call("inspect_packet_capture", input), - "inspect_packet_capture", - input, - packetCaptureSchema, - ); - const facts = packetFacts(descriptor.kind, report); - if (selection === "pages") { - const { stdout } = await promisify(execFile)( - "python3", - [ - fileURLToPath(new URL("./packet-oracle.py", import.meta.url)), - "postgres", - values.path, - "--row-limit", - "24", - ], - { timeout: 30_000, maxBuffer: 128 * 1024 }, - ); - const oracle = JSON.parse(stdout); - const nextInput = inspectPacketCaptureInputSchema.parse({ - ...input, - match_offset: 12, - }); - const next = checked( - await call("inspect_packet_capture", nextInput), - "inspect_packet_capture", - nextInput, - packetCaptureSchema, - ); - const repeated = checked( - await call("inspect_packet_capture", input), - "inspect_packet_capture", - input, - packetCaptureSchema, - ); - verifyPacketPage(report, next, oracle); - if ( - !isDeepStrictEqual(report.packets, repeated.packets) || - !isDeepStrictEqual(report.output_window, repeated.output_window) || - next.artifact.sha256 !== report.artifact.sha256 || - next.output_window.matching_packets !== - report.output_window.matching_packets || - Number(next.packets[0]?.["frame.number"]?.[0]) <= - Number(report.packets.at(-1)?.["frame.number"]?.[0]) - ) - throw new Error( - "Packet page order, digest or repeated observations changed", - ); - } - return { - artifact_sha256: inputSha, - facts, - delivery_observations: observations, - evidence_ids: [...new Set(observations.map((row) => row.evidence_id))], - }; -}; diff --git a/tests/support/analysis-efficiency/packet-oracle.py b/tests/support/analysis-efficiency/packet-oracle.py deleted file mode 100644 index db5797b5f6..0000000000 --- a/tests/support/analysis-efficiency/packet-oracle.py +++ /dev/null @@ -1,180 +0,0 @@ -"""Independent, narrow oracle for the three pinned Ethernet/IPv4 PCAPs. - -Reads header bytes, not TShark output. No PCAPNG, IP fragmentation or TCP -reassembly support is claimed. DNS compression follows RFC 1035; DHCP options -53/54 follow RFC 2132. Unsupported selected traffic fails rather than guessing. -""" -import argparse -import hashlib -import json -import struct -from pathlib import Path - - -def require(condition, reason): - if not condition: - raise ValueError(reason) - - -def packets(data): - require(len(data) >= 24, 'truncated PCAP header') - magic = data[:4] - require(magic in (b'\xd4\xc3\xb2\xa1', b'\xa1\xb2\xc3\xd4'), - 'oracle requires microsecond classic PCAP') - endian = '<' if magic == b'\xd4\xc3\xb2\xa1' else '>' - require(struct.unpack_from(endian + 'HH', data, 4) == (2, 4), - 'unsupported PCAP version') - require(struct.unpack_from(endian + 'I', data, 20)[0] == 1, - 'oracle requires Ethernet') - offset, frame = 24, 0 - while offset < len(data): - require(offset + 16 <= len(data), 'truncated PCAP record header') - captured, original = struct.unpack_from(endian + 'II', data, offset + 8) - offset += 16 - require(captured <= original and offset + captured <= len(data), - 'invalid captured packet length') - frame += 1 - yield frame, data[offset:offset + captured] - offset += captured - - -def transport(packet): - require(len(packet) >= 14, 'truncated Ethernet header') - if packet[12:14] == b'\x08\x06': - return None # ARP cannot contain the selected TCP/UDP messages. - require(packet[12:14] == b'\x08\x00', 'unsupported Ethernet protocol') - ip = packet[14:] - require(len(ip) >= 20 and ip[0] >> 4 == 4, 'invalid IPv4 header') - header = (ip[0] & 15) * 4 - total = struct.unpack_from('!H', ip, 2)[0] - require(20 <= header <= len(ip) and total >= header, 'invalid IPv4 lengths') - require(total <= len(ip), 'truncated IPv4 packet') - require(struct.unpack_from('!H', ip, 6)[0] & 0x3fff == 0, - 'fragmentation is outside oracle scope') - return ip[9], ip[header:min(total, len(ip))] - - -def dns_name(message, offset): - labels, visited, end = [], set(), None - while True: - require(offset < len(message) and offset not in visited, - 'invalid DNS name or compression cycle') - visited.add(offset) - size = message[offset] - if size & 0xc0 == 0xc0: - require(offset + 1 < len(message), 'truncated DNS compression pointer') - if end is None: - end = offset + 2 - offset = ((size & 0x3f) << 8) | message[offset + 1] - continue - require(size & 0xc0 == 0 and offset + 1 + size <= len(message), - 'invalid DNS label') - offset += 1 - if size == 0: - return '.'.join(labels), end if end is not None else offset - labels.append(message[offset:offset + size].decode('ascii')) - require(sum(len(label) + 1 for label in labels) <= 255, - 'DNS name exceeds protocol length') - offset += size - - -def dhcp_options(payload): - require(len(payload) >= 240 and payload[236:240] == b'\x63\x82\x53\x63', - 'invalid DHCP fixed header or cookie') - options, offset = {}, 240 - while offset < len(payload): - kind = payload[offset] - offset += 1 - if kind == 255: - return options - if kind == 0: - continue - require(offset < len(payload), 'missing DHCP option length') - size = payload[offset] - offset += 1 - require(offset + size <= len(payload), 'truncated DHCP option') - require(kind not in options, 'duplicate DHCP options outside oracle scope') - options[kind] = payload[offset:offset + size] - offset += size - raise ValueError('DHCP end option missing') - - -def analyze(data, kind, row_limit=12): - rows, matching, count = [], 0, 0 - for frame, packet in packets(data): - count = frame - decoded = transport(packet) - if decoded is None: - continue - protocol, segment = decoded - if kind == 'postgres' and protocol == 6 and frame <= 2000: - require(len(segment) >= 20, 'truncated TCP header') - tcp_header = (segment[12] >> 4) * 4 - require(20 <= tcp_header <= len(segment), 'invalid or truncated TCP header/options') - source, destination = struct.unpack_from('!HH', segment) - if 5432 not in (source, destination): - continue - matching += 1 - if len(rows) < row_limit: - rows.append({'frame': frame, 'source_port': source, - 'destination_port': destination, - 'flags': struct.unpack_from('!H', segment, 12)[0] & 0xfff}) - elif kind in ('dns', 'dhcp') and protocol == 17: - require(len(segment) >= 8, 'truncated UDP header') - source, destination, length = struct.unpack_from('!HHH', segment) - selected = 53 in (source, destination) if kind == 'dns' else ( - source, destination) in ((67, 68), (68, 67)) - if not selected: - continue - require(8 <= length <= len(segment), 'truncated selected UDP message') - payload = segment[8:length] - if kind == 'dns': - require(len(payload) >= 12, 'truncated DNS header') - identifier, flags, questions = struct.unpack_from('!HHH', payload) - if not flags & 0x8000 or flags & 15 == 0: - continue - matching += 1 - offset, names, types = 12, [], [] - for _ in range(questions): - name, offset = dns_name(payload, offset) - require(offset + 4 <= len(payload), 'truncated DNS question') - types.append(struct.unpack_from('!H', payload, offset)[0]) - names.append(name) - offset += 4 - rows.append({'frame': frame, 'id': identifier, 'rcode': flags & 15, - 'names': names, 'types': types}) - else: - options = dhcp_options(payload) - require(53 in options and len(options[53]) == 1, - 'missing DHCP message type') - server = options.get(54) - require(server is None or len(server) == 4, 'invalid DHCP server ID') - require(52 not in options, 'DHCP overload outside oracle scope') - matching += 1 - row = {'frame': frame, 'id': struct.unpack_from('!I', payload, 4)[0], - 'message_type': options[53][0], - 'your_address': '.'.join(map(str, payload[16:20]))} - if server is not None: - row['server_identifier'] = '.'.join(map(str, server)) - rows.append(row) - return {'kind': kind, 'capture_frames': count, 'matching_packets': matching, - 'question_input_frames': min(2000, count) if kind == 'postgres' else count, - 'rows': rows, 'scope': 'selected recorded headers; no network completeness claim'} - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('kind', choices=['postgres', 'dns', 'dhcp']) - parser.add_argument('path', type=Path) - parser.add_argument('--row-limit', type=int, choices=[12, 24], default=12, - help='PostgreSQL rows; 24 independently checks the second page') - args = parser.parse_args() - with args.path.open('rb') as source: - data = source.read(32 * 1024 * 1024 + 1) - require(len(data) <= 32 * 1024 * 1024, 'oracle input budget exceeded') - print(json.dumps({'sha256': hashlib.sha256(data).hexdigest(), - **analyze(data, args.kind, args.row_limit)}, indent=2)) - - -if __name__ == '__main__': - main() diff --git a/tests/support/analysis-efficiency/saz-entry.mjs b/tests/support/analysis-efficiency/saz-entry.mjs deleted file mode 100644 index 4952fcd5b1..0000000000 --- a/tests/support/analysis-efficiency/saz-entry.mjs +++ /dev/null @@ -1,326 +0,0 @@ -import assert from "node:assert/strict"; -import { execFile } from "node:child_process"; -import { readFile } from "node:fs/promises"; -import { resolve } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import { promisify } from "node:util"; - -const corpus = JSON.parse( - await readFile(new URL("./corpus.json", import.meta.url), "utf8"), -); -export const SAZ_ARTIFACTS = Object.fromEntries( - corpus.artifacts - .filter((entry) => entry.saz_question) - .map((entry) => [entry.id, entry]), -); - -export function sazRequest(artifact, path, selection) { - const question = Object.hasOwn(SAZ_ARTIFACTS, artifact) - ? SAZ_ARTIFACTS[artifact].saz_question - : undefined; - if (!question) throw new Error("Select a pinned SAZ question"); - return { - capture_path: path, - format: "saz", - ...(selection === "baseline" - ? {} - : { record_ordinals: question.record_ordinals }), - sensitive_values: [], - }; -} - -export function verifySazOracleIdentity(artifact, oracle) { - assert.ok( - Object.hasOwn(SAZ_ARTIFACTS, artifact), - "Unknown pinned SAZ question", - ); - const entry = SAZ_ARTIFACTS[artifact]; - assert.equal(oracle.kind, "saz"); - assert.equal(oracle.artifact, artifact); - assert.equal(oracle.sha256, entry.sha256, "Wrong immutable corpus"); - assert.equal(oracle.total_records, entry.saz_question.total_records); - assert.deepEqual(oracle.record_ordinals, entry.saz_question.record_ordinals); -} - -export function sazEvidenceParameters(input, totalRecords) { - return { - capture_path: input.capture_path, - format: input.format, - record_ordinals: - input.record_ordinals ?? - Array.from({ length: totalRecords }, (_, index) => index), - sensitive_value_count: input.sensitive_values.length, - }; -} - -export function verifySazRecords(records, oracle) { - assert.equal(oracle.kind, "saz", "Wrong independent question"); - assert.equal( - records.length, - oracle.records.length, - "Selected coverage differs", - ); - return records.map((record, index) => { - const raw = oracle.records[index]; - assert.equal(record.ordinal, raw.ordinal, "Caller order/ordinal differs"); - assert.equal( - record.reported.session_id, - raw.session_id, - "Padded source identity differs", - ); - assert.deepEqual( - record.location, - { kind: "archive-members", members: raw.members }, - "Original member coordinates differ", - ); - for (const side of ["request", "response"]) { - const expected = raw[side], - reported = record.reported[side]; - if (expected === null) { - assert.equal(reported, null, "Absent message invented"); - assert.ok( - !record.binary_fields.some( - (field) => field.pointer === `/${side}/body`, - ), - "Absent body invented", - ); - continue; - } - assert.equal(reported.member, expected.member); - for (const field of side === "request" - ? ["method", "url", "http_version"] - : ["status", "reason", "http_version"]) - assert.equal( - reported[field], - expected[field], - "Stored HTTP start line differs", - ); - const contentTypes = (headers) => - headers.filter( - (header) => header.name.toLowerCase() === "content-type", - ); - assert.deepEqual( - contentTypes(reported.headers), - contentTypes(expected.headers), - "Content-Type order/spelling/values differ", - ); - const bodies = record.binary_fields.filter( - (field) => field.pointer === `/${side}/body`, - ); - assert.equal(bodies.length, 1, "Stored body representation differs"); - const body = bodies[0]; - assert.equal(body.state, "retained", "Stored body unexpectedly unknown"); - assert.equal( - body.representation, - "producer-bytes", - "Encoded source representation differs", - ); - for (const field of ["bytes", "sha256", "content_base64"]) - assert.equal( - body[field], - expected.body[field], - "Stored encoded body bytes differ", - ); - assert.equal( - reported.body, - expected.body.text, - "UTF-8 projection or empty body differs", - ); - } - const response = raw.response; - return { - ordinal: record.ordinal, - session_id: raw.session_id, - members: raw.members, - request: - raw.request === null - ? null - : { method: raw.request.method, url: raw.request.url }, - response: - response === null - ? null - : { - status: response.status, - http_version: response.http_version, - content_types: response.headers.filter( - (header) => header.name.toLowerCase() === "content-type", - ), - body: { - bytes: response.body.bytes, - sha256: response.body.sha256, - }, - }, - }; - }); -} - -export async function measureSazEntry({ values, call, inputSha }) { - const runtime = (file) => - pathToFileURL(resolve(values["runtime-root"], file)).href; - const { parseEvidence } = await import(runtime("dist/domain/evidence.js")); - const { inspectWebNetworkCaptureInputSchema, webNetworkCaptureSchema } = - await import(runtime("dist/domain/webNetworkCapture.js")); - const { inspectAnalysisViewInputSchema, analysisViewResultSchema } = - await import(runtime("dist/domain/analysisView/analysisView.js")); - const read = await promisify(execFile)( - "python3", - [ - fileURLToPath(new URL("./saz-oracle.py", import.meta.url)), - values.artifact, - values.path, - ], - { maxBuffer: 2 * 1024 * 1024, timeout: 30_000 }, - ); - const oracle = JSON.parse(read.stdout); - verifySazOracleIdentity(values.artifact, oracle); - assert.equal(oracle.sha256, inputSha); - const input = inspectWebNetworkCaptureInputSchema.parse( - sazRequest(values.artifact, values.path, values["saz-selection"]), - ); - const observations = []; - const checked = (evidence, schema) => { - evidence = parseEvidence(evidence); - assert.equal(evidence.subject.local_path, values.path); - assert.equal(evidence.subject.digest.sha256, inputSha); - const report = schema.parse(evidence.normalized_result); - observations.push({ - evidence_id: evidence.evidence_id, - operation: evidence.operation, - parameters: evidence.parameters, - limitations: evidence.limitations, - coverage: report.coverage ?? { - total_records: report.total_records, - retained_records: report.records.length, - }, - }); - return report; - }; - const readView = async (source, view) => { - const request = inspectAnalysisViewInputSchema.parse({ source, view }); - const evidence = await call("inspect_analysis_view", request); - assert.equal(evidence.operation, "inspect_analysis_view"); - assert.deepEqual(evidence.parameters, { - parent_evidence_id: source.evidence_id, - view: request.view, - }); - const result = checked(evidence, analysisViewResultSchema); - assert.equal(result.kind, request.view.kind); - assert.equal(result.parent_evidence_id, source.evidence_id); - assert.equal(result.parent_operation, "inspect_web_network_capture"); - assert.deepEqual(result.view, request.view); - return result; - }; - let records, decoder; - if (values["saz-selection"] !== "retained") { - let evidence; - try { - evidence = await call("inspect_web_network_capture", input); - } catch (failure) { - const error = failure.value?.error; - const limits = error?.details?.reported_limits; - if ( - values["saz-selection"] !== "baseline" || - error?.code !== "resource_constraint" || - error.details.operation !== "inspect_web_network_capture" || - limits?.boundary !== "mcp-response" || - !limits.evidence_reference - ) - throw failure; - // Recover only the exact retained reference returned by this failed call. - // The failed RPC remains in the shared measurement log and task costs. - const source = limits.evidence_reference; - assert.equal(source.kind, "retained-evidence"); - const summary = await readView(source, { kind: "summary" }); - assert.equal(summary.summary.format, "saz"); - assert.equal(summary.summary.total_records, oracle.total_records); - assert.equal(summary.summary.retained_records, oracle.total_records); - decoder = summary.summary.decoder; - records = []; - for (const ordinal of oracle.record_ordinals) { - const page = await readView(source, { - kind: "capture", - detail: "complete", - offset: ordinal, - limit: 1, - }); - assert.equal(page.coverage.total, oracle.total_records); - assert.equal(page.items.length, 1); - records.push(page.items[0]); - } - } - if (evidence !== undefined) { - assert.equal(evidence.operation, "inspect_web_network_capture"); - assert.deepEqual( - evidence.parameters, - sazEvidenceParameters(input, oracle.total_records), - ); - const report = checked(evidence, webNetworkCaptureSchema); - assert.equal(report.format, "saz"); - assert.equal(report.total_records, oracle.total_records); - assert.equal( - report.records.length, - values["saz-selection"] === "baseline" - ? oracle.total_records - : oracle.records.length, - ); - if (values["saz-selection"] === "baseline") - assert.deepEqual( - report.records.map((record) => record.ordinal), - Array.from({ length: oracle.total_records }, (_, index) => index), - ); - records = - values["saz-selection"] === "baseline" - ? oracle.record_ordinals.map((ordinal) => - report.records.find((record) => record.ordinal === ordinal), - ) - : report.records; - decoder = report.decoder; - } - } else { - const initial = await call("inspect_web_network_capture", { - ...input, - detail: "summary", - }); - const summary = checked(initial, analysisViewResultSchema); - assert.equal(summary.kind, "summary"); - assert.equal(initial.operation, "inspect_analysis_view"); - assert.deepEqual(initial.parameters, { - parent_evidence_id: summary.parent_evidence_id, - view: { kind: "summary" }, - }); - assert.equal(summary.summary.format, "saz"); - assert.equal(summary.parent_operation, "inspect_web_network_capture"); - assert.equal(summary.summary.total_records, oracle.total_records); - assert.equal(summary.summary.retained_records, oracle.records.length); - decoder = summary.summary.decoder; - const source = { - kind: "retained-evidence", - evidence_id: summary.parent_evidence_id, - }; - const page = async (detail, offset, limit) => { - return readView(source, { kind: "capture", detail, offset, limit }); - }; - const preview = await page("preview", 0, 3); - assert.deepEqual( - preview.items.map((record) => record.ordinal), - oracle.record_ordinals, - ); - const first = await page("complete", 0, 2), - next = await page("complete", 2, 2), - repeated = await page("complete", 0, 2); - assert.deepEqual(first.items, repeated.items); - assert.deepEqual(first.coverage, repeated.coverage); - assert.equal(first.coverage.next_offset, 2); - assert.equal(first.coverage.exhausted, false); - assert.equal(next.coverage.exhausted, true); - assert.equal(next.coverage.next_offset, null); - records = [...first.items, ...next.items]; - } - return { - artifact_sha256: inputSha, - total_records: oracle.total_records, - decoder, - facts: verifySazRecords(records, oracle), - delivery_observations: observations, - }; -} diff --git a/tests/support/analysis-efficiency/saz-oracle.py b/tests/support/analysis-efficiency/saz-oracle.py deleted file mode 100644 index 6722289a2a..0000000000 --- a/tests/support/analysis-efficiency/saz-oracle.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Independent stored-HTTP facts for two immutable public Fiddler archives. - -Only digest-pinned inputs are admitted before ZIP object allocation. Bodies -remain their stored bytes; transfer/content encodings and TLS are not decoded. -""" - -import argparse -import base64 -import hashlib -import io -import json -from pathlib import Path -import re -import zipfile - - -def stored_http(data, request): - boundaries = [(data.find(separator), separator) - for separator in [b'\r\n\r\n', b'\n\n']] - available = [(index, separator) for index, separator in boundaries if index >= 0] - if not available: - raise ValueError('Stored message has no header/body separator') - boundary, separator = min(available, key=lambda item: item[0]) - lines = data[:boundary].split(b'\r\n' if separator.startswith(b'\r') else b'\n') - start = lines[0].decode('latin1') - fields = start.split(' ') - if request: - if len(fields) != 3 or fields[2] not in ['HTTP/1.0', 'HTTP/1.1']: - raise ValueError('Unsupported stored request line') - result = dict(zip(['method', 'url', 'http_version'], fields)) - else: - fields = start.split(' ', 2) - if len(fields) < 2 or fields[0] not in ['HTTP/1.0', 'HTTP/1.1'] or not re.fullmatch(r'[0-9]{3}', fields[1]): - raise ValueError('Unsupported stored response line') - result = {'http_version': fields[0], 'status': fields[1], - 'reason': fields[2] if len(fields) == 3 else ''} - headers = [] - newline = '\r\n' if separator.startswith(b'\r') else '\n' - for raw in lines[1:]: - line = raw.decode('latin1') - if line.startswith((' ', '\t')): - if not headers: - raise ValueError('Header continuation has no owning field') - headers[-1]['value'] += newline + line - continue - name, delimiter, value = line.partition(':') - if not delimiter or not name: - raise ValueError('Stored header has no field delimiter') - headers.append({'name': name, 'value': value}) - body = data[boundary + len(separator):] - try: - body_text = body.decode('utf8', errors='strict') - except UnicodeDecodeError: - body_text = None - return result | {'headers': headers, 'body': { - 'content_base64': base64.b64encode(body).decode('ascii'), - 'bytes': len(body), 'sha256': hashlib.sha256(body).hexdigest(), - 'text': body_text, - }} - - -def read_saz(artifact, path): - corpus = json.loads(Path(__file__).with_name('corpus.json').read_text()) - entry = next((item for item in corpus['artifacts'] - if item['id'] == artifact and 'saz_question' in item), None) - if entry is None: - raise ValueError('Select a pinned SAZ artifact') - # Real source sizes bound the snapshot; size/digest gate ZIP allocation. - if path.stat().st_size != entry['bytes']: - raise ValueError('Selected file size differs from pinned SAZ corpus') - with path.open('rb') as source: - data = source.read(entry['bytes'] + 1) - digest = hashlib.sha256(data).hexdigest() - if len(data) != entry['bytes'] or digest != entry['sha256']: - raise ValueError('Selected bytes differ from pinned SAZ corpus') - question = entry['saz_question'] - with zipfile.ZipFile(io.BytesIO(data)) as archive: - inventory = archive.infolist() - if (len(inventory) != question['members'] or - sum(item.file_size for item in inventory) != question['expanded_bytes']): - raise ValueError('Pinned directory coverage differs') - groups = {} - for item in inventory: - match = re.fullmatch(r'raw/(\d+)_(c\.txt|s\.txt|m\.xml|w\.txt|g\.txt)', item.filename) - if match: - groups.setdefault(match[1], []).append(item) - if len(groups) != question['total_records']: - raise ValueError('Pinned session coverage differs') - ordered = list(groups.items()) - records = [] - for ordinal in question['record_ordinals']: - session_id, members = ordered[ordinal] - result = {'ordinal': ordinal, 'session_id': session_id, - 'members': [item.filename for item in members], - 'request': None, 'response': None} - for item in members: - side = 'request' if item.filename.endswith('_c.txt') else 'response' if item.filename.endswith('_s.txt') else None - if side is None: - continue - with archive.open(item) as source: - content = source.read(item.file_size + 1) - if len(content) != item.file_size: - raise ValueError('Stored member size differs') - result[side] = {'member': item.filename, **stored_http(content, side == 'request')} - records.append(result) - with path.open('rb') as source: - after = source.read(entry['bytes'] + 1) - if hashlib.sha256(after).hexdigest() != digest: - raise ValueError('Selected archive changed during independent read') - return {'kind': 'saz', 'artifact': artifact, 'sha256': digest, - 'total_records': len(groups), 'record_ordinals': question['record_ordinals'], - 'records': records, - 'scope': 'Stored HTTP bytes and ZIP member coordinates; no body decompression, live attribution or certificate interpretation'} - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('artifact') - parser.add_argument('path', type=Path) - args = parser.parse_args() - print(json.dumps(read_saz(args.artifact, args.path))) diff --git a/tests/support/analysis-efficiency/signal-entry.mjs b/tests/support/analysis-efficiency/signal-entry.mjs deleted file mode 100644 index b44d4d66a0..0000000000 --- a/tests/support/analysis-efficiency/signal-entry.mjs +++ /dev/null @@ -1,264 +0,0 @@ -import { execFile } from "node:child_process"; -import { readFile } from "node:fs/promises"; -import { resolve } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import { isDeepStrictEqual, promisify } from "node:util"; - -const corpus = JSON.parse( - await readFile(new URL("./corpus.json", import.meta.url), "utf8"), -); - -export const SIGNAL_ARTIFACTS = { - "sigrok-uart-max3232e.sr": { - kind: "uart", - decoders: "uart:rx=MAX3232E DOUT1:baudrate=57600:format=hex:invert_rx=yes", - annotations: "uart=rx-data", - query: "rx-data", - firstPage: 32, - }, - "sigrok-i2c-nunchuk.sr": { - kind: "i2c", - decoders: "i2c:scl=SCL:sda=SDA", - annotations: "i2c=data-read", - query: "data-read", - firstPage: 9, - }, -}; - -export function signalRequest(artifact, path, selection) { - const descriptor = SIGNAL_ARTIFACTS[artifact]; - if (!descriptor) throw new Error("Unknown fixed signal question"); - return { - path, - decoders: descriptor.decoders, - input_format: null, - annotations: selection === "baseline" ? null : descriptor.annotations, - line_offset: 0, - line_limit: selection === "pages" ? descriptor.firstPage : null, - }; -} - -export function signalDataRows(kind, text) { - const pattern = - kind === "uart" - ? /^(\d+)-(\d+) uart-1: ([0-9A-F]{2})$/ - : /^(\d+)-(\d+) i2c-1: Data read: ([0-9A-F]{2})$/; - const rows = []; - for (const line of text.split("\n")) { - const match = pattern.exec(line); - if (!match) continue; - const start = Number(match[1]), - end = Number(match[2]); - if ( - !Number.isSafeInteger(start) || - !Number.isSafeInteger(end) || - start > end - ) - throw new Error("Unsupported decoded sample interval"); - rows.push({ start, end, byte: Number.parseInt(match[3], 16) }); - } - return rows; -} - -export function signalFacts(kind, rows) { - if (kind === "uart") return { kind, bytes: rows.map((row) => row.byte) }; - if (kind !== "i2c" || rows.length !== 18) - throw new Error( - "The fixed I2C question requires three complete six-byte reads", - ); - return { - kind, - reads: [0, 6, 12].map((offset) => - rows.slice(offset, offset + 6).map((row) => row.byte), - ), - }; -} - -export function verifySignalOracleIdentity(artifact, oracle) { - const pinned = corpus.artifacts.find((entry) => entry.id === artifact); - if ( - !pinned || - oracle.kind !== SIGNAL_ARTIFACTS[artifact]?.kind || - oracle.sha256 !== pinned.sha256 - ) - throw new Error( - "Independent oracle does not identify the pinned signal corpus", - ); -} - -export function verifySignalRows(kind, rows, oracle, digest) { - const expected = - kind === "uart" - ? oracle.rows - : oracle.reads.flatMap((read) => - read.bytes.slice(1).map((byte, index) => ({ - ...byte, - after_previous: read.bytes[index].last_clock, - before_next: read.bytes[index + 2]?.first_clock ?? read.stop, - })), - ); - if ( - oracle.kind !== kind || - oracle.sha256 !== digest || - rows.length !== expected.length - ) - throw new Error( - "Signal observations differ from independent sample identity/coverage", - ); - for (let index = 0; index < rows.length; index++) { - const observed = rows[index], - raw = expected[index]; - if ( - observed.byte !== raw.byte || - observed.start < 0 || - observed.end >= oracle.total_samples - ) - throw new Error("Decoded data differs from independent sample bits"); - if ( - kind === "uart" - ? !Number.isSafeInteger(raw.first_data_sample) || - !Number.isSafeInteger(raw.last_data_sample) || - observed.start < raw.start || - observed.end >= raw.end_exclusive || - observed.start > raw.first_data_sample || - observed.end < raw.last_data_sample - : !Number.isSafeInteger(raw.after_previous) || - !Number.isSafeInteger(raw.before_next) || - observed.start <= raw.after_previous || - observed.end >= raw.before_next || - observed.start > raw.first_clock || - observed.end < raw.last_clock - ) - throw new Error( - "Decoded interval does not identify the independently read byte", - ); - } -} - -export async function measureSignalEntry({ - values, - call, - inputSha, - catalogDefinitions, -}) { - const descriptor = SIGNAL_ARTIFACTS[values.artifact]; - const runtime = (file) => - pathToFileURL(resolve(values["runtime-root"], file)).href; - const { parseEvidence } = await import(runtime("dist/domain/evidence.js")); - const { decodeSignalRecordingInputSchema, signalRecordingSchema } = - await import(runtime("dist/domain/signals/signalRecording.js")); - const { inspectSignalDecodersInputSchema, signalDecodersSchema } = - await import(runtime("dist/domain/signals/signalDecoders.js")); - const observations = []; - const checked = (evidence, operation, input, schema) => { - parseEvidence(evidence); - const report = schema.parse(evidence.normalized_result); - if ( - evidence.operation !== operation || - !isDeepStrictEqual(evidence.parameters, input) || - !isDeepStrictEqual(report.selection, input) - ) - throw new Error( - "Signal Evidence differs from the actual requested operation or choices", - ); - if ( - operation === "decode_signal_recording" - ? evidence.subject?.local_path !== values.path || - evidence.subject?.digest?.sha256 !== inputSha || - report.artifact.path !== values.path || - report.artifact.sha256 !== inputSha - : evidence.subject?.local_path !== report.tool.path || - evidence.subject?.digest?.sha256 !== report.tool.sha256 - ) - throw new Error( - "Signal Evidence identifies different recording/tool bytes", - ); - observations.push({ - evidence_id: evidence.evidence_id, - operation, - selection: report.selection, - output_window: report.output_window, - limitations: evidence.limitations, - }); - return report; - }; - const selection = values["signal-selection"]; - for (const name of [ - "decode_signal_recording", - ...(selection === "discovery" ? ["inspect_signal_decoders"] : []), - ]) - if (!catalogDefinitions.has(name)) - throw new Error(`Missing live signal contract ${name}`); - if (selection === "discovery") { - const input = inspectSignalDecodersInputSchema.parse({ - decoder: descriptor.kind, - query: descriptor.query, - limit: 5, - }); - const report = checked( - await call("inspect_signal_decoders", input), - "inspect_signal_decoders", - input, - signalDecodersSchema, - ); - if (!report.help_lines.some((line) => line.text.includes(descriptor.query))) - throw new Error( - "Installed decoder help does not establish the selected annotation class", - ); - } - const input = decodeSignalRecordingInputSchema.parse( - signalRequest(values.artifact, values.path, selection), - ); - const first = checked( - await call("decode_signal_recording", input), - "decode_signal_recording", - input, - signalRecordingSchema, - ); - let rows = signalDataRows(descriptor.kind, first.annotations_text); - if (selection === "pages") { - const nextInput = decodeSignalRecordingInputSchema.parse({ - ...input, - line_offset: descriptor.firstPage, - line_limit: null, - }); - const next = checked( - await call("decode_signal_recording", nextInput), - "decode_signal_recording", - nextInput, - signalRecordingSchema, - ); - const repeated = checked( - await call("decode_signal_recording", input), - "decode_signal_recording", - input, - signalRecordingSchema, - ); - if ( - first.annotations_text !== repeated.annotations_text || - !isDeepStrictEqual(first.output_window, repeated.output_window) || - first.output_window.total_lines !== next.output_window.total_lines - ) - throw new Error("Repeated/next display pages changed observation scope"); - rows = [...rows, ...signalDataRows(descriptor.kind, next.annotations_text)]; - } - const { stdout } = await promisify(execFile)( - "python3", - [ - fileURLToPath(new URL("./signal-oracle.py", import.meta.url)), - descriptor.kind, - values.path, - ...(descriptor.kind === "uart" ? ["--invert"] : []), - ], - { timeout: 30_000, maxBuffer: 256 * 1024 }, - ); - const oracle = JSON.parse(stdout); - verifySignalOracleIdentity(values.artifact, oracle); - verifySignalRows(descriptor.kind, rows, oracle, inputSha); - return { - artifact_sha256: inputSha, - facts: signalFacts(descriptor.kind, rows), - delivery_observations: observations, - evidence_ids: [...new Set(observations.map((item) => item.evidence_id))], - }; -} diff --git a/tests/support/analysis-efficiency/signal-oracle.py b/tests/support/analysis-efficiency/signal-oracle.py deleted file mode 100644 index dbccf01a24..0000000000 --- a/tests/support/analysis-efficiency/signal-oracle.py +++ /dev/null @@ -1,211 +0,0 @@ -"""Independent readers for the two pinned, single-device digital SR recordings.""" - -import argparse -from bisect import bisect_right -import configparser -import hashlib -import io -import json -from pathlib import Path -import re -import struct -import zipfile - - -def read_edges(path, channels): - size = path.stat().st_size - if size > 32 * 1024 * 1024: - raise ValueError('Compressed SR input exceeds the experiment budget') - with path.open('rb') as source: - admitted = source.read(size + 1) - if len(admitted) != size: - raise ValueError('SR input size changed during admission') - marker = admitted.rfind(b'PK\x05\x06', max(0, len(admitted) - 65557)) - if marker < 0 or len(admitted) - marker < 22: - raise ValueError('Missing classic ZIP directory receipt') - header = struct.unpack_from('<4s4H2IH', admitted, marker) - if (marker >= 20 and admitted[marker-20:marker-16] == b'PK\x06\x07'): - raise ValueError('ZIP64 directory is unsupported') - if header[1] or header[2] or header[3] != header[4] or not 1 <= header[4] <= 64 or header[5] > 65536 or marker + 22 + header[7] != len(admitted) or header[6] + header[5] != marker: - raise ValueError('Unsupported or excessive ZIP directory representation') - directory = memoryview(admitted)[header[6]:marker] - position = count = 0 - while position < len(directory): - if count >= 64 or len(directory) - position < 46 or directory[position:position+4] != b'PK\x01\x02': - raise ValueError('Unsupported or excessive actual ZIP directory records') - name_size, extra_size, comment_size = struct.unpack_from('<3H', directory, position + 28) - end = position + 46 + name_size + extra_size + comment_size - if end > len(directory): - raise ValueError('Incomplete actual ZIP directory record') - extra = position + 46 + name_size - extra_end = extra + extra_size - while extra < extra_end: - if extra_end - extra < 4: - raise ValueError('Incomplete ZIP extra field') - tag, length = struct.unpack_from('<2H', directory, extra) - if tag == 1: - raise ValueError('ZIP64 member is unsupported') - extra += 4 + length - if extra > extra_end: - raise ValueError('Incomplete ZIP extra field') - count += 1 - position = end - if count != header[4]: - raise ValueError('Declared and actual ZIP directory counts differ') - before = hashlib.sha256(admitted).hexdigest() - # Admission and ZipFile parse the same bounded snapshot, not a reopened path. - with zipfile.ZipFile(io.BytesIO(admitted), allowZip64=False) as archive: - entries = archive.infolist() - names = [item.filename for item in entries] - if len(names) != len(set(names)) or len(names) > 64: - raise ValueError("Ambiguous or excessive SR members") - if sum(item.file_size for item in entries) > 32 * 1024 * 1024: - raise ValueError("Expanded SR input exceeds the experiment budget") - if archive.getinfo('metadata').file_size > 8192: - raise ValueError("Excessive SR metadata") - # Declared sizes are untrusted. A bounded read also caps each inflater - # step; logic members below likewise use bounded block reads. - with archive.open('metadata') as source: - metadata_bytes = source.read(8193) - if len(metadata_bytes) > 8192: - raise ValueError("Expanded SR metadata exceeds the experiment budget") - metadata = metadata_bytes.decode('utf-8') - config = configparser.ConfigParser(interpolation=None) - config.read_string(metadata) - if set(config.sections()) != {'global', 'device 1'}: - raise ValueError("Only the pinned single-device profile is supported") - device = config['device 1'] - unit = int(device['unitsize']) - probes = int(device['total probes']) - if unit not in [1, 2] or not 1 <= probes <= unit * 8: - raise ValueError("Unsupported digital sample geometry") - rate = re.fullmatch(r'(\d+)\s+(Hz|kHz|MHz)', device['samplerate']) - if rate is None or int(rate[1]) <= 0: - raise ValueError("Unsupported sample-rate representation") - samplerate = int(rate[1]) * {'Hz': 1, 'kHz': 1000, 'MHz': 1000000}[rate[2]] - bits = [] - for channel in channels: - matches = [index - 1 for index in range(1, probes + 1) - if device.get(f'probe{index}') == channel] - if len(matches) != 1: - raise ValueError("Channel identity is missing or ambiguous") - bits.append(matches[0]) - prefix = device['capturefile'] - chunks = [name for name in names if re.fullmatch(re.escape(prefix) + r'-\d+', name)] - if prefix in names and chunks: - raise ValueError("Ambiguous direct and chunked logic data") - if prefix in names: - chunks = [prefix] - else: - chunks.sort(key=lambda name: int(name.rsplit('-', 1)[1])) - if [int(name.rsplit('-', 1)[1]) for name in chunks] != list(range(1, len(chunks) + 1)): - raise ValueError("Missing or ambiguous logic chunk order") - if not chunks or any(archive.getinfo(name).file_size % unit for name in chunks): - raise ValueError("Missing or incomplete digital samples") - edges = [[] for _ in channels] - previous = None - sample = 0 - mask = sum(1 << bit for bit in bits) - for name in chunks: - with archive.open(name) as source: - while block := source.read(65536): - values = block if unit == 1 else (item[0] for item in struct.iter_unpack('> bit) & 1 - if not edges[index] or edges[index][-1][1] != level: - if sum(map(len, edges)) >= 65536: - raise ValueError('Selected transitions exceed the experiment representation budget') - edges[index].append((sample, level)) - previous = selected - sample += 1 - with path.open('rb') as source: - after = source.read(size + 1) - if len(after) != size or hashlib.sha256(after).hexdigest() != before: - raise ValueError("Recording bytes changed during the independent read") - return {'sha256': before, 'metadata': metadata, 'sample_rate_hz': samplerate, - 'total_samples': sample, 'channels': dict(zip(channels, bits))}, edges - - -def uart(path, invert): - metadata, traces = read_edges(path, ['MAX3232E DOUT1']) - edges = [(sample, level ^ invert) for sample, level in traces[0]] - positions = [sample for sample, _ in edges] - rate, baud = metadata['sample_rate_hz'], 57600 - def level(sample): - return edges[bisect_right(positions, sample) - 1][1] - rows = [] - busy_until = 0 - for index, (start, value) in enumerate(edges): - if index == 0 or value != 0 or start < busy_until: - continue - center = lambda halfbits: start + (halfbits * rate) // (2 * baud) - if center(19) >= metadata['total_samples']: - raise ValueError("Incomplete UART frame at the capture boundary") - if level(center(1)) != 0 or level(center(19)) != 1: - raise ValueError(f"UART framing differs from selected polarity at sample {start}") - byte = sum(level(center(3 + 2 * bit)) << bit for bit in range(8)) - # A UART receiver becomes ready after sampling the stop bit. Waiting - # a full nominal ten bits would miss a valid slightly faster sender. - rows.append({'start': start, 'end_exclusive': center(19) + 1, 'byte': byte, - 'first_data_sample': center(3), 'last_data_sample': center(17)}) - busy_until = center(19) + 1 - return metadata | {'kind': 'uart', 'baud_rate': baud, 'invert': bool(invert), - 'complete_frames': len(rows), 'rows': rows} - - -def i2c(path): - metadata, traces = read_edges(path, ['SCL', 'SDA']) - transitions = {} - for channel, edges in enumerate(traces): - for sample, value in edges: - transitions.setdefault(sample, {})[channel] = value - clock = data = None - active = None - bits = [] - transactions = [] - for sample, changes in sorted(transitions.items()): - next_clock, next_data = changes.get(0, clock), changes.get(1, data) - if clock is not None and data is not None: - if clock == next_clock == 1 and next_data != data: - if next_data == 0: - if active is not None: - raise ValueError("The pinned profile does not contain repeated START") - active = {'start': sample, 'bytes': []} - bits = [] - elif active is not None: - # SCL may rise with SDA low to prepare STOP; that final - # rise is not the first bit of a new data byte. - if len(bits) == 1 and bits[0][1] == 0: - bits = [] - if bits: - raise ValueError("Incomplete I2C byte before STOP") - active['stop'] = sample - transactions.append(active) - active = None - elif active is not None and clock == 0 and next_clock == 1: - if next_data != data: - raise ValueError("Ambiguous simultaneous clock/data transition") - bits.append((sample, next_data)) - if len(bits) == 9: - byte = sum(bit << (7-index) for index, (_, bit) in enumerate(bits[:8])) - active['bytes'].append({'byte': byte, 'first_clock': bits[0][0], - 'last_clock': bits[7][0], 'ack': bits[8][1] == 0}) - bits = [] - clock, data = next_clock, next_data - if active is not None or bits: - raise ValueError("Incomplete I2C transaction at capture end") - reads = [transaction for transaction in transactions - if transaction['bytes'] and transaction['bytes'][0]['byte'] & 1] - return metadata | {'kind': 'i2c', 'transactions': transactions, 'reads': reads} - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('kind', choices=['uart', 'i2c']) - parser.add_argument('path', type=Path) - parser.add_argument('--invert', action='store_true') - args = parser.parse_args() - print(json.dumps(uart(args.path, args.invert) if args.kind == 'uart' else i2c(args.path))) diff --git a/tests/support/analysis-efficiency/test_packet_oracle.py b/tests/support/analysis-efficiency/test_packet_oracle.py deleted file mode 100644 index 6727b3b7eb..0000000000 --- a/tests/support/analysis-efficiency/test_packet_oracle.py +++ /dev/null @@ -1,67 +0,0 @@ -import importlib.util -import struct -import unittest -from pathlib import Path - -spec = importlib.util.spec_from_file_location('packet_oracle', Path(__file__).with_name('packet-oracle.py')) -oracle = importlib.util.module_from_spec(spec) -spec.loader.exec_module(oracle) - - -class PacketOracleTest(unittest.TestCase): - def test_compression_pointer_preserves_question_cursor(self): - message = b'\x03www\x07example\x03com\0' + b'\xc0\0' + b'\0\x01\0\x01' - self.assertEqual(oracle.dns_name(message, 17), ('www.example.com', 19)) - - def test_compression_cycle_and_missing_pointer_byte_fail(self): - for message in (b'\xc0\0', b'\xc0', b'\xc0\x02\xc0\0'): - with self.subTest(message=message), self.assertRaises(ValueError): - oracle.dns_name(message, 0) - - def test_truncated_pcap_record_is_not_exhaustion(self): - header = struct.pack('> index) & 1 for index in range(8)], 1]: - levels.extend([bit] * 17) - levels.extend([1] * 100) - data = b''.join(struct.pack('> index) & 1 for index in reversed(range(8))] + [ack] - if incomplete and byte == 0x75: - values = values[:4] - for bit in values: - states.extend([(0, bit), (1, bit)]) - states.extend([(0, 0), (1, 0), (1, 1)]) - return bytes(clock | data << 1 for clock, data in states) - - def test_i2c_stop_setup_is_distinct_from_another_data_byte(self): - self.recording(self.i2c_levels(), 1, ['SCL', 'SDA']) - result = oracle.i2c(self.path) - self.assertEqual(len(result['reads']), 1) - self.assertEqual([row['byte'] for row in result['reads'][0]['bytes']], [0xA5, 0x75]) - self.assertFalse(result['reads'][0]['bytes'][-1]['ack']) - - def test_incomplete_i2c_byte_is_not_invented_at_stop(self): - self.recording(self.i2c_levels(incomplete=True), 1, ['SCL', 'SDA']) - with self.assertRaisesRegex(ValueError, 'Incomplete I2C byte'): - oracle.i2c(self.path) - - def test_channel_names_must_identify_one_stored_bit(self): - self.recording(b'\x00', 1, ['SCL', 'SCL']) - with self.assertRaisesRegex(ValueError, 'ambiguous'): - oracle.read_edges(self.path, ['SCL']) - - def test_missing_chunk_is_not_renumbered(self): - self.recording(b'\x00', 1, ['SCL', 'SDA'], chunk='logic-1-2') - with self.assertRaisesRegex(ValueError, 'chunk order'): - oracle.read_edges(self.path, ['SCL', 'SDA']) - - def test_partial_sample_word_is_rejected(self): - self.recording(b'\x00', 2, ['unused', 'MAX3232E DOUT1']) - with self.assertRaisesRegex(ValueError, 'incomplete digital samples'): - oracle.read_edges(self.path, ['MAX3232E DOUT1']) - - def test_directory_budget_precedes_zip_object_allocation(self): - self.recording(b'\x00', 1, ['SCL', 'SDA'], extra=62) - with patch.object(oracle.zipfile, 'ZipFile', side_effect=AssertionError('Too late')): - with self.assertRaisesRegex(ValueError, 'excessive ZIP directory'): - oracle.read_edges(self.path, ['SCL', 'SDA']) - - def test_declared_expansion_is_rejected_before_decompression(self): - self.recording(b'\x00', 1, ['SCL', 'SDA']) - data = bytearray(self.path.read_bytes()) - directory = data.index(b'PK\x01\x02') - struct.pack_into('