Merge origin/main into feat/dylib-resolution

Follow #952's Apple ownership: the Mach-O load-command reader, dylib
resolution adapter and fixture move to src/artifacts/apple/, the resolver,
dyld path semantics and findings to src/domain/apple/, and the dylib lane
helper to scripts/verify/apple/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822 2026-10-07 13:28:14 +00:00
commit e5c6fccc35
80 changed files with 1263 additions and 250 deletions

View File

@ -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

View File

@ -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

View File

@ -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

3
.gitignore vendored
View File

@ -45,3 +45,6 @@ Thumbs.db
# Local reference sources and analysis artifacts
/_reference/
# Generated website download
/website/public/examples/notes-example.zip

View File

@ -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.

View File

@ -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

View File

@ -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",

View File

@ -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,

View File

@ -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")

View File

@ -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

View File

@ -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,

View File

@ -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. */

View File

@ -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 {

View File

@ -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,
),
),

View File

@ -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);

View File

@ -8,7 +8,7 @@ import {
artifactCliEvidence,
artifactMcpResult,
withArtifactMcp,
} from "./artifact-e2e.mjs";
} from "../../lib/artifact-e2e.mjs";
const exec = promisify(execFile);

View File

@ -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,

View File

@ -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;

View File

@ -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 {

View File

@ -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<string> => {
if (entry.declaredSize === null || entry.declaredSize > MAX_CATALOG_BYTES)

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<Uint8Array>;

View File

@ -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");

View File

@ -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;

View File

@ -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";

View File

@ -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";

View File

@ -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"),

View File

@ -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,

View File

@ -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";

View File

@ -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);

View File

@ -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({

View File

@ -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);

View File

@ -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,

View File

@ -10,7 +10,7 @@ import {
nativeInvestigationGraphSchema,
nativeInvestigationNodeSchema,
nativeInvestigationTraceLimitsSchema,
} from "./nativeInvestigationGraph.js";
} from "../nativeInvestigationGraph.js";
export { parseInterfaceBuilderRecords } from "./interfaceBuilderKeyedArchive.js";

View File

@ -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());

View File

@ -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({

View File

@ -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;

View File

@ -8,7 +8,7 @@ import {
import {
buildInterfaceBuilderAnalysis,
interfaceBuilderLimitsSchema,
} from "./interfaceBuilderGraph.js";
} from "./apple/interfaceBuilderGraph.js";
const evidence = [
{

View File

@ -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,

View File

@ -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,

View File

@ -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";

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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.

View File

@ -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");
});
});

View File

