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
-
-
- Terminal
+
+ Or run setup yourself
+
+
+ Terminal
+ Copy
+
+
+
npx rea-agents@latest setup
+
+
Choose your client and review the setup plan. REA adds its MCP
connection and the matching investigation skill, keeping a backup
diff --git a/website/public/guides/browser/index.html b/website/public/guides/browser/index.html
index 8a1a6cb7f..6c643bd0e 100644
--- a/website/public/guides/browser/index.html
+++ b/website/public/guides/browser/index.html
@@ -22,7 +22,7 @@
>Reverse Engineer Anything
- Save these six files together in a folder named
- notes-example. Static analysis needs Node.js and npm;
- you can inspect the code without installing or launching Electron.
+ Download the example and unzip it. The archive contains the
+ complete notes-example folder. Analyze it with
+ Node.js and npm; Electron is not needed for this example.
- Replace the path with the folder you saved. Look for
+ Replace the path with the folder you unzipped. Look for
normalized_result.summary.ipc and the source
locations in normalized_result.graph. The example has
one channel and one paired invocation.
@@ -281,6 +292,12 @@
runtime observations give you another way to investigate them.
- REA gives your coding agent tools to inspect apps, binaries and
- runtime behavior. Ask a question, follow the code, and use what you
- learn in your own project.
+ Use REA with your coding agent to inspect binaries, trace application
+ code and observe runtime behavior.
+ ›Install REA and connect it to this coding agent using npx
+ rea-agents@latest setup. Show me the setup plan for approval,
+ then verify the installation.
+
+
+
Or run this command in your terminal:
npx rea-agents@latest setup
-
- Connects REA to your agent and installs its investigation workflow.
-
+
+
+
+
+
+
+
What REA can analyze
+
+ Choose a guide for your binary, JavaScript app or browser session.
+
+
+
+
+
Native binaries
+
+ Inspect functions, strings, references and call relationships in
+ executables and libraries.
+
- Use REA from your terminal or through your agent's MCP connection.
- Analysis runs locally, and results include the source locations,
- evidence and open questions you need to continue.
+ A copy operation crosses from the page to Electron's main process.
+ Inspect Notion's packaged JavaScript with REA, then follow one
+ channel through the wrappers to the system clipboard.
-
-
-
Use your analysis tools
- Connect a local analysis provider for native binaries. Static
- JavaScript analysis runs with Node.js and npm.
+ Short code excerpts show each handoff. A second figure explains
+ how an HTML marker connects ordinary clipboard text to Notion's
+ structured block data.
+ Start with a question about a real application. See what REA finds,
+ follow the code, and understand how the pieces fit together.
+
+
+
+
+
Native · Reconstruction
+
+
Rebuilding DX-Ball
+
+ Recover a sound-pan calculation from a 1996 Windows game. REA's
+ instructions, caller references and numeric constants lead to a
+ small C function, checked against the original behavior and all 63
+ compiled bytes.
+
+ Trace a page-facing API through Notion's packaged JavaScript.
+ Follow its IPC wrappers to the system clipboard, then see how a
+ small HTML marker keeps rich block data connected to a copy.
+
+ A familiar action leads through several files. Follow Notion's
+ clipboard API from the page, through Electron, to the system
+ clipboard.
+
+
Notion Desktop 7.6.1 · JavaScript · REA
+
+
+
+
The bridge inside Notion's desktop package
+
+
+ 01 · Page-facing API
+ clipboard.write(data)
+ The preload exposes a callable method.
+
+
+ 02 · Preload wrapper
+ ipcRenderer.invoke
+ The wrapper sends the channel and arguments.
+
+
+ 03 · Main-process handler
+ ipcMain.handle
+ The receiver checks which page sent it.
+
+
+ 04 · System clipboard
+ clipboard.write(data)
+ Electron writes the supplied text and HTML.
+
+
+
+ One channel joins the two sides:
+ notion:clipboard:write
+
+
+ The packaged code uses the same channel in the preload and main
+ process. Resolving the wrappers reveals the Electron calls behind
+ those names.
+
+
+
+
+ The page sees a method called clipboard.write. Its
+ implementation in the preload is a wrapper. The useful question is
+ what that wrapper sends, and which code receives it.
+
+
+
+ Your coding agent
+ Copy
+
+
+
+ ›Use REA to trace how Notion copies text and HTML to the
+ system clipboard. Show the code at each step.
+
+
+
+ An example prompt with Notion's local application package
+ available to your coding agent.
+
+
+
REA finds where to start reading
+
+ Analyzing the tab preload with REA locates the API exposed to the
+ page and the call that sends an IPC request. Both findings come
+ with source ranges in the minified bundle:
+
+
+ Selected fields from a fresh
+ analyze_javascript_application result on Notion's
+ tab preload, recorded with REA 4.1.0. Positions use the original
+ bundle's line and zero-based column.
+
+
+
+ The agent opens those locations, finds the clipboard member in the
+ assembled API, and reads invokerInMain. Its call site
+ supplies
+ notion:clipboard:write. That
+ name gives the agent something precise to look for in the main
+ bundle. The four excerpts below show the route it connects.
+
+
+
Four short pieces of code
+
+ Simplified from the packaged JavaScript, with readable variable
+ names. Unrelated members are omitted.
+
+
+
+
Start at the API exposed to the page
+
+
+ tab_browser_view/preload.js · API member
+ Copy
+
+
+
const clipboardApi = {
+ write: invokerInMain("notion:clipboard:write")
+};
+
+// Included in the API exposed through contextBridge:
+// window.__electronApi.clipboard
+
+
+ This member supplies the channel name. The implementation of
+ invokerInMain tells us how the request leaves the
+ page's process.
+
+ The returned function forwards the arguments through
+ Electron's request-and-response IPC. We can now look for
+ notion:clipboard:write
+ on the main-process side.
+
+ This is the destination. After checking the sender, the
+ handler passes the supplied data to Electron's clipboard API.
+ The channel and the two resolved wrappers connect the exposed
+ method to this call.
+
+
+
+
+
+
+
How does a copied block keep its structure?
+
+ The cached web editor adds a small detail to programmatic copies.
+ It sends text and HTML through
+ electronApi.clipboard.write, while keeping Notion's
+ private block data in a local virtual clipboard. An ID in the HTML
+ connects the two.
+
id: <copy-id>
+mimeTypes:
+ text/_notion-blocks-v3-production
+ → structured block data
+
+
+
+
Read the HTML ID
+
Find the same local ID
+
Restore private formats
+
+
+ Illustrative text and ID. The saved module probe recovered the
+ private formats when IDs matched; a stale entry left only the
+ ordinary text and HTML.
+
+
+
+ On paste, the reader looks for the marker and checks it against
+ the saved copy. A matching ID reconnects the text to its richer
+ representation. This explains why the HTML carries more than
+ visible content.
+
+
+ Web assets cached on July 13, 2026. The desktop-package bridge
+ above and this web-side format are separate pieces of the
+ investigation.
+
+
+
+
+
From Markdown to structured blocks
+
+ The same investigation also recovered Notion's Markdown
+ processing. A small table makes the internal format easy to see:
+ its rows and cells become data that the block renderer can use.
+
+
+ A saved extracted-module probe of web parser
+ 422457. Both page and chat filters preserve this
+ table. The tree shows the recorded rows and cell text.
+
+
+
+ Finding that transformation required separating the ordinary
+ paste/import path from the structured Markdown and AI/chat
+ parsers. Each has its own entry point and intermediate format.
+
+
+
+
+
Try another question
+
+ Start with one feature and follow the code that crosses a process
+ boundary. A named API, a literal channel and its receiver are
+ useful places to begin.
+
+
+ Another path: the offline SQLite connection
+
+ Notion's renderer requests getSqliteMeta through
+ IPC. The main process creates a message channel and gives one
+ port to the renderer and the other to a SQLite utility process.
+ Subsequent statement batches and results travel over that port.
+
+
+ Following this handoff explains where the database work runs,
+ and why tracing only the initial IPC request misses the later
+ conversation.
+
+
+
+ Sources and how to read the excerpts
+
+ The clipboard bridge comes from Notion Desktop 7.6.1's
+ app.asar: the tab preload and
+ main/index.js. The excerpts expand minified
+ variable names and omit unrelated code; the sender-check helper
+ is shown with its underlying
+ isNotionWebContents call.
+
+
+ The virtual clipboard and Markdown examples come from web assets
+ cached on July 13, 2026, including modules 747001,
+ 58169, 736810 and 422457.
+ Those assets have their own version. Saved probes executed
+ extracted modules and small harnesses; their outputs describe
+ those probes rather than a captured desktop UI session.
+
+
+ The REA result shown here is a focused analysis of the saved tab
+ preload, recorded with the published REA 4.1.0 package. It
+ locates the API and invoke operation. The clipboard member and
+ matching receiver are connected by reading their wrapper
+ implementations in the package.
+