diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f851393e..d1bb5bef5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -311,7 +311,7 @@ jobs: - name: Real binary metadata and Interface Builder integration run: npm run verify:apple-dispatch && npm run verify:interface-builder - name: Native plist CLI selection regressions - run: npm run test:focused -- tests/boundary/cli/nativePlistCli.test.ts + run: npm run test:focused -- tests/boundary/cli/nativePlistCli.test.ts tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts tests/boundary/cli/nativePlistSelectionFailureCli.test.ts reconstruction-readiness: name: Reconstruction readiness diff --git a/.github/workflows/website-check.yml b/.github/workflows/website-check.yml index 42257f867..4f410ef62 100644 --- a/.github/workflows/website-check.yml +++ b/.github/workflows/website-check.yml @@ -6,6 +6,7 @@ on: paths: - "website/**" - "scripts/verify-website.py" + - "scripts/prepare-website.py" - ".github/workflows/**" workflow_dispatch: @@ -24,5 +25,7 @@ jobs: steps: - name: Check out the repository uses: actions/checkout@v4 + - name: Prepare the example download + run: python3 scripts/prepare-website.py - name: Check website links and publishing configuration run: python3 scripts/verify-website.py diff --git a/.github/workflows/website-pages.yml b/.github/workflows/website-pages.yml index a6118bb6a..52908cd8b 100644 --- a/.github/workflows/website-pages.yml +++ b/.github/workflows/website-pages.yml @@ -21,6 +21,8 @@ jobs: steps: - name: Check out the repository uses: actions/checkout@v4 + - name: Prepare the example download + run: python3 scripts/prepare-website.py - name: Check website links and publishing configuration run: python3 scripts/verify-website.py - name: Configure Pages diff --git a/.gitignore b/.gitignore index e699d15fd..330e03a16 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ Thumbs.db # Local reference sources and analysis artifacts /_reference/ + +# Generated website download +/website/public/examples/notes-example.zip diff --git a/docs/capability-migration.md b/docs/capability-migration.md index 5761279c1..3789591d6 100644 --- a/docs/capability-migration.md +++ b/docs/capability-migration.md @@ -356,3 +356,32 @@ repository-root default. Both point to `_reference/firmware-integration/generate when invoked through the documented npm commands. Optional ext4/Ghidra lanes keep their own prerequisites. Source-fixture generation and synthetic producer tests do not establish real Binwalk/Unblob analysis or another host's execution support. + +## Apple artifact capability ownership + +`src/domain/apple/` owns Apple application anatomy/projection, asset catalog +facts, Interface Builder graphs, keyed archive semantics and plist values. +`src/application/apple/AppleApplicationService.ts` retains the shared workflow +that projects authenticated IPA/macOS inventory Evidence. + +`src/artifacts/apple/` owns the actual producer boundary: directory acquisition, +assetutil invocation, plist/NIB decoding, encoded view-parent interpretation and +keyed archive inspection. These helpers are used by ArtifactProvider, retain +its existing typed analysis boundary, injected assetutil seam and original reader +cleanup, and do not require a new port for each helper. Producer code no longer +lives in the application layer. Guards reject application/composition/caller +imports from this owner while admitting pure facts and shared artifact readers. +Adapter-source tests retain their assertions in the existing forked adapter lane. + +`scripts/verify/apple/` groups archive, asset catalog, Interface Builder, +Objective-C/Swift dispatch and macOS bundle verification. The macOS bundle fixture +builder lives in `scripts/fixtures/apple/`; NIB byte fixtures stay beside their +artifact decoder. Existing format-specific conformance sources and goldens keep +their locations, and relocated verifiers resolve them against their own file +URLs. Public npm entrypoints and CLI/MCP contracts stay stable; generated-document +cache inputs include the relocated runtime scripts and fixture builder. + +Portable decoder and injected producer tests establish their format/boundary +claims. Actual Swift/Xcode, assetutil, native NIB, bundle signing and DMG lifecycle +claims require the real Apple lane. Native binary API/value-flow semantics and +shared cross-domain contract aggregates keep their separate owners. diff --git a/docs/testing.md b/docs/testing.md index 024d3a1e5..8d77e8090 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -98,6 +98,12 @@ covers `trace-dylib-resolution`: It runs in macOS CI. +Apple artifact verifiers live in `scripts/verify/apple/`, with the macOS bundle +builder under `scripts/fixtures/apple/` and NIB byte fixtures beside the decoder +in `src/artifacts/apple/`. The npm entrypoints are unchanged. Format-specific +Swift/XIB/asset sources and goldens retain their locations; real Apple workflows +resolve them from the verifier file URL and run in the macOS CI lane. + MCP SDK transport tests with recording providers remain integration tests. They are useful for schema drift and failure projection but do not prove that Hopper, Ghidra or another substituted engine works. `verify:package` proves diff --git a/package.json b/package.json index ad1e9cab9..bfdb50a05 100644 --- a/package.json +++ b/package.json @@ -121,17 +121,17 @@ "typecheck": "tsc --noEmit", "verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs", "verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs", - "verify:apple-dispatch": "npm run build:cached && node scripts/verify-apple-dispatch.mjs", + "verify:apple-dispatch": "npm run build:cached && node scripts/verify/apple/dispatch.mjs", "verify:native-ui": "npm run build:cached && node scripts/verify-native-ui.mjs", - "verify:keyed-archive": "npm run build:cached && node scripts/verify-keyed-archive.mjs", - "verify:macos-bundle": "npm run build:cached && node scripts/verify-macos-bundle.mjs", + "verify:keyed-archive": "npm run build:cached && node scripts/verify/apple/keyed-archive.mjs", + "verify:macos-bundle": "npm run build:cached && node scripts/verify/apple/macos-bundle.mjs", "verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs", "verify:ida": "npm run build:cached && node scripts/verify-real-ida.mjs", "verify:ghidra:switch": "npm run build:cached && node scripts/verify-real-ghidra-switch.mjs", "verify:ghidra:dos": "npm run build:cached && node scripts/verify-real-ghidra-dos.mjs", "verify:ghidra:com": "npm run build:cached && node scripts/verify-real-ghidra-com.mjs", "verify:ghidra:aarch64-jump-table": "npm run build:cached && node scripts/verify-real-ghidra.mjs --aarch64-jump-table", - "verify:interface-builder": "npm run build:cached && node scripts/verify-interface-builder.mjs", + "verify:interface-builder": "npm run build:cached && node scripts/verify/apple/interface-builder.mjs", "verify:ghidra:cross-format": "npm run build:cached && node scripts/verify-real-ghidra.mjs --cross-format", "verify:ghidra:windows": "npm run build:cached && npm run build:ghidra-windows-fixture && node scripts/verify-real-ghidra-windows.mjs", "verify:browser": "npm run build:cached && node scripts/verify-real-browser.mjs", @@ -171,7 +171,7 @@ "docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs", "prepack": "node scripts/prepack.mjs", "prepare": "node scripts/prepare.mjs", - "verify:asset-catalog": "npm run build:cached && node scripts/verify-asset-catalog.mjs", + "verify:asset-catalog": "npm run build:cached && node scripts/verify/apple/asset-catalog.mjs", "verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial", "test:focused": "node scripts/run-development-tests.mjs focused", "build:windows-native": "node scripts/build-windows-native.mjs", diff --git a/scripts/lib/macos-bundle-fixture.mjs b/scripts/fixtures/apple/macos-bundle.mjs similarity index 100% rename from scripts/lib/macos-bundle-fixture.mjs rename to scripts/fixtures/apple/macos-bundle.mjs diff --git a/scripts/lib/module-boundaries.mjs b/scripts/lib/module-boundaries.mjs index e43ccd44c..ca1e4d933 100644 --- a/scripts/lib/module-boundaries.mjs +++ b/scripts/lib/module-boundaries.mjs @@ -69,6 +69,7 @@ const failedBoundary = (file, target) => { return "provider-construction"; if ( (file.startsWith("src/artifacts/javascript/") || + file.startsWith("src/artifacts/apple/") || /^src\/artifacts\/ArtifactHash\.(?:js|ts)$/u.test(file)) && (["application", "composition", "server", "cli", "main"].includes( targetLayer, diff --git a/scripts/prepare-website.py b/scripts/prepare-website.py new file mode 100644 index 000000000..1abc9c39d --- /dev/null +++ b/scripts/prepare-website.py @@ -0,0 +1,40 @@ +"""Package the public Notes example for the static website.""" + +from io import BytesIO +from pathlib import Path +from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo + + +EXAMPLE_FILES = ( + "package.json", + "main.js", + "preload.js", + "renderer.js", + "csv.js", + "index.html", +) + + +def prepare_notes_example(site): + """Create a ZIP with public source only and reproducible metadata.""" + source = site / "examples/notes-electron" + contents = BytesIO() + with ZipFile(contents, "w") as archive: + for name in EXAMPLE_FILES: + entry = ZipInfo(f"notes-example/{name}", (1980, 1, 1, 0, 0, 0)) + entry.create_system = 3 + entry.external_attr = 0o100644 << 16 + archive.writestr( + entry, + (source / name).read_bytes(), + compress_type=ZIP_DEFLATED, + compresslevel=9, + ) + destination = site / "examples/notes-example.zip" + destination.write_bytes(contents.getvalue()) + print(f"Prepared examples/notes-example.zip ({len(EXAMPLE_FILES)} files).") + + +if __name__ == "__main__": + root = Path(__file__).resolve().parent.parent + prepare_notes_example(root / "website/public") diff --git a/scripts/verify-website.py b/scripts/verify-website.py index 60e8e0b0f..d6f4a6be0 100644 --- a/scripts/verify-website.py +++ b/scripts/verify-website.py @@ -6,6 +6,7 @@ import re import sys from urllib.parse import unquote, urlsplit import xml.etree.ElementTree as ET +from zipfile import BadZipFile, ZipFile class HtmlReferences(HTMLParser): @@ -89,15 +90,39 @@ def check_publisher(root): return [] +def check_example_archive(site): + """Verify the downloadable ZIP contains the current six source files.""" + source = site / "examples/notes-electron" + expected = { + f"notes-example/{name}": source / name + for name in ("package.json", "main.js", "preload.js", "renderer.js", "csv.js", "index.html") + } + path = site / "examples/notes-example.zip" + if not path.is_file(): + return ["Notes example ZIP is missing; run python3 scripts/prepare-website.py."] + try: + with ZipFile(path) as archive: + if sorted(archive.namelist()) != sorted(expected): + return ["Notes example ZIP must contain exactly the six files in notes-example/."] + return [ + f"Notes example ZIP has stale source: {name}" + for name, original in expected.items() + if archive.read(name) != original.read_bytes() + ] + except (BadZipFile, RuntimeError) as error: + return [f"Notes example ZIP cannot be read: {error}"] + + def main(): """Run the same checks locally, on website PRs and before publishing.""" root = Path(__file__).resolve().parent.parent pages, references, errors = check_site(root / "website/public") + errors.extend(check_example_archive(root / "website/public")) errors.extend(check_publisher(root)) if errors: print("Website checks failed:\n" + "\n".join(errors), file=sys.stderr) return 1 - print(f"Verified {pages} HTML pages, {references} local references and SVG XML.") + print(f"Verified {pages} HTML pages, {references} local references, SVG XML and the example ZIP.") print("website-pages.yml is the sole GitHub Pages publisher.") return 0 diff --git a/scripts/verify-asset-catalog.mjs b/scripts/verify/apple/asset-catalog.mjs similarity index 93% rename from scripts/verify-asset-catalog.mjs rename to scripts/verify/apple/asset-catalog.mjs index 84087f371..8a62396f1 100644 --- a/scripts/verify-asset-catalog.mjs +++ b/scripts/verify/apple/asset-catalog.mjs @@ -10,7 +10,7 @@ import { artifactCli, artifactMcpResult, withArtifactMcp, -} from "./lib/artifact-e2e.mjs"; +} from "../../lib/artifact-e2e.mjs"; if (process.platform !== "darwin") throw new Error( @@ -30,7 +30,7 @@ try { "clang", fileURLToPath( new URL( - "../tests/conformance/interface-builder/fixture.c", + "../../../tests/conformance/interface-builder/fixture.c", import.meta.url, ), ), @@ -44,7 +44,10 @@ try { await exec("/usr/bin/xcrun", [ "actool", fileURLToPath( - new URL("../tests/conformance/native/Assets.xcassets", import.meta.url), + new URL( + "../../../tests/conformance/native/Assets.xcassets", + import.meta.url, + ), ), "--compile", resources, diff --git a/scripts/verify-apple-dispatch.mjs b/scripts/verify/apple/dispatch.mjs similarity index 96% rename from scripts/verify-apple-dispatch.mjs rename to scripts/verify/apple/dispatch.mjs index 25e80c279..3e108c10e 100644 --- a/scripts/verify-apple-dispatch.mjs +++ b/scripts/verify/apple/dispatch.mjs @@ -4,8 +4,8 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; -import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js"; -import { inspectAppleDispatchMetadata } from "../dist/native/AppleDispatchMetadata.js"; +import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js"; +import { inspectAppleDispatchMetadata } from "../../../dist/native/AppleDispatchMetadata.js"; if (process.platform !== "darwin") throw new Error( @@ -14,7 +14,7 @@ if (process.platform !== "darwin") const exec = promisify(execFile); const source = (name) => fileURLToPath( - new URL(`../tests/conformance/native/${name}`, import.meta.url), + new URL(`../../../tests/conformance/native/${name}`, import.meta.url), ); /** Both link modes: legacy LC_DYLD_INFO binds and the default chained fixups. */ diff --git a/scripts/verify-interface-builder.mjs b/scripts/verify/apple/interface-builder.mjs similarity index 93% rename from scripts/verify-interface-builder.mjs rename to scripts/verify/apple/interface-builder.mjs index 350411dd0..1f8ec323e 100644 --- a/scripts/verify-interface-builder.mjs +++ b/scripts/verify/apple/interface-builder.mjs @@ -8,8 +8,8 @@ import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; -import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js"; -import { ArtifactProvider } from "../dist/artifacts/ArtifactProvider.js"; +import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js"; +import { ArtifactProvider } from "../../../dist/artifacts/ArtifactProvider.js"; const exec = promisify(execFile); const fixtureRoot = await mkdtemp(join(tmpdir(), "rea-interface-builder-")); @@ -18,7 +18,7 @@ const resourcesPath = join(appPath, "Contents", "Resources"); const executablePath = join(appPath, "Contents", "MacOS", "Fixture"); const nibPath = join(resourcesPath, "Main.nib"); const sourceRoot = fileURLToPath( - new URL("../tests/conformance/interface-builder/", import.meta.url), + new URL("../../../tests/conformance/interface-builder/", import.meta.url), ); try { diff --git a/scripts/verify-keyed-archive.mjs b/scripts/verify/apple/keyed-archive.mjs similarity index 94% rename from scripts/verify-keyed-archive.mjs rename to scripts/verify/apple/keyed-archive.mjs index b9f9aa480..924c3bb0e 100644 --- a/scripts/verify-keyed-archive.mjs +++ b/scripts/verify/apple/keyed-archive.mjs @@ -10,7 +10,7 @@ import { artifactCli, artifactMcpResult, withArtifactMcp, -} from "./lib/artifact-e2e.mjs"; +} from "../../lib/artifact-e2e.mjs"; if (process.platform !== "darwin") throw new Error( @@ -25,7 +25,7 @@ try { join(root, "modules"), fileURLToPath( new URL( - "../tests/conformance/native/keyed-archive.swift", + "../../../tests/conformance/native/keyed-archive.swift", import.meta.url, ), ), @@ -66,7 +66,7 @@ try { const expected = JSON.parse( await readFile( new URL( - "../tests/fixtures/golden/keyed-archive/graph.json", + "../../../tests/fixtures/golden/keyed-archive/graph.json", import.meta.url, ), "utf8", @@ -124,7 +124,7 @@ try { join(root, "modules"), fileURLToPath( new URL( - "../tests/conformance/native/keyed-archive-uid-root.swift", + "../../../tests/conformance/native/keyed-archive-uid-root.swift", import.meta.url, ), ), diff --git a/scripts/verify-macos-bundle.mjs b/scripts/verify/apple/macos-bundle.mjs similarity index 97% rename from scripts/verify-macos-bundle.mjs rename to scripts/verify/apple/macos-bundle.mjs index 0f3c986cc..972c8a7c4 100644 --- a/scripts/verify-macos-bundle.mjs +++ b/scripts/verify/apple/macos-bundle.mjs @@ -9,12 +9,12 @@ import { artifactCliEvidence, artifactMcpResult, withArtifactMcp, -} from "./lib/artifact-e2e.mjs"; +} from "../../lib/artifact-e2e.mjs"; import { buildMacosBundleFixture, preflightMacosBundleFixture, -} from "./lib/macos-bundle-fixture.mjs"; -import { verifyDylibResolution } from "./lib/macos-dylib-e2e.mjs"; +} from "../../fixtures/apple/macos-bundle.mjs"; +import { verifyDylibResolution } from "./macos-dylib-e2e.mjs"; const exec = promisify(execFile); diff --git a/scripts/lib/macos-dylib-e2e.mjs b/scripts/verify/apple/macos-dylib-e2e.mjs similarity index 99% rename from scripts/lib/macos-dylib-e2e.mjs rename to scripts/verify/apple/macos-dylib-e2e.mjs index 828da9953..eacd06e9f 100644 --- a/scripts/lib/macos-dylib-e2e.mjs +++ b/scripts/verify/apple/macos-dylib-e2e.mjs @@ -8,7 +8,7 @@ import { artifactCliEvidence, artifactMcpResult, withArtifactMcp, -} from "./artifact-e2e.mjs"; +} from "../../lib/artifact-e2e.mjs"; const exec = promisify(execFile); diff --git a/src/application/NativeUiActionTrace.ts b/src/application/NativeUiActionTrace.ts index f94051a12..25e9b9dc3 100644 --- a/src/application/NativeUiActionTrace.ts +++ b/src/application/NativeUiActionTrace.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import type { AnalysisOperationPort } from "./AnalysisProvider.js"; import type { EnhancedResult } from "./EnhancedToolTypes.js"; import type { enhancedInputSchemas } from "../contracts/enhancedInputs.js"; -import { interfaceBuilderAnalysisSchema } from "../domain/interfaceBuilderGraph.js"; +import { interfaceBuilderAnalysisSchema } from "../domain/apple/interfaceBuilderGraph.js"; import { nativeInvestigationGraphSchema, nativeInvestigationTraceSchema, diff --git a/src/application/AppleApplicationService.ts b/src/application/apple/AppleApplicationService.ts similarity index 66% rename from src/application/AppleApplicationService.ts rename to src/application/apple/AppleApplicationService.ts index 75c24540c..a30cb654c 100644 --- a/src/application/AppleApplicationService.ts +++ b/src/application/apple/AppleApplicationService.ts @@ -1,12 +1,12 @@ -import type { AnalysisError } from "../domain/analysisErrorBase.js"; -import type { Evidence } from "../domain/evidence.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import type { Evidence } from "../../domain/evidence.js"; import { appleApplicationProjectionInputSchema, projectAppleApplication, -} from "../domain/appleApplication.js"; -import type { Result } from "../domain/result.js"; -import { APPLE_APPLICATION_PROVIDER } from "./InvestigationProviders.js"; -import { projectInventoryEvidence } from "./InventoryProjectionEvidence.js"; +} from "../../domain/apple/appleApplication.js"; +import type { Result } from "../../domain/result.js"; +import { APPLE_APPLICATION_PROVIDER } from "../InvestigationProviders.js"; +import { projectInventoryEvidence } from "../InventoryProjectionEvidence.js"; const OPERATION = "project_apple_application_graph" as const; diff --git a/src/artifacts/ArtifactProvider.ts b/src/artifacts/ArtifactProvider.ts index 2041a89e9..b180dd34b 100644 --- a/src/artifacts/ArtifactProvider.ts +++ b/src/artifacts/ArtifactProvider.ts @@ -7,13 +7,13 @@ import { type ProviderIdentity, type ExecutionOptions, } from "../application/AnalysisProvider.js"; -import { inspectBundleKeyedArchive } from "./KeyedArchiveReader.js"; -import { traceDylibResolution } from "./DylibResolutionReader.js"; +import { inspectBundleKeyedArchive } from "./apple/KeyedArchiveReader.js"; +import { traceDylibResolution } from "./apple/DylibResolutionReader.js"; import { basename, dirname } from "node:path"; import { inventoryArtifact } from "../application/ArtifactInventory.js"; import { extractArtifact } from "../application/ArtifactExtraction.js"; -import { analyzeInterfaceBuilderBundle } from "../application/InterfaceBuilderAnalysis.js"; -import { analyzeAppleAssetCatalogs } from "../application/AppleAssetCatalogAnalysis.js"; +import { analyzeInterfaceBuilderBundle } from "./apple/InterfaceBuilderAnalysis.js"; +import { analyzeAppleAssetCatalogs } from "./apple/AppleAssetCatalogAnalysis.js"; import { ARTIFACT_ANALYSIS_OPERATIONS, artifactInventoryInputSchema, @@ -25,7 +25,7 @@ import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore. import { ArtifactOperationError } from "../domain/artifactOperationError.js"; import { type AnalysisError } from "../domain/analysisErrorBase.js"; import type { JsonValue } from "../domain/jsonValue.js"; -import { interfaceBuilderLimitsSchema } from "../domain/interfaceBuilderGraph.js"; +import { interfaceBuilderLimitsSchema } from "../domain/apple/interfaceBuilderGraph.js"; import { err, ok } from "../domain/result.js"; import { ArtifactReaderFailure } from "./ArtifactReader.js"; import { diff --git a/src/application/AppleAssetCatalogAnalysis.test.ts b/src/artifacts/apple/AppleAssetCatalogAnalysis.test.ts similarity index 100% rename from src/application/AppleAssetCatalogAnalysis.test.ts rename to src/artifacts/apple/AppleAssetCatalogAnalysis.test.ts diff --git a/src/application/AppleAssetCatalogAnalysis.ts b/src/artifacts/apple/AppleAssetCatalogAnalysis.ts similarity index 95% rename from src/application/AppleAssetCatalogAnalysis.ts rename to src/artifacts/apple/AppleAssetCatalogAnalysis.ts index 8ba8b04c6..36b88eb13 100644 --- a/src/application/AppleAssetCatalogAnalysis.ts +++ b/src/artifacts/apple/AppleAssetCatalogAnalysis.ts @@ -1,14 +1,14 @@ import { createHash } from "node:crypto"; import { lstat } from "node:fs/promises"; -import { DirectoryArtifactReader } from "../artifacts/DirectoryArtifactReader.js"; -import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; +import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js"; +import { ArtifactReaderFailure } from "../ArtifactReader.js"; import { analyzeInterfaceBuilderBundle } from "./InterfaceBuilderAnalysis.js"; import { appleAssetCatalogInputSchema, parseAppleAssetCatalogRecords, projectAppleAssetCatalogPage, -} from "../domain/appleAssetCatalog.js"; -import { execFileOutput } from "../process/ExecFileOutput.js"; +} from "../../domain/apple/appleAssetCatalog.js"; +import { execFileOutput } from "../../process/ExecFileOutput.js"; const ASSETUTIL = "/usr/bin/assetutil"; const MAX_CATALOG_BYTES = 64 * 1024 * 1024; @@ -105,7 +105,7 @@ export const analyzeAppleAssetCatalogs = async (input: { const inspectAssetCatalogEntry = async (input: { readonly reader: DirectoryArtifactReader; - readonly entry: import("../artifacts/ArtifactReader.js").ArtifactEntry; + readonly entry: import("../ArtifactReader.js").ArtifactEntry; readonly runAssetUtil: ( path: string, signal?: AbortSignal, @@ -237,7 +237,7 @@ const runAssetUtil = async ( const digestBounded = async ( reader: DirectoryArtifactReader, - entry: import("../artifacts/ArtifactReader.js").ArtifactEntry, + entry: import("../ArtifactReader.js").ArtifactEntry, signal?: AbortSignal, ): Promise => { if (entry.declaredSize === null || entry.declaredSize > MAX_CATALOG_BYTES) diff --git a/src/artifacts/DylibResolutionReader.ts b/src/artifacts/apple/DylibResolutionReader.ts similarity index 97% rename from src/artifacts/DylibResolutionReader.ts rename to src/artifacts/apple/DylibResolutionReader.ts index 696147e02..b18c369b0 100644 --- a/src/artifacts/DylibResolutionReader.ts +++ b/src/artifacts/apple/DylibResolutionReader.ts @@ -3,7 +3,7 @@ import { constants } from "node:fs"; import { lstat, open, readlink, realpath } from "node:fs/promises"; import { join, relative, sep } from "node:path"; -import { resolveTreePath } from "../domain/dyldPaths.js"; +import { resolveTreePath } from "../../domain/apple/dyldPaths.js"; import { dylibResolutionInputSchema, dylibResolutionResultSchema, @@ -12,9 +12,9 @@ import { type DylibTreeEntry, type DylibTreeView, type MachoImageFacts, -} from "../domain/dylibResolution.js"; -import { ArtifactReaderFailure } from "./ArtifactReader.js"; -import { DirectoryArtifactReader } from "./DirectoryArtifactReader.js"; +} from "../../domain/apple/dylibResolution.js"; +import { ArtifactReaderFailure } from "../ArtifactReader.js"; +import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js"; import { hasMachoMagic, readMachoImage } from "./MachoLoadCommandReader.js"; const HASH_CHUNK_BYTES = 1024 * 1024; diff --git a/src/application/InterfaceBuilderAnalysis.ts b/src/artifacts/apple/InterfaceBuilderAnalysis.ts similarity index 96% rename from src/application/InterfaceBuilderAnalysis.ts rename to src/artifacts/apple/InterfaceBuilderAnalysis.ts index 3dc03463f..6966bbe9c 100644 --- a/src/application/InterfaceBuilderAnalysis.ts +++ b/src/artifacts/apple/InterfaceBuilderAnalysis.ts @@ -1,28 +1,25 @@ import { createHash } from "node:crypto"; import { parseBinary } from "plist"; -import { DirectoryArtifactReader } from "../artifacts/DirectoryArtifactReader.js"; -import { projectPlistValue } from "../domain/plistValue.js"; +import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js"; +import { projectPlistValue } from "../../domain/apple/plistValue.js"; import { omittedPrototypeKeysLimitation, parseXmlPropertyList, -} from "../domain/propertyListKeys.js"; -import { - decodeNibArchive, - type NibArchiveDocument, -} from "../artifacts/NibArchive.js"; -import type { ArtifactEntry } from "../artifacts/ArtifactReader.js"; +} from "../../domain/propertyListKeys.js"; +import { decodeNibArchive, type NibArchiveDocument } from "./NibArchive.js"; +import type { ArtifactEntry } from "../ArtifactReader.js"; import { buildInterfaceBuilderAnalysis, interfaceBuilderLimitsSchema, type InterfaceBuilderDocumentInput, -} from "../domain/interfaceBuilderGraph.js"; +} from "../../domain/apple/interfaceBuilderGraph.js"; import { mergeNibHierarchies, projectNibViewHierarchy, type NibHierarchyNode, } from "./NibViewHierarchy.js"; -import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js"; +import { jsonValueSchema, type JsonValue } from "../../domain/jsonValue.js"; const MAX_DOCUMENT_BYTES = 64 * 1024 * 1024; diff --git a/src/artifacts/KeyedArchiveReader.test.ts b/src/artifacts/apple/KeyedArchiveReader.test.ts similarity index 100% rename from src/artifacts/KeyedArchiveReader.test.ts rename to src/artifacts/apple/KeyedArchiveReader.test.ts diff --git a/src/artifacts/KeyedArchiveReader.ts b/src/artifacts/apple/KeyedArchiveReader.ts similarity index 92% rename from src/artifacts/KeyedArchiveReader.ts rename to src/artifacts/apple/KeyedArchiveReader.ts index b96253d5d..e94861997 100644 --- a/src/artifacts/KeyedArchiveReader.ts +++ b/src/artifacts/apple/KeyedArchiveReader.ts @@ -2,19 +2,19 @@ import { createHash } from "node:crypto"; import { TextDecoder } from "node:util"; import { parseBinary } from "plist"; import { z } from "zod"; -import type { JsonValue } from "../domain/jsonValue.js"; -import { projectPlistValue } from "../domain/plistValue.js"; +import type { JsonValue } from "../../domain/jsonValue.js"; +import { projectPlistValue } from "../../domain/apple/plistValue.js"; import { omittedPrototypeKeysLimitation, parseXmlPropertyList, -} from "../domain/propertyListKeys.js"; +} from "../../domain/propertyListKeys.js"; import { keyedArchiveInputSchema, keyedArchiveResultSchema, projectKeyedArchive, -} from "../domain/keyedArchive.js"; -import { DirectoryArtifactReader } from "./DirectoryArtifactReader.js"; -import { ArtifactReaderFailure } from "./ArtifactReader.js"; +} from "../../domain/apple/keyedArchive.js"; +import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js"; +import { ArtifactReaderFailure } from "../ArtifactReader.js"; const MAX_BYTES = 64 * 1024 * 1024; diff --git a/src/artifacts/MachoImage.fixture.ts b/src/artifacts/apple/MachoImage.fixture.ts similarity index 100% rename from src/artifacts/MachoImage.fixture.ts rename to src/artifacts/apple/MachoImage.fixture.ts diff --git a/src/artifacts/MachoLoadCommandReader.test.ts b/src/artifacts/apple/MachoLoadCommandReader.test.ts similarity index 100% rename from src/artifacts/MachoLoadCommandReader.test.ts rename to src/artifacts/apple/MachoLoadCommandReader.test.ts diff --git a/src/artifacts/MachoLoadCommandReader.ts b/src/artifacts/apple/MachoLoadCommandReader.ts similarity index 99% rename from src/artifacts/MachoLoadCommandReader.ts rename to src/artifacts/apple/MachoLoadCommandReader.ts index 63801fb41..1eb4fccb7 100644 --- a/src/artifacts/MachoLoadCommandReader.ts +++ b/src/artifacts/apple/MachoLoadCommandReader.ts @@ -2,7 +2,7 @@ import type { MachoDependency, MachoImageFacts, MachoSlice, -} from "../domain/dylibResolution.js"; +} from "../../domain/apple/dylibResolution.js"; /** Read `length` bytes at `offset`; shorter results mean end of file. */ export type ReadAt = (offset: number, length: number) => Promise; diff --git a/src/artifacts/NibArchive.fixture.ts b/src/artifacts/apple/NibArchive.fixture.ts similarity index 100% rename from src/artifacts/NibArchive.fixture.ts rename to src/artifacts/apple/NibArchive.fixture.ts diff --git a/src/artifacts/NibArchive.test.ts b/src/artifacts/apple/NibArchive.test.ts similarity index 100% rename from src/artifacts/NibArchive.test.ts rename to src/artifacts/apple/NibArchive.test.ts diff --git a/src/artifacts/NibArchive.ts b/src/artifacts/apple/NibArchive.ts similarity index 99% rename from src/artifacts/NibArchive.ts rename to src/artifacts/apple/NibArchive.ts index 23d991bed..c383922d1 100644 --- a/src/artifacts/NibArchive.ts +++ b/src/artifacts/apple/NibArchive.ts @@ -1,4 +1,4 @@ -import type { JsonValue } from "../domain/jsonValue.js"; +import type { JsonValue } from "../../domain/jsonValue.js"; import { TextDecoder } from "node:util"; const MAGIC = Buffer.from("NIBArchive", "ascii"); diff --git a/src/application/NibViewHierarchy.test.ts b/src/artifacts/apple/NibViewHierarchy.test.ts similarity index 100% rename from src/application/NibViewHierarchy.test.ts rename to src/artifacts/apple/NibViewHierarchy.test.ts diff --git a/src/application/NibViewHierarchy.ts b/src/artifacts/apple/NibViewHierarchy.ts similarity index 97% rename from src/application/NibViewHierarchy.ts rename to src/artifacts/apple/NibViewHierarchy.ts index 231f456b6..61c669e53 100644 --- a/src/application/NibViewHierarchy.ts +++ b/src/artifacts/apple/NibViewHierarchy.ts @@ -1,5 +1,5 @@ -import type { NibArchiveDocument } from "../artifacts/NibArchive.js"; -import type { JsonValue } from "../domain/jsonValue.js"; +import type { NibArchiveDocument } from "./NibArchive.js"; +import type { JsonValue } from "../../domain/jsonValue.js"; export type NibHierarchyNode = { objectID: string; diff --git a/src/cli/applicationCommands.ts b/src/cli/applicationCommands.ts index 967ecbf6c..f4a6fb33b 100644 --- a/src/cli/applicationCommands.ts +++ b/src/cli/applicationCommands.ts @@ -2,7 +2,7 @@ import { reconstructionCoverageEvaluationInputSchema } from "../domain/reconstru import { Cli, z } from "incur"; import { projectAndroidApplicationEvidence } from "../application/android/AndroidApplicationService.js"; -import { projectAppleApplicationEvidence } from "../application/AppleApplicationService.js"; +import { projectAppleApplicationEvidence } from "../application/apple/AppleApplicationService.js"; import { compareApplicationVersionsEvidenceValidated, compareJavaScriptExportShapesEvidenceValidated, @@ -19,7 +19,7 @@ import { AnalysisInputError } from "../domain/analysisErrorCore.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { type AnalysisError } from "../domain/analysisErrorBase.js"; import { androidApplicationProjectionInputSchema } from "../domain/android/androidApplication.js"; -import { appleApplicationProjectionInputSchema } from "../domain/appleApplication.js"; +import { appleApplicationProjectionInputSchema } from "../domain/apple/appleApplication.js"; import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js"; import type { Logger } from "../logger.js"; import { traceApplicationFeatureInputSchema } from "../domain/javascript/javascriptFeatureTraceSchemas.js"; diff --git a/src/contracts/applicationToolContracts.ts b/src/contracts/applicationToolContracts.ts index 763e6632a..6ef806c36 100644 --- a/src/contracts/applicationToolContracts.ts +++ b/src/contracts/applicationToolContracts.ts @@ -23,7 +23,7 @@ import { import { appleApplicationProjectionInputSchema, appleApplicationProjectionResultSchema, -} from "../domain/appleApplication.js"; +} from "../domain/apple/appleApplication.js"; import type { ToolContract } from "./toolContracts.js"; import { toolContractMetadata } from "./toolEffects.js"; import { evidenceResultOf } from "./toolOutputSchemas.js"; diff --git a/src/contracts/artifactToolContracts.ts b/src/contracts/artifactToolContracts.ts index ade9cf25b..b52f115af 100644 --- a/src/contracts/artifactToolContracts.ts +++ b/src/contracts/artifactToolContracts.ts @@ -5,9 +5,9 @@ import { artifactOutputSchemas } from "./toolOutputSchemas.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; import { toolContractMetadata } from "./toolEffects.js"; import { requireOutputSchema } from "./toolOutputSchemaPrimitives.js"; -import { appleAssetCatalogInputSchema } from "../domain/appleAssetCatalog.js"; -import { dylibResolutionInputSchema } from "../domain/dylibResolution.js"; -import { keyedArchiveInputSchema } from "../domain/keyedArchive.js"; +import { appleAssetCatalogInputSchema } from "../domain/apple/appleAssetCatalog.js"; +import { dylibResolutionInputSchema } from "../domain/apple/dylibResolution.js"; +import { keyedArchiveInputSchema } from "../domain/apple/keyedArchive.js"; /** Exact caller boundary for deterministic artifact inventory. */ export const artifactInventoryInputSchema = z.strictObject({ integrity_policy: z.enum(["fail", "record-and-continue"]).default("fail"), diff --git a/src/contracts/toolOutputSchemaGroups.ts b/src/contracts/toolOutputSchemaGroups.ts index 40aedf263..55225ffee 100644 --- a/src/contracts/toolOutputSchemaGroups.ts +++ b/src/contracts/toolOutputSchemaGroups.ts @@ -30,10 +30,10 @@ import { } from "../domain/nativeInspection.js"; import { artifactExtractionResultSchema } from "../domain/artifactGraph.js"; import { artifactInspectionResultSchema } from "../domain/artifactInspection.js"; -import { interfaceBuilderAnalysisSchema } from "../domain/interfaceBuilderGraph.js"; -import { dylibResolutionResultSchema } from "../domain/dylibResolution.js"; -import { keyedArchiveResultSchema } from "../domain/keyedArchive.js"; -import { appleAssetCatalogResultSchema } from "../domain/appleAssetCatalog.js"; +import { interfaceBuilderAnalysisSchema } from "../domain/apple/interfaceBuilderGraph.js"; +import { dylibResolutionResultSchema } from "../domain/apple/dylibResolution.js"; +import { keyedArchiveResultSchema } from "../domain/apple/keyedArchive.js"; +import { appleAssetCatalogResultSchema } from "../domain/apple/appleAssetCatalog.js"; import { managedArtifactInspectionSchema, managedMemberInspectionSchema, diff --git a/src/domain/appleApplication.test.ts b/src/domain/apple/appleApplication.test.ts similarity index 98% rename from src/domain/appleApplication.test.ts rename to src/domain/apple/appleApplication.test.ts index 47c45a746..ea5419dc9 100644 --- a/src/domain/appleApplication.test.ts +++ b/src/domain/apple/appleApplication.test.ts @@ -3,9 +3,9 @@ import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; import { projectAppleApplication } from "./appleApplication.js"; -import { canonicalDigest } from "./comparisonSemantics.js"; -import { createEvidence, type Evidence } from "./evidence.js"; -import { jsonValueSchema } from "./jsonValue.js"; +import { canonicalDigest } from "../comparisonSemantics.js"; +import { createEvidence, type Evidence } from "../evidence.js"; +import { jsonValueSchema } from "../jsonValue.js"; type EntryKind = "file" | "directory" | "symlink"; type RootFormat = "ipa" | "zip" | "dmg" | "directory" | "asar"; diff --git a/src/domain/appleApplication.ts b/src/domain/apple/appleApplication.ts similarity index 98% rename from src/domain/appleApplication.ts rename to src/domain/apple/appleApplication.ts index 42bc970b6..bb88a8c31 100644 --- a/src/domain/appleApplication.ts +++ b/src/domain/apple/appleApplication.ts @@ -14,10 +14,10 @@ import { platformsOf, type AppleInventoryEntry, } from "./appleBundleAnatomy.js"; -import { parseArtifactInventoryEvidence } from "./artifactInventoryEvidence.js"; -import { evidenceSchema } from "./evidence.js"; -import { digestSchema } from "./../domain/digests.js"; -import { prefixedDigestSchema } from "./../domain/digests.js"; +import { parseArtifactInventoryEvidence } from "../artifactInventoryEvidence.js"; +import { evidenceSchema } from "../evidence.js"; +import { digestSchema } from "../digests.js"; +import { prefixedDigestSchema } from "../digests.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); const pathSchema = z.string().min(1); diff --git a/src/domain/appleAssetCatalog.test.ts b/src/domain/apple/appleAssetCatalog.test.ts similarity index 100% rename from src/domain/appleAssetCatalog.test.ts rename to src/domain/apple/appleAssetCatalog.test.ts diff --git a/src/domain/appleAssetCatalog.ts b/src/domain/apple/appleAssetCatalog.ts similarity index 98% rename from src/domain/appleAssetCatalog.ts rename to src/domain/apple/appleAssetCatalog.ts index 12439fce0..2954ebda8 100644 --- a/src/domain/appleAssetCatalog.ts +++ b/src/domain/apple/appleAssetCatalog.ts @@ -1,7 +1,7 @@ import { z } from "zod"; -import { jsonValueSchema } from "./jsonValue.js"; -import { AnalysisOutputError } from "./analysisErrorCore.js"; +import { jsonValueSchema } from "../jsonValue.js"; +import { AnalysisOutputError } from "../analysisErrorCore.js"; /** Caller-controlled page bounds for compiled Apple asset metadata. */ export const appleAssetCatalogInputSchema = z.strictObject({ diff --git a/src/domain/appleBundleAnatomy.ts b/src/domain/apple/appleBundleAnatomy.ts similarity index 99% rename from src/domain/appleBundleAnatomy.ts rename to src/domain/apple/appleBundleAnatomy.ts index f98def7c6..224c5ff49 100644 --- a/src/domain/appleBundleAnatomy.ts +++ b/src/domain/apple/appleBundleAnatomy.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { digestSchema, prefixedDigestSchema } from "./digests.js"; +import { digestSchema, prefixedDigestSchema } from "../digests.js"; const pathSchema = z.string().min(1); diff --git a/src/domain/dyldPaths.ts b/src/domain/apple/dyldPaths.ts similarity index 100% rename from src/domain/dyldPaths.ts rename to src/domain/apple/dyldPaths.ts diff --git a/src/domain/dylibResolution.test.ts b/src/domain/apple/dylibResolution.test.ts similarity index 100% rename from src/domain/dylibResolution.test.ts rename to src/domain/apple/dylibResolution.test.ts diff --git a/src/domain/dylibResolution.ts b/src/domain/apple/dylibResolution.ts similarity index 99% rename from src/domain/dylibResolution.ts rename to src/domain/apple/dylibResolution.ts index 2d1235fd1..241a50be8 100644 --- a/src/domain/dylibResolution.ts +++ b/src/domain/apple/dylibResolution.ts @@ -8,7 +8,7 @@ import { resolveTreePath, type Expansion, } from "./dyldPaths.js"; -import { digestSchema } from "./digests.js"; +import { digestSchema } from "../digests.js"; import { DYLIB_RESOLUTION_LIMITATIONS, deriveFindings, diff --git a/src/domain/dylibResolutionFindings.ts b/src/domain/apple/dylibResolutionFindings.ts similarity index 100% rename from src/domain/dylibResolutionFindings.ts rename to src/domain/apple/dylibResolutionFindings.ts diff --git a/src/domain/interfaceBuilderGraph.test.ts b/src/domain/apple/interfaceBuilderGraph.test.ts similarity index 100% rename from src/domain/interfaceBuilderGraph.test.ts rename to src/domain/apple/interfaceBuilderGraph.test.ts diff --git a/src/domain/interfaceBuilderGraph.ts b/src/domain/apple/interfaceBuilderGraph.ts similarity index 99% rename from src/domain/interfaceBuilderGraph.ts rename to src/domain/apple/interfaceBuilderGraph.ts index 2e882ab26..22d8a544b 100644 --- a/src/domain/interfaceBuilderGraph.ts +++ b/src/domain/apple/interfaceBuilderGraph.ts @@ -10,7 +10,7 @@ import { nativeInvestigationGraphSchema, nativeInvestigationNodeSchema, nativeInvestigationTraceLimitsSchema, -} from "./nativeInvestigationGraph.js"; +} from "../nativeInvestigationGraph.js"; export { parseInterfaceBuilderRecords } from "./interfaceBuilderKeyedArchive.js"; diff --git a/src/domain/interfaceBuilderKeyedArchive.ts b/src/domain/apple/interfaceBuilderKeyedArchive.ts similarity index 99% rename from src/domain/interfaceBuilderKeyedArchive.ts rename to src/domain/apple/interfaceBuilderKeyedArchive.ts index 602392c22..d1ee5c3ea 100644 --- a/src/domain/interfaceBuilderKeyedArchive.ts +++ b/src/domain/apple/interfaceBuilderKeyedArchive.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { jsonValueSchema } from "./jsonValue.js"; +import { jsonValueSchema } from "../jsonValue.js"; const recordSchema = z.record(z.string(), z.unknown()); diff --git a/src/domain/keyedArchive.ts b/src/domain/apple/keyedArchive.ts similarity index 99% rename from src/domain/keyedArchive.ts rename to src/domain/apple/keyedArchive.ts index 32e855f18..2fc6a792f 100644 --- a/src/domain/keyedArchive.ts +++ b/src/domain/apple/keyedArchive.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { jsonValueSchema, type JsonValue } from "./jsonValue.js"; +import { jsonValueSchema, type JsonValue } from "../jsonValue.js"; /** Select one archive within the active bundle and optionally one named root. */ export const keyedArchiveInputSchema = z.strictObject({ diff --git a/src/domain/plistValue.test.ts b/src/domain/apple/plistValue.test.ts similarity index 100% rename from src/domain/plistValue.test.ts rename to src/domain/apple/plistValue.test.ts diff --git a/src/domain/plistValue.ts b/src/domain/apple/plistValue.ts similarity index 97% rename from src/domain/plistValue.ts rename to src/domain/apple/plistValue.ts index 6c427a8ad..dc6e4d0ea 100644 --- a/src/domain/plistValue.ts +++ b/src/domain/apple/plistValue.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import type { JsonValue } from "./jsonValue.js"; +import type { JsonValue } from "../jsonValue.js"; const MAX_PLIST_DEPTH = 128; diff --git a/src/domain/nativeInvestigationGraph.test.ts b/src/domain/nativeInvestigationGraph.test.ts index f11aef020..160d29660 100644 --- a/src/domain/nativeInvestigationGraph.test.ts +++ b/src/domain/nativeInvestigationGraph.test.ts @@ -8,7 +8,7 @@ import { import { buildInterfaceBuilderAnalysis, interfaceBuilderLimitsSchema, -} from "./interfaceBuilderGraph.js"; +} from "./apple/interfaceBuilderGraph.js"; const evidence = [ { diff --git a/src/native/parsers/plist.ts b/src/native/parsers/plist.ts index 977e7909a..b27f67b1e 100644 --- a/src/native/parsers/plist.ts +++ b/src/native/parsers/plist.ts @@ -5,7 +5,7 @@ import type { JsonValue } from "../../domain/jsonValue.js"; import { projectPlistValue, type ProjectedPlistValue, -} from "../../domain/plistValue.js"; +} from "../../domain/apple/plistValue.js"; import { omitPrototypeKeys, omittedPrototypeKeysLimitation, diff --git a/src/server/registerApplicationTools/projectMobileApplicationGraph.ts b/src/server/registerApplicationTools/projectMobileApplicationGraph.ts index 573eee331..cac99a638 100644 --- a/src/server/registerApplicationTools/projectMobileApplicationGraph.ts +++ b/src/server/registerApplicationTools/projectMobileApplicationGraph.ts @@ -1,7 +1,7 @@ import type { McpServer } from "@modelcontextprotocol/server"; import { projectAndroidApplicationEvidence } from "../../application/android/AndroidApplicationService.js"; -import { projectAppleApplicationEvidence } from "../../application/AppleApplicationService.js"; +import { projectAppleApplicationEvidence } from "../../application/apple/AppleApplicationService.js"; import { APPLICATION_TOOL_CONTRACTS, applicationToolContract, diff --git a/tests/boundary/cli/dylibResolutionCli.test.ts b/tests/boundary/cli/dylibResolutionCli.test.ts index db1646023..1911d8551 100644 --- a/tests/boundary/cli/dylibResolutionCli.test.ts +++ b/tests/boundary/cli/dylibResolutionCli.test.ts @@ -9,8 +9,8 @@ import { dylibCommand, machoImage, rpathCommand, -} from "../../../src/artifacts/MachoImage.fixture.js"; -import { dylibResolutionResultSchema } from "../../../src/domain/dylibResolution.js"; +} from "../../../src/artifacts/apple/MachoImage.fixture.js"; +import { dylibResolutionResultSchema } from "../../../src/domain/apple/dylibResolution.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; diff --git a/tests/boundary/filesystem/interfaceBuilderBundle.test.ts b/tests/boundary/filesystem/interfaceBuilderBundle.test.ts index df2173003..a66984272 100644 --- a/tests/boundary/filesystem/interfaceBuilderBundle.test.ts +++ b/tests/boundary/filesystem/interfaceBuilderBundle.test.ts @@ -7,8 +7,8 @@ import { promisify } from "node:util"; import { buildBinary } from "plist"; import { describe, expect, it } from "vitest"; -import { analyzeInterfaceBuilderBundle } from "../../../src/application/InterfaceBuilderAnalysis.js"; -import { encodeNibArchiveFixture } from "../../../src/artifacts/NibArchive.fixture.js"; +import { analyzeInterfaceBuilderBundle } from "../../../src/artifacts/apple/InterfaceBuilderAnalysis.js"; +import { encodeNibArchiveFixture } from "../../../src/artifacts/apple/NibArchive.fixture.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const compile = promisify(execFile); diff --git a/tests/boundary/filesystem/nativeArtifactGoldens.test.ts b/tests/boundary/filesystem/nativeArtifactGoldens.test.ts index 30ea71470..7387b2aeb 100644 --- a/tests/boundary/filesystem/nativeArtifactGoldens.test.ts +++ b/tests/boundary/filesystem/nativeArtifactGoldens.test.ts @@ -1,10 +1,10 @@ import { readFile } from "node:fs/promises"; import { describe, expect, it } from "vitest"; -import { decodeKeyedArchiveBytes } from "../../../src/artifacts/KeyedArchiveReader.js"; +import { decodeKeyedArchiveBytes } from "../../../src/artifacts/apple/KeyedArchiveReader.js"; import { parseAppleAssetCatalogRecords, projectAppleAssetCatalogPage, -} from "../../../src/domain/appleAssetCatalog.js"; +} from "../../../src/domain/apple/appleAssetCatalog.js"; const golden = (path: string) => new URL(`../../fixtures/golden/${path}`, import.meta.url); diff --git a/tests/boundary/filesystem/nibNativeHierarchy.test.ts b/tests/boundary/filesystem/nibNativeHierarchy.test.ts index 83b025b79..360b7e190 100644 --- a/tests/boundary/filesystem/nibNativeHierarchy.test.ts +++ b/tests/boundary/filesystem/nibNativeHierarchy.test.ts @@ -6,8 +6,8 @@ import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import { decodeNibArchive } from "../../../src/artifacts/NibArchive.js"; -import { analyzeInterfaceBuilderBundle } from "../../../src/application/InterfaceBuilderAnalysis.js"; +import { decodeNibArchive } from "../../../src/artifacts/apple/NibArchive.js"; +import { analyzeInterfaceBuilderBundle } from "../../../src/artifacts/apple/InterfaceBuilderAnalysis.js"; const compile = promisify(execFile); diff --git a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts index b879b7bd9..b099a3825 100644 --- a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts +++ b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts @@ -7,7 +7,7 @@ import { expect, it } from "vitest"; import { z } from "zod"; import { parseConfig } from "../../../src/config.js"; import { createBinarySession } from "../../../src/application/runtime.js"; -import { keyedArchiveResultSchema } from "../../../src/domain/keyedArchive.js"; +import { keyedArchiveResultSchema } from "../../../src/domain/apple/keyedArchive.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; diff --git a/tests/boundary/mcp/dylibResolutionMcp.test.ts b/tests/boundary/mcp/dylibResolutionMcp.test.ts index b0639047e..2a2c18f06 100644 --- a/tests/boundary/mcp/dylibResolutionMcp.test.ts +++ b/tests/boundary/mcp/dylibResolutionMcp.test.ts @@ -12,8 +12,8 @@ import { dylibCommand, machoImage, rpathCommand, -} from "../../../src/artifacts/MachoImage.fixture.js"; -import { dylibResolutionResultSchema } from "../../../src/domain/dylibResolution.js"; +} from "../../../src/artifacts/apple/MachoImage.fixture.js"; +import { dylibResolutionResultSchema } from "../../../src/domain/apple/dylibResolution.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/providers/native/keyedArchiveEncoding.test.ts b/tests/boundary/providers/native/keyedArchiveEncoding.test.ts index 31db384c1..c32a6e037 100644 --- a/tests/boundary/providers/native/keyedArchiveEncoding.test.ts +++ b/tests/boundary/providers/native/keyedArchiveEncoding.test.ts @@ -5,7 +5,7 @@ import { join } from "node:path"; import { buildBinary, parse } from "plist"; import { expect, it } from "vitest"; -import { inspectBundleKeyedArchive } from "../../../../src/artifacts/KeyedArchiveReader.js"; +import { inspectBundleKeyedArchive } from "../../../../src/artifacts/apple/KeyedArchiveReader.js"; import { nativeFixture } from "../../../fixtures/nativeCommands.js"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; diff --git a/tests/composition/analysis-sessions/appleApplication.test.ts b/tests/composition/analysis-sessions/appleApplication.test.ts index 3f38ec9ae..8f870ad47 100644 --- a/tests/composition/analysis-sessions/appleApplication.test.ts +++ b/tests/composition/analysis-sessions/appleApplication.test.ts @@ -16,9 +16,9 @@ import { requireSuccessfulProjection, } from "../../support/applicationSessionFixture.js"; -import { projectAppleApplicationEvidence } from "../../../src/application/AppleApplicationService.js"; +import { projectAppleApplicationEvidence } from "../../../src/application/apple/AppleApplicationService.js"; import { runProviderAnalysis } from "../../../src/application/DirectAnalysis.js"; -import { appleApplicationProjectionResultSchema } from "../../../src/domain/appleApplication.js"; +import { appleApplicationProjectionResultSchema } from "../../../src/domain/apple/appleApplication.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; // Real ZIP entries cross the former component and bridge-candidate limits. diff --git a/tests/evaluation/moduleBoundaries.test.ts b/tests/evaluation/moduleBoundaries.test.ts index b9b1146d8..4b808f331 100644 --- a/tests/evaluation/moduleBoundaries.test.ts +++ b/tests/evaluation/moduleBoundaries.test.ts @@ -272,3 +272,22 @@ describe("test lane import boundaries", () => { if (!allowed) expect(result.stdout).toContain("no-restricted-imports"); }); }); + +describe("Apple artifact producer ownership", () => { + it.each([ + ["../../application/Workflow.js", false], + ["../../composition/android.js", false], + ["../../cli.js", false], + ["../../domain/apple/plistValue.js", true], + ["../DirectoryArtifactReader.js", true], + ])("checks the producer dependency %s", (dependency, allowed) => { + const violations = inspectModuleBoundaries( + "src/artifacts/apple/probe.ts", + `import type { Value } from ${JSON.stringify(dependency)};`, + process.cwd(), + ); + + expect(violations.length === 0).toBe(allowed); + if (!allowed) expect(violations[0]?.boundary).toBe("artifact-acquisition"); + }); +}); diff --git a/turbo.json b/turbo.json index 086d4db29..62e4f0ade 100644 --- a/turbo.json +++ b/turbo.json @@ -111,6 +111,8 @@ "scripts/lib/*.mjs", "scripts/verify/managed/*.mjs", "scripts/fixtures/managed/*.mjs", + "scripts/verify/apple/*.mjs", + "scripts/fixtures/apple/*.mjs", "src/generated*.ts", "src/contracts/**/*.ts", "docs/**", diff --git a/website/README.md b/website/README.md index 434cc2db2..300f000c1 100644 --- a/website/README.md +++ b/website/README.md @@ -1,15 +1,16 @@ # REA website -An English static website with explanatory figures, worked guides and a DX-Ball investigation. +An English static website with explanatory figures, worked guides and DX-Ball and Notion investigations. The public files are in `website/public/`. The site uses HTML, CSS and a small -script for copying code and following the assembly-to-C comparison; it has no -build step or npm dependencies. +script for copying code and following the assembly-to-C comparison. Python +packages the downloadable example; there is no frontend bundler or npm dependency. ## Local preview From the repository root: ```sh +python3 scripts/prepare-website.py python3 -m http.server 4173 --bind 127.0.0.1 --directory website/public ``` @@ -17,8 +18,10 @@ Open . Refresh the browser after editing a file. ## Pages -- `public/index.html`: product introduction and DX-Ball overview. +- `public/index.html`: product introduction and case-study overviews. +- `public/showcase/index.html`: the case-study index. - `public/showcase/dx-ball/index.html`: sound-pan investigation and project status. +- `public/showcase/notion/index.html`: Notion's Electron clipboard bridge and rich clipboard format. - `public/get-started/index.html`: agent setup, first CLI result and provider guides. - `public/guides/`: a guide hub and native, JavaScript/Electron and browser examples. - `public/examples/`: downloadable Electron source and an interactive Notes browser app. @@ -40,9 +43,18 @@ Electron fixture is for static analysis; Electron is not a prerequisite for following that example. The separate Notes browser app runs in the local preview and constructs a CSV download after fetching its JSON data. +The Electron guide offers one ZIP containing the six source files under +`notes-example/`. `scripts/prepare-website.py` generates this download from +an explicit file list, with fixed timestamps and permissions. The ZIP is ignored +by Git; website checks and each manual publication regenerate it before checking +and uploading the public directory. The verifier checks that its entries match +the current source and contain no extra files. + Agent terminals show example prompts, not transcripts of previous -investigations. A short cursor animation starts when the prompt enters view and -respects reduced-motion preferences. All prompt text is present without JavaScript. +investigations. All cursors blink continuously with the same CSS animation, +respecting reduced-motion preferences. Both the prompt and animation work +without JavaScript. The homepage and agent setup section share a copyable +installation prompt; setup still presents its plan for approval. DX-Ball figures and findings refer to the linked 7 October 2026 checkpoint, commit `a55dca27ec0a07018c1b2c95ae2be027f7d8c3c4`. Update those links and figures @@ -63,6 +75,15 @@ The worked guides use semantic HTML flows that stack vertically on smaller screens. The Electron teaching example uses CommonJS, matching its preload code; the scoped lint override admits `require` only in that example directory. +The Notion case follows the same HTML figure style. Its short excerpts explain +the packaged clipboard bridge; separate web-cache probes illustrate the rich +clipboard and Markdown formats. Only selected source details and generic +example data belong on the site. Machine paths, account identifiers, local +configuration, complete vendor bundles and raw captured results stay outside +the website. +[evidence/notion-clipboard.md](evidence/notion-clipboard.md) records the REA +package version, selected findings, source anchors and module-probe scope. + ## GitHub Pages `.github/workflows/website-pages.yml` prepares and deploys only `website/public`. @@ -75,6 +96,7 @@ and the single Pages publisher without installing npm dependencies. The same checks run before each manual deployment. You can also run them locally: ```sh +python3 scripts/prepare-website.py python3 scripts/verify-website.py ``` diff --git a/website/evidence/guide-examples.md b/website/evidence/guide-examples.md index cae290a8c..70cdc859c 100644 --- a/website/evidence/guide-examples.md +++ b/website/evidence/guide-examples.md @@ -30,6 +30,12 @@ rea analyze-javascript-application \ The source was formatted before this result was recorded. Re-run analysis when the example files change. +The guide's `examples/notes-example.zip` is generated from exactly these six +source files, under the `notes-example/` folder. Packaging changes no file +contents. Website verification compares every ZIP entry to its source file and +rejects extra entries. Existing source digests and static findings remain valid +for the unzipped example. + ## Notes browser example The three-file browser app is in `website/public/examples/notes-web/`. diff --git a/website/evidence/notion-clipboard.md b/website/evidence/notion-clipboard.md new file mode 100644 index 000000000..c7296ca1d --- /dev/null +++ b/website/evidence/notion-clipboard.md @@ -0,0 +1,133 @@ +# Notion clipboard case: sources and scope + +The case uses Notion Desktop 7.6.1's shipped JavaScript, a fresh REA inspection +of its tab preload, and separately recorded probes of cached web-editor modules. +The public page contains selected result fields, small code excerpts and generic +example data. Original packages, complete analysis results and captured local +state are not website assets. + +## Fresh REA result + +The published npm REA 4.1.0 package inspected the saved tab preload on +7 October 2026, using Node.js 24.18.0. An independent source build of public +checkpoint [`810fe53e`](https://github.com/morluto/rea/tree/810fe53e) returned the +same API and invoke findings. The page uses the published-package result. + +The successful `analyze_javascript_application` request inspected the extracted +directory `.webpack/renderer/tab_browser_view`. Its only file was `preload.js`: +24,579 bytes, one parsed JavaScript file and no parse failures or truncated +scopes. The result's relevant observations were: + +| Finding | Selected fields | Source range | +| -------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------- | +| Context bridge | `api_key: "__electronApi"`, `api_status: "dynamic"`, `members: []`, `unknown_members: 1` | `preload.js`, line 1, columns 22764–22828 | +| Renderer IPC | `operation: "invoke"`, `channel_expression: "e"`, `channel: null`, `resolution: "dynamic"` | `preload.js`, line 1, columns 6486–6522 | + +These are `ast-static-analysis` observations. Lines are one-based and columns +zero-based, in the original decoded source. The page's two result panels are a +readable projection of these fields, not a complete or replayable Evidence +object. + +The focused result locates the exposed bridge and IPC wrapper. Reading the +packaged API object supplies the `clipboard.write` member and its literal +`notion:clipboard:write` argument. Reading the main-process wrapper and receiver +connects that channel to Electron's clipboard call. The four-step figure +summarizes this source reading; it is not an automatically paired IPC graph or +a captured desktop execution. + +Additional feature traces reused the focused result: + +- API seed `__electronApi`, exact match: one seed, partial coverage, 17 nodes, + 16 edges and no terminal paths. +- Channel seed `notion:clipboard:write`, exact match: no graph match. Its + propagation through the wrappers was unresolved in that graph. + +Neither trace establishes a complete clipboard feature graph. The page therefore +uses the directly observed API/invoke source ranges and the separately inspected +wrapper code, rather than presenting those trace counts as the clipboard route. + +## Packaged bridge excerpts + +Artifact: Notion Desktop 7.6.1 Windows `app.asar`, 8,903,750 bytes. +SHA-256: `97c41bc14fc0ea8c91cd34f502660963dfdf294d5eb35165ccac9d30963069d4`. + +| Relative packaged file | Relevant source anchors | +| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `.webpack/renderer/tab_browser_view/preload.js` | Clipboard API member around character 8274; invoke helpers around 6105/6460; context bridge exposure around 22764 | +| `.webpack/main/index.js` | Handler wrapper around character 313027; clipboard receiver around 726249; sender-check adapter around 750262; underlying `isNotionWebContents` around 1281553 | + +Character anchors refer to decoded JavaScript, counted from the start of each +file. They are distinct from REA's line/column ranges above. + +Source digests: + +- Tab preload: `c209b1222cd15721bf1988f88c3e57e187d11306edb5eddb13eca441feb6e337`. +- Main bundle: `7d06e97721281403e7cf221389de22c6e40d800030a87511231b5390df1452ea`. + +The HTML excerpts expand minified variable names and omit unrelated members. +The API-member excerpt illustrates the relevant member; it does not reproduce +the entire assembled API object. The sender-check expression expands the +adapter to its underlying `isNotionWebContents` call. The meaningful operations +are preserved: bind the literal channel, invoke it, register its receiver, +check the sender and call `clipboard.write(data)`. + +## Separately cached web-editor code + +The web examples come from a 13 July 2026 cache, build +`swv2-23.13.20260713.0259`. This is a separate artifact identity from the desktop +ASAR. Corresponding API names help explain the two sides; no same-version live +copy/paste session was captured for this case. + +The clipboard examples use: + +- Module `747001`: copy-payload construction, private MIME storage and virtual + clipboard handling. +- Module `58169`: marker encoding and decoding. +- Module `736810`: desktop clipboard routing, including the + `electronApi.clipboard.write` call. + +The routing helper's asset, +`clipboard-817bda3acae9bf1e__be64437d1c0e.js`, has SHA-256 +`be64437d1c0eba9c74b48f4307c5936790c59e85aa2ce845e22016a101c2ff7f`. + +Saved Node/JSDOM probes used extracted modules, faithful lifted helpers and +stubbed environment APIs. The marker helper is extracted code; the virtual +clipboard reader includes a lifted implementation. A matching-ID specimen +recovered private formats; a stale local entry retained ordinary text and HTML. +This supports the recovered matching policy, rather than proof of OS clipboard +hydration or fidelity of a complex block in the running app. + +The figure's “Project notes” and `` are generic illustrations. +`` stands for the generated marker ID; it is not a literal runtime UUID. + +## Markdown specimen + +The table uses the saved extracted-module probe of structured Markdown parser +`422457`. Input: + +```markdown +| A | B | +| --- | --- | +| 1 | 2 | +``` + +In the structured sample record, `/samples/table/raw/blocks/0` is a table with +`headerRow: true`, two `table_row` children, and cell text `A`/`B` followed by +`1`/`2`. The corresponding page and chat filtered outputs preserve those rows +and cells. The public tree is an outline of these fields, not an unmodified +JSON object or a persisted Notion page. + +The broader historical harness included stubs and several parser variants. +This specimen comes from the intact structured-parser result, rather than a +legacy table-creation record affected by a synthetic ID collision. Historical +module probes are attributed to that harness; REA's new role in this page is +the separately verified preload analysis. + +## SQLite follow-up + +The short expandable follow-up is based on the same desktop package. Its +`getSqliteMeta` path transfers `MessageChannelMain` ports between the renderer +and a SQLite utility process. Later statement batches and results use that +port. Relevant files are the tab preload, main bundle and `SqliteServer.js`. +The page explains the source-level handoff without claiming a captured SQL +session. diff --git a/website/public/assets/main.js b/website/public/assets/main.js index 00efa23a3..40a1d9808 100644 --- a/website/public/assets/main.js +++ b/website/public/assets/main.js @@ -5,8 +5,13 @@ function initializeCopyButtons() { if (target === null) return; const status = document.getElementById("copy-status"); + const content = target.textContent.trim(); + const text = + target.closest(".terminal-prompt") === null + ? content + : content.replace(/\s+/g, " "); try { - await navigator.clipboard.writeText(target.textContent.trim()); + await navigator.clipboard.writeText(text); button.textContent = "Copied"; if (status !== null) status.textContent = "Code copied to clipboard."; } catch { @@ -55,22 +60,3 @@ function initializeStepComparisons() { } initializeStepComparisons(); - -function initializeAgentTerminals() { - if (!("IntersectionObserver" in window)) return; - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (!entry.isIntersecting) return; - entry.target.classList.add("is-visible"); - observer.unobserve(entry.target); - }); - }, - { threshold: 0.25 }, - ); - document.querySelectorAll(".agent-terminal").forEach((terminal) => { - observer.observe(terminal); - }); -} - -initializeAgentTerminals(); diff --git a/website/public/assets/styles.css b/website/public/assets/styles.css index f27f68211..66bf75dbb 100644 --- a/website/public/assets/styles.css +++ b/website/public/assets/styles.css @@ -766,9 +766,7 @@ pre code { margin-left: 0.4em; vertical-align: -0.12em; background: #9bbfe5; -} -.agent-terminal.is-visible .terminal-cursor { - animation: terminal-blink 1s step-end 6; + animation: terminal-blink 1s step-end infinite; } @keyframes terminal-blink { 50% { @@ -840,6 +838,61 @@ pre code { color: var(--ink); font-size: 0.9rem; } +.notion-bridge-flow { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} +.notion-channel { + color: var(--accent); + background: #edf2f8; + border-radius: 2px; + box-decoration-break: clone; + -webkit-box-decoration-break: clone; +} +.notion-clipboard-grid, +.notion-table-example { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} +.notion-clipboard-grid .code-block, +.notion-table-example .code-block { + min-width: 0; + margin: 0; +} +.notion-clipboard-grid pre, +.notion-table-example pre { + padding: 1rem; + font-size: 0.75rem; + white-space: pre-wrap; + overflow-wrap: anywhere; +} +.notion-mini-flow { + display: flex; + flex-wrap: wrap; + gap: 0.6rem 1.8rem; + margin: 1.3rem 0 0; + padding: 0; + list-style: none; + font-size: 0.8rem; +} +.notion-mini-flow li { + position: relative; + margin: 0; +} +.notion-mini-flow li:not(:last-child)::after { + content: "→"; + position: absolute; + right: -1.3rem; + color: #8191a2; +} +.case-preview { + margin: 0; +} +.case-preview pre { + white-space: pre-wrap; + overflow-wrap: anywhere; + font-size: 0.8rem; +} .workflow-figure figcaption { margin-top: 1rem; color: var(--muted); @@ -940,7 +993,8 @@ pre code { .rea-query-pair { grid-template-columns: 1fr; } - .flow-steps { + .flow-steps, + .notion-bridge-flow { grid-template-columns: 1fr; gap: 2rem; } @@ -1048,6 +1102,20 @@ pre code { .workflow-figure { padding: 1rem; } + .notion-clipboard-grid, + .notion-table-example { + grid-template-columns: 1fr; + } + .notion-mini-flow { + display: grid; + gap: 1.5rem; + } + .notion-mini-flow li:not(:last-child)::after { + content: "↓"; + right: auto; + left: 0.4rem; + bottom: -1.4rem; + } .step-selector { grid-template-columns: repeat(3, minmax(0, 1fr)); } @@ -1061,7 +1129,7 @@ pre code { } @media (prefers-reduced-motion: reduce) { - .agent-terminal.is-visible .terminal-cursor { + .terminal-cursor { animation: none; } } diff --git a/website/public/get-started/index.html b/website/public/get-started/index.html index 0541e21c4..15314573f 100644 --- a/website/public/get-started/index.html +++ b/website/public/get-started/index.html @@ -22,7 +22,7 @@ >Reverse Engineer Anything