@ -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/**",

View File

@ -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 <http://127.0.0.1:4173/>. 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
```

View File

@ -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/`.

View File

@ -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 `<copy-id>` are generic illustrations.
`<copy-id>` 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.

View File

@ -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();

View File

@ -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;
}
}

View File

@ -22,7 +22,7 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../showcase/dx-ball/">DX-Ball</a
<a href="../showcase/">Showcases</a
><a class="nav-start" href="./" aria-current="page">Get started</a
><a href="../guides/">Guides</a
><a href="https://github.com/morluto/rea">GitHub</a>
@ -59,20 +59,48 @@
include Claude Code, Codex, Cursor, Gemini CLI, Windsurf and other
supported MCP clients.
</p>
<div class="code-block">
<div class="code-header">
<span>Terminal</span
><button
<div class="agent-terminal">
<div class="terminal-header">
<span class="terminal-title"
><span class="terminal-dots" aria-hidden="true"
><i></i><i></i><i></i></span
>Your coding agent</span
>
<button
class="copy-button"
type="button"
data-copy="agent-setup"
aria-label="Copy agent setup command"
data-copy="agent-install-prompt"
aria-label="Copy REA installation prompt"
>
Copy
</button>
</div>
<pre><code id="agent-setup">npx rea-agents@latest setup</code></pre>
<p class="terminal-prompt">
<span class="prompt-symbol" aria-hidden="true">›</span
><span id="agent-install-prompt"
>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.</span
><span class="terminal-cursor" aria-hidden="true"></span>
</p>
</div>
<details class="evidence-details">
<summary>Or run setup yourself</summary>
<div class="code-block">
<div class="code-header">
<span>Terminal</span
><button
class="copy-button"
type="button"
data-copy="agent-setup"
aria-label="Copy agent setup command"
>
Copy
</button>
</div>
<pre><code id="agent-setup">npx rea-agents@latest setup</code></pre>
</div>
</details>
<p>
Choose your client and review the setup plan. REA adds its MCP
connection and the matching investigation skill, keeping a backup

View File

@ -22,7 +22,7 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../../showcase/dx-ball/">DX-Ball</a
<a href="../../showcase/">Showcases</a
><a class="nav-start" href="../../get-started/">Get started</a
><a href="../">Guides</a
><a href="https://github.com/morluto/rea">GitHub</a>

View File

@ -22,7 +22,7 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../showcase/dx-ball/">DX-Ball</a
<a href="../showcase/">Showcases</a
><a class="nav-start" href="../get-started/">Get started</a
><a href="./" aria-current="page">Guides</a
><a href="https://github.com/morluto/rea">GitHub</a>
@ -71,6 +71,10 @@
<span>IPC</span> <b aria-hidden="true">→</b> <span>CSV file</span>
</div>
<a class="text-link" href="javascript/">Application workflows</a>
<p>
For a larger packaged app, follow
<a href="../showcase/notion/">Notion's clipboard bridge</a>.
</p>
</div>
</section>
<section class="guide-entry">

View File

@ -22,7 +22,7 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../../showcase/dx-ball/">DX-Ball</a
<a href="../../showcase/">Showcases</a
><a class="nav-start" href="../../get-started/">Get started</a
><a href="../">Guides</a
><a href="https://github.com/morluto/rea">GitHub</a>
@ -203,42 +203,53 @@
<section class="article-section" id="try">
<h2>Analyze the example yourself</h2>
<p>
Save these six files together in a folder named
<code>notes-example</code>. 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 <code>notes-example</code> folder. Analyze it with
Node.js and npm; Electron is not needed for this example.
</p>
<ul class="example-files">
<li>
<a href="../../examples/notes-electron/package.json" download
>package.json</a
>
</li>
<li>
<a href="../../examples/notes-electron/main.js" download
>main.js</a
>
</li>
<li>
<a href="../../examples/notes-electron/preload.js" download
>preload.js</a
>
</li>
<li>
<a href="../../examples/notes-electron/renderer.js" download
>renderer.js</a
>
</li>
<li>
<a href="../../examples/notes-electron/csv.js" download
>csv.js</a
>
</li>
<li>
<a href="../../examples/notes-electron/index.html" download
>index.html</a
>
</li>
</ul>
<p>
<a
class="button"
href="../../examples/notes-example.zip"
download="notes-example.zip"
>Download example ZIP</a
>
</p>
<details class="evidence-details">
<summary>Browse the six source files</summary>
<ul class="example-files">
<li>
<a href="../../examples/notes-electron/package.json" download
>package.json</a
>
</li>
<li>
<a href="../../examples/notes-electron/main.js" download
>main.js</a
>
</li>
<li>
<a href="../../examples/notes-electron/preload.js" download
>preload.js</a
>
</li>
<li>
<a href="../../examples/notes-electron/renderer.js" download
>renderer.js</a
>
</li>
<li>
<a href="../../examples/notes-electron/csv.js" download
>csv.js</a
>
</li>
<li>
<a href="../../examples/notes-electron/index.html" download
>index.html</a
>
</li>
</ul>
</details>
<div class="code-block">
<div class="code-header">
<span>Static application analysis</span
@ -255,7 +266,7 @@
/absolute/path/to/notes-example --json</code></pre>
</div>
<p>
Replace the path with the folder you saved. Look for
Replace the path with the folder you unzipped. Look for
<code>normalized_result.summary.ipc</code> and the source
locations in <code>normalized_result.graph</code>. The example has
one channel and one paired invocation.
@ -281,6 +292,12 @@
runtime observations give you another way to investigate them.
</p>
<ul class="reading-list">
<li>
<a href="../../showcase/notion/"
>Follow Notion's clipboard bridge</a
>
<span>Apply the same approach to a packaged Electron app.</span>
</li>
<li>
<a href="../browser/">Observe a browser export</a
><span

View File

@ -22,7 +22,7 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../../showcase/dx-ball/">DX-Ball</a
<a href="../../showcase/">Showcases</a
><a class="nav-start" href="../../get-started/">Get started</a
><a href="../">Guides</a
><a href="https://github.com/morluto/rea">GitHub</a>

View File

@ -22,7 +22,7 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="showcase/dx-ball/">DX-Ball</a>
<a href="showcase/">Showcases</a>
<a class="nav-start" href="get-started/">Get started</a>
<a href="guides/">Guides</a>
<a href="https://github.com/morluto/rea">GitHub</a>
@ -34,17 +34,40 @@
<p class="eyebrow">Software analysis · CLI &amp; MCP</p>
<h1>Find out how<br />software works.</h1>
<p class="lede">
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.
</p>
<div class="actions">
<a class="button" href="get-started/">Get started</a>
<a class="text-link" href="showcase/dx-ball/"
>Read the DX-Ball case study</a
>
<a class="text-link" href="showcase/">Explore the case studies</a>
</div>
<div class="hero-command">
<div class="agent-terminal">
<div class="terminal-header">
<span class="terminal-title"
><span class="terminal-dots" aria-hidden="true"
><i></i><i></i><i></i></span
>Your coding agent</span
>
<button
class="copy-button"
type="button"
data-copy="home-install-prompt"
aria-label="Copy REA installation prompt"
>
Copy
</button>
</div>
<p class="terminal-prompt">
<span class="prompt-symbol" aria-hidden="true">›</span
><span id="home-install-prompt"
>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.</span
><span class="terminal-cursor" aria-hidden="true"></span>
</p>
</div>
<p class="caption">Or run this command in your terminal:</p>
<div class="code-block code-inline">
<code id="setup-command">npx rea-agents@latest setup</code>
<button
@ -56,9 +79,43 @@
Copy
</button>
</div>
<p class="caption">
Connects REA to your agent and installs its investigation workflow.
</p>
</div>
</section>
<section class="section">
<div class="container">
<div class="section-heading">
<h2>What REA can analyze</h2>
<p>
Choose a guide for your binary, JavaScript app or browser session.
</p>
</div>
<div class="workflows">
<div>
<h3>Native binaries</h3>
<p>
Inspect functions, strings, references and call relationships in
executables and libraries.
</p>
<a href="guides/native/">Native analysis guide</a>
</div>
<div>
<h3>JavaScript &amp; Electron</h3>
<p>
Map modules, routes, IPC and native dependencies from an
application folder or ASAR archive.
</p>
<a href="guides/javascript/">Application workflows</a>
</div>
<div>
<h3>Browser &amp; runtime activity</h3>
<p>
Capture selected browser or process activity, then compare the
results across runs.
</p>
<a href="guides/browser/">Browser observation guide</a>
</div>
</div>
</div>
</section>
@ -93,7 +150,7 @@
<section class="section">
<div class="container feature-case">
<div>
<p class="eyebrow">Case study</p>
<p class="eyebrow">Case study · Native</p>
<h2>Rebuilding DX-Ball</h2>
<p>
DX-Ball is a 1996 Windows game. The project is reconstructing it
@ -134,64 +191,44 @@
</div>
</section>
<section class="section">
<div class="container">
<div class="section-heading">
<h2>Bring a target. Ask a question.</h2>
<p>
Start with a local app or file, then narrow the investigation to
the feature or behavior you want to understand.
</p>
</div>
<div class="workflows">
<div>
<h3>Native applications</h3>
<p>
Inspect functions, strings, references and call relationships in
executables and libraries.
</p>
<a href="guides/native/">Native analysis guide</a>
</div>
<div>
<h3>JavaScript &amp; Electron</h3>
<p>
Map modules, routes, IPC and native dependencies from an
application folder or ASAR archive.
</p>
<a href="guides/javascript/">Application workflows</a>
</div>
<div>
<h3>Runtime behavior</h3>
<p>
Capture selected browser or process activity, then compare the
results across runs.
</p>
<a href="guides/browser/">Browser observation guide</a>
</div>
</div>
</div>
</section>
<section class="section">
<div class="container feature-case">
<div>
<h2>Keep the investigation in your workflow.</h2>
<p class="eyebrow">Case study · Electron</p>
<h2>Notion's Electron clipboard</h2>
<p>
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.
</p>
</div>
<div>
<h3>Use your analysis tools</h3>
<p>
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.
</p>
<a class="text-link" href="get-started/#native"
>Choose a native provider</a
<a class="text-link" href="showcase/notion/"
>Follow the clipboard bridge</a
>
</div>
<figure class="workflow-figure case-preview">
<p class="figure-title">Clipboard IPC channel</p>
<div class="code-block">
<div class="code-header"><span>Preload · send</span></div>
<pre><code>invokerInMain(
<span class="notion-channel">"notion:clipboard:write"</span>
)</code></pre>
</div>
<div class="code-block">
<div class="code-header"><span>Main process · receive</span></div>
<pre><code>addListener(
<span class="notion-channel">"notion:clipboard:write"</span>,
handler
)</code></pre>
</div>
<figcaption>
Matching names give the investigation a path to follow.
</figcaption>
</figure>
</div>
</section>
@ -200,7 +237,8 @@
<div>
<h2>What would you like to understand?</h2>
<p>
Connect your agent, or get your first result from the terminal.
Connect REA to your coding agent, or run your first analysis from
the terminal.
</p>
</div>
<a class="button" href="get-started/">Get started</a>

View File

@ -22,7 +22,7 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="./" aria-current="page">DX-Ball</a>
<a href="../">Showcases</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../guides/">Guides</a>
<a href="https://github.com/morluto/rea">GitHub</a>
@ -31,7 +31,9 @@
</header>
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb"><a href="../../">Home</a> / DX-Ball</p>
<p class="breadcrumb">
<a href="../../">Home</a> / <a href="../">Showcases</a> / DX-Ball
</p>
<p class="eyebrow">Case study · DX-Ball 1.07</p>
<h1>Rebuilding DX-Ball<br />from its executable.</h1>
<p class="lede">

View File

@ -0,0 +1,102 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Case studies — REA</title>
<meta
name="description"
content="Follow REA investigations through code and figures: recover a native calculation in DX-Ball and trace Notion's Electron clipboard bridge."
/>
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../assets/styles.css" />
<script src="../assets/main.js" defer></script>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../" aria-label="REA home"
><span class="brand-name">REA</span
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="./" aria-current="page">Showcases</a>
<a class="nav-start" href="../get-started/">Get started</a>
<a href="../guides/">Guides</a>
<a href="https://github.com/morluto/rea">GitHub</a>
</nav>
</div>
</header>
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb"><a href="../">Home</a> / Showcases</p>
<h1>Follow the<br />investigation.</h1>
<p class="lede">
Start with a question about a real application. See what REA finds,
follow the code, and understand how the pieces fit together.
</p>
</div>
<div class="guide-index">
<section class="guide-entry">
<p class="eyebrow">Native · Reconstruction</p>
<div>
<h2>Rebuilding DX-Ball</h2>
<p>
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.
</p>
<div class="guide-route">
<span>Executable</span> <b aria-hidden="true">→</b>
<span>REA findings</span> <b aria-hidden="true">→</b>
<span>Recovered C</span> <b aria-hidden="true">→</b>
<span>Validation</span>
</div>
<a class="text-link" href="dx-ball/">Read the DX-Ball case study</a>
</div>
</section>
<section class="guide-entry">
<p class="eyebrow">Electron · Feature tracing</p>
<div>
<h2>Following a copy through Notion</h2>
<p>
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.
</p>
<div class="guide-route">
<span>Packaged app</span> <b aria-hidden="true">→</b>
<span>Exposed API</span> <b aria-hidden="true">→</b>
<span>IPC wrappers</span> <b aria-hidden="true">→</b>
<span>Clipboard</span>
</div>
<a class="text-link" href="notion/">Read the Notion case study</a>
</div>
</section>
</div>
<section class="article-section">
<h2>Try a smaller example first.</h2>
<p>
The guides walk through a native calculation, an Electron export and a
browser request. Each starts with a prompt and a concrete target.
</p>
<a class="text-link" href="../guides/">Explore the worked guides</a>
</section>
</main>
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
<div class="footer-links">
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
</div>
</div>
</footer>
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
</body>
</html>

View File

@ -0,0 +1,477 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Following Notion's clipboard through Electron — REA</title>
<meta
name="description"
content="Follow Notion's clipboard API through Electron's preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
/>
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
<script src="../../assets/main.js" defer></script>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
><span class="brand-name">REA</span
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../guides/">Guides</a>
<a href="https://github.com/morluto/rea">GitHub</a>
</nav>
</div>
</header>
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
<a href="../../">Home</a> / <a href="../">Showcases</a> / Notion
</p>
<p class="eyebrow">Case study · Electron</p>
<h1>Following a copy<br />through Notion.</h1>
<p class="lede">
A familiar action leads through several files. Follow Notion's
clipboard API from the page, through Electron, to the system
clipboard.
</p>
<p class="page-meta">Notion Desktop 7.6.1 · JavaScript · REA</p>
</div>
<figure class="workflow-figure">
<p class="figure-title">The bridge inside Notion's desktop package</p>
<ol class="flow-steps notion-bridge-flow">
<li>
<span class="flow-label">01 · Page-facing API</span>
<span class="flow-node"><code>clipboard.write(data)</code></span>
<span class="flow-note"
>The preload exposes a callable method.</span
>
</li>
<li>
<span class="flow-label">02 · Preload wrapper</span>
<span class="flow-node"><code>ipcRenderer.invoke</code></span>
<span class="flow-note"
>The wrapper sends the channel and arguments.</span
>
</li>
<li>
<span class="flow-label">03 · Main-process handler</span>
<span class="flow-node"><code>ipcMain.handle</code></span>
<span class="flow-note"
>The receiver checks which page sent it.</span
>
</li>
<li>
<span class="flow-label">04 · System clipboard</span>
<span class="flow-node"><code>clipboard.write(data)</code></span>
<span class="flow-note"
>Electron writes the supplied text and HTML.</span
>
</li>
</ol>
<p class="flow-result">
One channel joins the two sides:
<code class="notion-channel">notion:clipboard:write</code>
</p>
<figcaption>
The packaged code uses the same channel in the preload and main
process. Resolving the wrappers reveals the Electron calls behind
those names.
</figcaption>
</figure>
<div class="article-layout">
<nav class="contents" aria-label="On this page">
<p>In this case study</p>
<ul>
<li><a href="#clipboard">Follow the clipboard bridge</a></li>
<li><a href="#rich-clipboard">Keeping a block's structure</a></li>
<li><a href="#markdown">Markdown becomes blocks</a></li>
<li><a href="#continue">Try another question</a></li>
</ul>
</nav>
<article class="article" aria-label="Notion Electron case study">
<section class="article-section" id="clipboard">
<h2>Where does the copy operation go?</h2>
<p>
The page sees a method called <code>clipboard.write</code>. Its
implementation in the preload is a wrapper. The useful question is
what that wrapper sends, and which code receives it.
</p>
<div class="agent-terminal">
<div class="terminal-header">
<span class="terminal-title"
><span class="terminal-dots" aria-hidden="true"
><i></i><i></i><i></i></span
>Your coding agent</span
><button
class="copy-button"
type="button"
data-copy="notion-prompt"
aria-label="Copy Notion analysis prompt"
>
Copy
</button>
</div>
<p class="terminal-prompt">
<span class="prompt-symbol" aria-hidden="true">›</span
><span id="notion-prompt"
>Use REA to trace how Notion copies text and HTML to the
system clipboard. Show the code at each step.</span
><span class="terminal-cursor" aria-hidden="true"></span>
</p>
</div>
<p class="caption">
An example prompt with Notion's local application package
available to your coding agent.
</p>
<h3 class="small-heading">REA finds where to start reading</h3>
<p>
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:
</p>
<figure class="workflow-figure">
<p class="figure-title">REA findings → two places to open</p>
<div class="rea-query-pair">
<div class="code-block">
<div class="code-header"><span>Exposed page API</span></div>
<pre><code>api_key: "__electronApi"
api_status: "dynamic"
source: preload.js:1:22764–22828</code></pre>
</div>
<div class="code-block">
<div class="code-header"><span>Renderer IPC call</span></div>
<pre><code>operation: "invoke"
channel_expression: "e"
source: preload.js:1:6486–6522</code></pre>
</div>
</div>
<figcaption>
Selected fields from a fresh
<code>analyze_javascript_application</code> result on Notion's
tab preload, recorded with REA 4.1.0. Positions use the original
bundle's line and zero-based column.
</figcaption>
</figure>
<p>
The agent opens those locations, finds the clipboard member in the
assembled API, and reads <code>invokerInMain</code>. Its call site
supplies
<code class="notion-channel">notion:clipboard:write</code>. That
name gives the agent something precise to look for in the main
bundle. The four excerpts below show the route it connects.
</p>
<h3 class="small-heading">Four short pieces of code</h3>
<p class="caption">
Simplified from the packaged JavaScript, with readable variable
names. Unrelated members are omitted.
</p>
<ol class="rea-queries">
<li>
<h4>Start at the API exposed to the page</h4>
<div class="code-block">
<div class="code-header">
<span>tab_browser_view/preload.js · API member</span
><button
class="copy-button"
type="button"
data-copy="notion-preload-source"
aria-label="Copy Notion preload excerpt"
>
Copy
</button>
</div>
<pre><code id="notion-preload-source">const clipboardApi = {
write: invokerInMain(<span class="notion-channel">"notion:clipboard:write"</span>)
};
// Included in the API exposed through contextBridge:
// window.__electronApi.clipboard</code></pre>
</div>
<p>
This member supplies the channel name. The implementation of
<code>invokerInMain</code> tells us how the request leaves the
page's process.
</p>
</li>
<li>
<h4>Resolve the preload's wrapper</h4>
<div class="code-block">
<div class="code-header">
<span>tab_browser_view/preload.js · IPC helper</span
><button
class="copy-button"
type="button"
data-copy="notion-invoke-source"
aria-label="Copy Notion invoke wrapper excerpt"
>
Copy
</button>
</div>
<pre><code id="notion-invoke-source">function invokerInMain(channel) {
return (...args) =&gt; invokeInMainAndReturnResult(channel, ...args);
}
function invokeInMainAndReturnResult(channel, ...args) {
return ipcRenderer.invoke(channel, ...args);
}</code></pre>
</div>
<p>
The returned function forwards the arguments through
Electron's request-and-response IPC. We can now look for
<code class="notion-channel">notion:clipboard:write</code>
on the main-process side.
</p>
</li>
<li>
<h4>Check how the receiver is registered</h4>
<div class="code-block">
<div class="code-header">
<span>main/index.js · handler helper</span
><button
class="copy-button"
type="button"
data-copy="notion-handler-source"
aria-label="Copy Notion handler wrapper excerpt"
>
Copy
</button>
</div>
<pre><code id="notion-handler-source">const handleRequestFromRenderer = {
addListener(channel, handler) {
ipcMain.handle(channel, handler);
}
};</code></pre>
</div>
<p>
The main process uses another wrapper. Its
<code>addListener</code> method registers an
<code>ipcMain.handle</code> receiver for that channel.
</p>
</li>
<li>
<h4>Find the write and its sender check</h4>
<div class="code-block">
<div class="code-header">
<span>main/index.js · clipboard receiver</span
><button
class="copy-button"
type="button"
data-copy="notion-write-source"
aria-label="Copy Notion clipboard receiver excerpt"
>
Copy
</button>
</div>
<pre><code id="notion-write-source">handleRequestFromRenderer.addListener(
<span class="notion-channel">"notion:clipboard:write"</span>,
(event, data) =&gt;
isNotionWebContents(event.sender)
? clipboard.write(data)
: undefined
);</code></pre>
</div>
<p>
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.
</p>
</li>
</ol>
</section>
<section class="article-section" id="rich-clipboard">
<h2>How does a copied block keep its structure?</h2>
<p>
The cached web editor adds a small detail to programmatic copies.
It sends text and HTML through
<code>electronApi.clipboard.write</code>, while keeping Notion's
private block data in a local virtual clipboard. An ID in the HTML
connects the two.
</p>
<figure class="workflow-figure">
<p class="figure-title">One copy, two representations</p>
<div class="notion-clipboard-grid">
<div class="code-block">
<div class="code-header">
<span>Sent to the system clipboard</span>
</div>
<pre><code>text: "Project notes"
html: "&lt;p&gt;Project notes&lt;/p&gt;"
+ "&lt;!-- notionvc: <span class="notion-channel">&lt;copy-id&gt;</span> --&gt;"</code></pre>
</div>
<div class="code-block">
<div class="code-header">
<span>Kept in the local virtual clipboard</span>
</div>
<pre><code>id: <span class="notion-channel">&lt;copy-id&gt;</span>
mimeTypes:
text/_notion-blocks-v3-production
→ structured block data</code></pre>
</div>
</div>
<ol
class="notion-mini-flow"
aria-label="Restoring rich clipboard data on paste"
>
<li>Read the HTML ID</li>
<li>Find the same local ID</li>
<li>Restore private formats</li>
</ol>
<figcaption>
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.
</figcaption>
</figure>
<p>
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.
</p>
<p class="caption">
Web assets cached on July 13, 2026. The desktop-package bridge
above and this web-side format are separate pieces of the
investigation.
</p>
</section>
<section class="article-section" id="markdown">
<h2>From Markdown to structured blocks</h2>
<p>
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.
</p>
<figure class="workflow-figure">
<p class="figure-title">
A table through Notion's Markdown parser
</p>
<div class="notion-table-example">
<div class="code-block">
<div class="code-header"><span>Markdown input</span></div>
<pre><code>| A | B |
| - | - |
| 1 | 2 |</code></pre>
</div>
<div class="code-block">
<div class="code-header">
<span>Structured parser output · excerpt</span>
</div>
<pre><code>table · headerRow: true
table_row
cells: ["A"], ["B"]
table_row
cells: ["1"], ["2"]</code></pre>
</div>
</div>
<figcaption>
A saved extracted-module probe of web parser
<code>422457</code>. Both page and chat filters preserve this
table. The tree shows the recorded rows and cell text.
</figcaption>
</figure>
<p>
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.
</p>
</section>
<section class="article-section" id="continue">
<h2>Try another question</h2>
<p>
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.
</p>
<details class="evidence-details">
<summary>Another path: the offline SQLite connection</summary>
<p>
Notion's renderer requests <code>getSqliteMeta</code> 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.
</p>
<p>
Following this handoff explains where the database work runs,
and why tracing only the initial IPC request misses the later
conversation.
</p>
</details>
<details class="evidence-details">
<summary>Sources and how to read the excerpts</summary>
<p>
The clipboard bridge comes from Notion Desktop 7.6.1's
<code>app.asar</code>: the tab preload and
<code>main/index.js</code>. The excerpts expand minified
variable names and omit unrelated code; the sender-check helper
is shown with its underlying
<code>isNotionWebContents</code> call.
</p>
<p>
The virtual clipboard and Markdown examples come from web assets
cached on July 13, 2026, including modules <code>747001</code>,
<code>58169</code>, <code>736810</code> and <code>422457</code>.
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.
</p>
<p>
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.
</p>
</details>
<ul class="reading-list">
<li>
<a href="../../guides/javascript/"
>JavaScript application guide</a
><span
>Follow a small Electron application's export feature.</span
>
</li>
<li>
<a href="../dx-ball/">DX-Ball reconstruction</a
><span
>Follow native instructions to a checked C function.</span
>
</li>
</ul>
<div class="actions">
<a class="button" href="../../get-started/"
>Start with your own target</a
>
</div>
</section>
</article>
</div>
</main>
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
<div class="footer-links">
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
</div>
</div>
</footer>
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
</body>
</html>