chore: merge main and regenerate combined catalog metadata
This commit is contained in:
@@ -1,30 +1,14 @@
|
||||
name: Documentation site
|
||||
name: Documentation build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
- ".nvmrc"
|
||||
- ".github/workflows/pages.yml"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
- ".nvmrc"
|
||||
- ".github/workflows/pages.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: pages-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
group: documentation-build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
@@ -33,6 +17,7 @@ jobs:
|
||||
build:
|
||||
name: Build documentation
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -41,23 +26,3 @@ jobs:
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run docs:build
|
||||
- uses: actions/upload-pages-artifact@v4
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
with:
|
||||
path: docs/.vitepress/dist
|
||||
|
||||
deploy:
|
||||
name: Publish documentation
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- uses: actions/configure-pages@v5
|
||||
- uses: actions/deploy-pages@v4
|
||||
id: deployment
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Website checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "website/**"
|
||||
- "scripts/verify-website.py"
|
||||
- ".github/workflows/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: website-check-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Website checks
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check website links and publishing configuration
|
||||
run: python3 scripts/verify-website.py
|
||||
@@ -21,6 +21,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check website links and publishing configuration
|
||||
run: python3 scripts/verify-website.py
|
||||
- name: Configure Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload the website
|
||||
|
||||
+2
-1
@@ -28,7 +28,8 @@
|
||||
{
|
||||
"files": [
|
||||
"scripts/electron-active-hook*.cjs",
|
||||
"tests/conformance/readiness/electron/*.js"
|
||||
"tests/conformance/readiness/electron/*.js",
|
||||
"website/public/examples/notes-electron/*.js"
|
||||
],
|
||||
"rules": {
|
||||
"no-var-requires": "off"
|
||||
|
||||
@@ -284,7 +284,13 @@ Cross-version matching first prefers exact CIL/signature identity. When that
|
||||
does not match, it pairs an exact declared type, method name, and raw signature
|
||||
before trying structural body shape. The exact-signature key uses names only
|
||||
as part of that full tuple; names alone never select a pair. Duplicate tuples
|
||||
remain ambiguous rather than being paired by token order.
|
||||
remain ambiguous rather than being paired by token order. Fields follow the
|
||||
same exact tiers. The raw signature is exact whether or not REA decoded it, so
|
||||
undecoded signatures pair by that tuple too, but they never enter structural
|
||||
rounds. An unmatched member is therefore reported as `unknown`, not added or
|
||||
removed, when its own signature was not decoded or when the other side has an
|
||||
unpaired member, one-sided or ambiguous, with the same declared type and name
|
||||
whose signature was not decoded.
|
||||
|
||||
For a matched method, an unavailable or partial body makes body-shape facets
|
||||
unknown while preserving observed signature differences. Structural identity
|
||||
|
||||
@@ -669,9 +669,9 @@
|
||||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "daccefdcf1dee770d0901a557d6afb8828ea9b9161f1fce896dbb5489ff7779d",
|
||||
"tools_sha256": "967c3d60f973aeb86a90a5772a217bef90cc5e48d2eea9621d3a2cc2a68834eb",
|
||||
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
|
||||
"combined_sha256": "2b780592f00bd9bb79650a0ded8a034e2946fccef535343d935841dd42637c4b",
|
||||
"combined_sha256": "e893b9df15ed6eec249c8f9adb583dc4870c240f9126f8e42680a014cd6fba90",
|
||||
"providers_sha256": "a7d885531ee25d42c51385444faec544d34cd7eba6250a7dc75c4faedd4454a3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"manifest_id": "ecm_51967090044994ae062dd96dd2e6bdf7c5a589ffaa1de3c13833bf6cce3914f4",
|
||||
"manifest_id": "ecm_1cd8dcd8881d15dcd8ff0829cb45f1cb14501e5de6c36f6c968a986b7c022daf",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
@@ -13,7 +13,7 @@
|
||||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "36782d5c8183a2e3dcbed8065cf41c1c296be6ba0d4afe3a58f000013b6fa5bb"
|
||||
"sha256": "d337de130efddaaee02ecef46a3e4ecb5ee2520ad9594a48ae4e471253c5f781"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Check local website references and the sole GitHub Pages publisher."""
|
||||
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import unquote, urlsplit
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
class HtmlReferences(HTMLParser):
|
||||
"""Collect document IDs and references without fetching external URLs."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.ids = set()
|
||||
self.references = []
|
||||
self.duplicate_ids = []
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
attributes = dict(attrs)
|
||||
identifier = attributes.get("id")
|
||||
if identifier is not None:
|
||||
if identifier in self.ids:
|
||||
self.duplicate_ids.append(identifier)
|
||||
self.ids.add(identifier)
|
||||
for attribute in ("href", "src"):
|
||||
reference = attributes.get(attribute)
|
||||
if reference is not None:
|
||||
self.references.append(reference)
|
||||
|
||||
|
||||
def check_site(site):
|
||||
"""Check assets, HTML fragment destinations and SVG syntax."""
|
||||
pages = {}
|
||||
errors = []
|
||||
references = 0
|
||||
for path in sorted(site.rglob("*.html")):
|
||||
parser = HtmlReferences()
|
||||
parser.feed(path.read_text(encoding="utf-8"))
|
||||
pages[path] = parser
|
||||
if site / "index.html" not in pages:
|
||||
errors.append("website/public/index.html is missing")
|
||||
|
||||
for path, page in pages.items():
|
||||
label = path.relative_to(site)
|
||||
for identifier in page.duplicate_ids:
|
||||
errors.append(f"{label}: duplicate ID {identifier!r}")
|
||||
for reference in page.references:
|
||||
url = urlsplit(reference)
|
||||
if url.scheme or url.netloc:
|
||||
continue
|
||||
references += 1
|
||||
if url.path.startswith("/"):
|
||||
errors.append(f"{label}: use a relative site reference: {reference}")
|
||||
continue
|
||||
target = (path.parent / unquote(url.path)).resolve() if url.path else path
|
||||
if not target.is_relative_to(site):
|
||||
errors.append(f"{label}: reference leaves the website: {reference}")
|
||||
continue
|
||||
if target.is_dir():
|
||||
target /= "index.html"
|
||||
if not target.is_file():
|
||||
errors.append(f"{label}: missing local target: {reference}")
|
||||
elif target in pages and url.fragment and unquote(url.fragment) not in pages[target].ids:
|
||||
errors.append(f"{label}: missing HTML fragment: {reference}")
|
||||
|
||||
for path in sorted(site.rglob("*.svg")):
|
||||
try:
|
||||
ET.parse(path)
|
||||
except ET.ParseError as error:
|
||||
errors.append(f"{path.relative_to(site)}: invalid SVG XML: {error}")
|
||||
return len(pages), references, errors
|
||||
|
||||
|
||||
def check_publisher(root):
|
||||
"""Detect a second Pages action that could replace the website."""
|
||||
expected = root / ".github/workflows/website-pages.yml"
|
||||
workflows = root / ".github/workflows"
|
||||
deploy_action = re.compile(r"^\s*-?\s*uses:\s*['\"]?actions/deploy-pages@", re.MULTILINE)
|
||||
publishers = [
|
||||
path
|
||||
for path in sorted(workflows.iterdir())
|
||||
if path.suffix in (".yml", ".yaml") and deploy_action.search(path.read_text(encoding="utf-8"))
|
||||
]
|
||||
if publishers != [expected]:
|
||||
actual = ", ".join(str(path.relative_to(root)) for path in publishers) or "none"
|
||||
return [f"Pages must have one publisher, website-pages.yml; found: {actual}"]
|
||||
return []
|
||||
|
||||
|
||||
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_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("website-pages.yml is the sole GitHub Pages publisher.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
|
||||
metadata:
|
||||
version: "26"
|
||||
tool_count: 131
|
||||
catalog_digest: "2b780592f00bd9bb79650a0ded8a034e2946fccef535343d935841dd42637c4b"
|
||||
catalog_digest: "e893b9df15ed6eec249c8f9adb583dc4870c240f9126f8e42680a014cd6fba90"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
||||
@@ -10,9 +10,63 @@ import { createEvidence } from "../../domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../../domain/evidenceBundle.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
import { createAnalysisExecution } from "../AnalysisProvider.js";
|
||||
import { AnalysisSnapshotCache } from "./AnalysisSnapshotCache.js";
|
||||
import type {
|
||||
AnalysisOperation,
|
||||
CapabilityDescriptor,
|
||||
} from "../AnalysisProvider.js";
|
||||
import {
|
||||
AnalysisSnapshotCache,
|
||||
isSnapshotCacheable,
|
||||
} from "./AnalysisSnapshotCache.js";
|
||||
|
||||
describe("analysis snapshot cache partitioning", () => {
|
||||
it.each<{
|
||||
operation: Exclude<AnalysisOperation, "health">;
|
||||
parameters: Record<string, string>;
|
||||
cacheable: boolean;
|
||||
}>([
|
||||
{ operation: "list_strings", parameters: {}, cacheable: false },
|
||||
{
|
||||
operation: "list_strings",
|
||||
parameters: { document: "fixture" },
|
||||
cacheable: true,
|
||||
},
|
||||
{
|
||||
operation: "address_name",
|
||||
parameters: { document: "fixture" },
|
||||
cacheable: false,
|
||||
},
|
||||
{
|
||||
operation: "address_name",
|
||||
parameters: { document: "fixture", address: "0x1000" },
|
||||
cacheable: true,
|
||||
},
|
||||
{ operation: "binary_overview", parameters: {}, cacheable: true },
|
||||
])(
|
||||
"retains explicit document and cursor requirements for $operation",
|
||||
({ operation, parameters, cacheable }) => {
|
||||
const descriptor: CapabilityDescriptor = {
|
||||
operation,
|
||||
provider: ANALYSIS_SNAPSHOT_PROVIDER,
|
||||
available: true,
|
||||
reason: null,
|
||||
effects: {
|
||||
mutatesArtifact: false,
|
||||
launchesProcess: false,
|
||||
mayShowUi: false,
|
||||
mayAccessNetwork: false,
|
||||
mayWriteFilesystem: false,
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
},
|
||||
limitations: [],
|
||||
};
|
||||
expect(isSnapshotCacheable(operation, descriptor, parameters)).toBe(
|
||||
cacheable,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("returns detached data only from the exact provider/profile partition", () => {
|
||||
const cache = new AnalysisSnapshotCache();
|
||||
const execution = createAnalysisExecution(
|
||||
|
||||
@@ -24,7 +24,7 @@ import type {
|
||||
AnalysisOperation,
|
||||
CapabilityDescriptor,
|
||||
} from "../AnalysisProvider.js";
|
||||
import { GENERATED_MCP_TOOL_CATALOG } from "../../generatedMcpToolCatalog.js";
|
||||
import { OFFICIAL_TOOL_CONTRACTS } from "../../contracts/officialToolContracts.js";
|
||||
|
||||
const STATEFUL_OPERATIONS: ReadonlySet<AnalysisOperation> = new Set([
|
||||
"health",
|
||||
@@ -46,14 +46,12 @@ const CURSOR_DEFAULT_OPERATIONS: ReadonlySet<AnalysisOperation> = new Set([
|
||||
]);
|
||||
|
||||
const DOCUMENT_SCOPED_OPERATIONS: ReadonlySet<string> = new Set(
|
||||
GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "official-proxy")
|
||||
.map(({ name }) => name)
|
||||
.filter(
|
||||
(name) =>
|
||||
name !== "current_document" &&
|
||||
name !== "list_documents" &&
|
||||
name !== "set_current_document",
|
||||
),
|
||||
OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name).filter(
|
||||
(name) =>
|
||||
name !== "current_document" &&
|
||||
name !== "list_documents" &&
|
||||
name !== "set_current_document",
|
||||
),
|
||||
);
|
||||
|
||||
/** Whether an operation is immutable and independent of provider UI state. */
|
||||
|
||||
@@ -3,7 +3,8 @@ import { AnyMap, eachMapping } from "@jridgewell/trace-mapping";
|
||||
|
||||
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
|
||||
import { isUrlLikeModuleSpecifier } from "../domain/webBundleAnalyzerAst.js";
|
||||
import { analyzeParsedJavaScriptSemantics } from "../domain/javascript/javascriptSemanticAnalysis.js";
|
||||
import { analyzeParsedJavaScriptReferences } from "../domain/javascript/javascriptSemanticAnalysis.js";
|
||||
import { traverseJavaScriptAst } from "../domain/javascript/javascriptSemanticTraversal.js";
|
||||
import { parseJavaScriptSource } from "../domain/javascript/javascriptSourceParser.js";
|
||||
import { hasValidSourceMapContents } from "../domain/sourceMapContents.js";
|
||||
import type {
|
||||
@@ -237,42 +238,42 @@ const originalModuleEdges = (
|
||||
// after an unrecoverable point are missing, so the edges are a subset.
|
||||
if (parsed.errors.length > 0) incomplete.push(source.source);
|
||||
let unboundRequires: ReadonlySet<string> | undefined;
|
||||
t.traverseFast(parsed, (node) => {
|
||||
const dependency = originalDependency(node);
|
||||
if (dependency === null) return;
|
||||
const { kind, specifier } = dependency;
|
||||
if (kind === "require" && t.isCallExpression(node)) {
|
||||
unboundRequires ??= new Set(
|
||||
analyzeParsedJavaScriptSemantics(parsed)
|
||||
.references.filter(
|
||||
({ name, role, resolution }) =>
|
||||
name === "require" &&
|
||||
role === "read" &&
|
||||
resolution === "unbound",
|
||||
traverseJavaScriptAst(parsed, {
|
||||
enter: (node) => {
|
||||
const dependency = originalDependency(node);
|
||||
if (dependency === null) return;
|
||||
const { kind, specifier } = dependency;
|
||||
if (kind === "require" && t.isCallExpression(node)) {
|
||||
unboundRequires ??= new Set(
|
||||
analyzeParsedJavaScriptReferences(parsed, "require")
|
||||
.filter(
|
||||
({ role, resolution }) =>
|
||||
role === "read" && resolution === "unbound",
|
||||
)
|
||||
.map(
|
||||
({ location }) =>
|
||||
`${String(location.start.line)}:${String(location.start.column)}`,
|
||||
),
|
||||
);
|
||||
const location = node.callee.loc?.start;
|
||||
if (
|
||||
location === undefined ||
|
||||
!unboundRequires.has(
|
||||
`${String(location.line)}:${String(location.column)}`,
|
||||
)
|
||||
.map(
|
||||
({ location }) =>
|
||||
`${String(location.start.line)}:${String(location.start.column)}`,
|
||||
),
|
||||
);
|
||||
const location = node.callee.loc?.start;
|
||||
if (
|
||||
location === undefined ||
|
||||
!unboundRequires.has(
|
||||
`${String(location.line)}:${String(location.column)}`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
}
|
||||
const key = `${source.source}\0${kind}\0${specifier}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
edges.push({
|
||||
from_source: source.source,
|
||||
kind,
|
||||
specifier,
|
||||
resolved_source: resolveOriginalSource(specifier, source.source),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const key = `${source.source}\0${kind}\0${specifier}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
edges.push({
|
||||
from_source: source.source,
|
||||
kind,
|
||||
specifier,
|
||||
resolved_source: resolveOriginalSource(specifier, source.source),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
return { edges, incomplete };
|
||||
|
||||
+47
-33
@@ -9,26 +9,31 @@ import { CLI_COMMAND_NAMES } from "./cliCommandNames.js";
|
||||
|
||||
export { CLI_COMMAND_NAMES } from "./cliCommandNames.js";
|
||||
|
||||
const toolCatalog = TOOL_CONTRACTS.map((contract) => ({
|
||||
name: contract.name,
|
||||
title: contract.title,
|
||||
surface: contract.kind,
|
||||
description: contract.description,
|
||||
effects: { ...contract.effects },
|
||||
annotations: contract.annotations,
|
||||
input_schema: {
|
||||
type: "object",
|
||||
...z.toJSONSchema(contract.inputSchema, {
|
||||
unrepresentable: "any",
|
||||
}),
|
||||
},
|
||||
output_schema: {
|
||||
type: "object",
|
||||
...z.toJSONSchema(contract.outputSchema, {
|
||||
unrepresentable: "any",
|
||||
}),
|
||||
},
|
||||
})).sort((left, right) => left.name.localeCompare(right.name));
|
||||
const sortedToolContracts = [...TOOL_CONTRACTS].sort((left, right) =>
|
||||
left.name.localeCompare(right.name),
|
||||
);
|
||||
|
||||
const createToolCatalog = () =>
|
||||
sortedToolContracts.map((contract) => ({
|
||||
name: contract.name,
|
||||
title: contract.title,
|
||||
surface: contract.kind,
|
||||
description: contract.description,
|
||||
effects: { ...contract.effects },
|
||||
annotations: contract.annotations,
|
||||
input_schema: {
|
||||
type: "object",
|
||||
...z.toJSONSchema(contract.inputSchema, {
|
||||
unrepresentable: "any",
|
||||
}),
|
||||
},
|
||||
output_schema: {
|
||||
type: "object",
|
||||
...z.toJSONSchema(contract.outputSchema, {
|
||||
unrepresentable: "any",
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const promptCatalog = PROMPT_CONTRACTS.map((contract) => ({
|
||||
name: contract.name,
|
||||
@@ -45,26 +50,35 @@ const digest = (value: unknown): string => {
|
||||
return createHash("sha256").update(encoded).digest("hex");
|
||||
};
|
||||
|
||||
/** Stable, schema-sensitive identity for every public REA surface. */
|
||||
export const CATALOG_IDENTITY = {
|
||||
counts: {
|
||||
cli_commands: CLI_COMMAND_NAMES.length,
|
||||
mcp_tools: toolCatalog.length,
|
||||
mcp_prompts: promptCatalog.length,
|
||||
},
|
||||
digests: {
|
||||
tools_sha256: digest(toolCatalog),
|
||||
const createCatalogDigests = () => {
|
||||
const tools = createToolCatalog();
|
||||
return {
|
||||
tools_sha256: digest(tools),
|
||||
prompts_sha256: digest(promptCatalog),
|
||||
combined_sha256: digest({
|
||||
cli: CLI_COMMAND_NAMES,
|
||||
tools: toolCatalog,
|
||||
tools,
|
||||
prompts: promptCatalog,
|
||||
}),
|
||||
} as const;
|
||||
};
|
||||
|
||||
let catalogDigests: ReturnType<typeof createCatalogDigests> | undefined;
|
||||
|
||||
/** Stable, schema-sensitive identity; schema digests are computed on first use. */
|
||||
export const CATALOG_IDENTITY = {
|
||||
counts: {
|
||||
cli_commands: CLI_COMMAND_NAMES.length,
|
||||
mcp_tools: sortedToolContracts.length,
|
||||
mcp_prompts: promptCatalog.length,
|
||||
},
|
||||
tools: toolCatalog.map(({ name, surface, effects, annotations }) => ({
|
||||
get digests() {
|
||||
return (catalogDigests ??= createCatalogDigests());
|
||||
},
|
||||
tools: sortedToolContracts.map(({ name, kind, effects, annotations }) => ({
|
||||
name,
|
||||
surface,
|
||||
effects,
|
||||
surface: kind,
|
||||
effects: { ...effects },
|
||||
annotations: {
|
||||
read_only: annotations.readOnlyHint ?? false,
|
||||
destructive: annotations.destructiveHint ?? false,
|
||||
|
||||
@@ -126,7 +126,7 @@ export const MANAGED_WORKFLOW_TOOL_CONTRACTS = [
|
||||
name: "compare_managed_members",
|
||||
...toolContractMetadata("compare_managed_members"),
|
||||
description:
|
||||
"Compare two authenticated inspect_managed_members Evidence records using unique-only decoded-CIL/signature and structural method-shape tiers. Names are reported as observations but are not used as a matching basis; metadata tokens remain build-local coordinates bound to each artifact SHA-256 and MVID, and the tuple digest does not itself remap them.",
|
||||
"Compare two authenticated inspect_managed_members Evidence records using unique-only tiers: exact CIL/signature, exact declared type, name, and raw signature (decoded or not), and decoded structural method shape. Names alone are never a matching basis; a member whose signature was not decoded, or whose same-named counterpart's was not, stays unknown rather than added or removed. Metadata tokens remain build-local coordinates bound to each artifact SHA-256 and MVID, and the tuple digest does not itself remap them.",
|
||||
kind: "application",
|
||||
inputSchema: compareManagedMembersReferenceInputSchema,
|
||||
outputSchema: comparisonOutputSchema,
|
||||
|
||||
@@ -317,3 +317,110 @@ describe("keyed archive decoding and graph limits", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AppKit keyed archive connectors", () => {
|
||||
it("reads outlet and control connectors from NS-prefixed keys", () => {
|
||||
const parsed = parseInterfaceBuilderRecords({
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$objects: [
|
||||
"$null",
|
||||
{ $class: { UID: 6 } },
|
||||
{ $class: { UID: 7 }, identifier: "unrelated-identifier" },
|
||||
{
|
||||
$class: { UID: 8 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 2 },
|
||||
NSLabel: { UID: 5 },
|
||||
},
|
||||
{
|
||||
$class: { UID: 9 },
|
||||
NSSource: { UID: 2 },
|
||||
NSDestination: { UID: 1 },
|
||||
NSLabel: { UID: 10 },
|
||||
},
|
||||
"o_button",
|
||||
{
|
||||
$classname: "NSCustomObject",
|
||||
$classes: ["NSCustomObject", "NSObject"],
|
||||
},
|
||||
{ $classname: "NSButton", $classes: ["NSButton", "NSObject"] },
|
||||
{
|
||||
$classname: "NSNibOutletConnector",
|
||||
$classes: ["NSNibOutletConnector", "NSNibConnector", "NSObject"],
|
||||
},
|
||||
{
|
||||
$classname: "NSNibControlConnector",
|
||||
$classes: ["NSNibControlConnector", "NSNibConnector", "NSObject"],
|
||||
},
|
||||
"buttonClicked:",
|
||||
],
|
||||
$top: {},
|
||||
});
|
||||
|
||||
expect(parsed.connections).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "outlet",
|
||||
source_id: "1",
|
||||
destination_id: "2",
|
||||
label: "o_button",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "action",
|
||||
source_id: "2",
|
||||
destination_id: "1",
|
||||
label: "buttonClicked:",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("projects an AppKit action from the control through the selector to its target", () => {
|
||||
const analysis = buildInterfaceBuilderAnalysis({
|
||||
targetSha256: hash,
|
||||
toolVersion: "test",
|
||||
documents: [
|
||||
{
|
||||
relativePath: "MainMenu.nib",
|
||||
archiveSha256: hash,
|
||||
documentKind: "nib",
|
||||
raw: {
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$objects: [
|
||||
"$null",
|
||||
{ $class: { UID: 4 } },
|
||||
{ $class: { UID: 5 } },
|
||||
{
|
||||
$class: { UID: 6 },
|
||||
NSSource: { UID: 2 },
|
||||
NSDestination: { UID: 1 },
|
||||
NSLabel: "buttonClicked:",
|
||||
},
|
||||
{ $classname: "BuildDelegate", $classes: ["NSObject"] },
|
||||
{ $classname: "NSButton", $classes: ["NSButton", "NSObject"] },
|
||||
{
|
||||
$classname: "NSNibControlConnector",
|
||||
$classes: ["NSNibControlConnector", "NSObject"],
|
||||
},
|
||||
],
|
||||
$top: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
limits: interfaceBuilderLimitsSchema.parse({}),
|
||||
});
|
||||
const byId = new Map(analysis.graph.nodes.map((node) => [node.id, node]));
|
||||
const action = analysis.graph.nodes.find(({ kind }) => kind === "action");
|
||||
const from = analysis.graph.edges
|
||||
.filter(
|
||||
({ to, relation }) => relation === "target_action" && to === action?.id,
|
||||
)
|
||||
.map(({ from: id }) => byId.get(id)?.name);
|
||||
const to = analysis.graph.edges
|
||||
.filter(
|
||||
({ from: id, relation }) =>
|
||||
relation === "target_action" && id === action?.id,
|
||||
)
|
||||
.map(({ to: id }) => (id === null ? null : byId.get(id)?.kind));
|
||||
expect(from).toEqual(["NSButton"]);
|
||||
expect(to).toEqual(expect.arrayContaining(["objc_selector", "unknown"]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -216,8 +216,13 @@ const parseKeyedArchive = (
|
||||
)
|
||||
) {
|
||||
const controlAction = /ControlConnector/iu.test(className);
|
||||
// AppKit connectors (NSNibOutletConnector, NSNibControlConnector)
|
||||
// archive NSSource as the outlet owner or sending control and
|
||||
// NSDestination as the outlet value or action target, so they are read
|
||||
// in that order without the inversion the unprefixed fields use.
|
||||
const source = firstObjectReference(
|
||||
controlAction ? fields.destination : fields.source,
|
||||
archivedObjectReference(fields.NSSource),
|
||||
controlAction ? fields.to : fields.from,
|
||||
controlAction ? fields.target : fields.owner,
|
||||
);
|
||||
@@ -229,11 +234,13 @@ const parseKeyedArchive = (
|
||||
source_id: source,
|
||||
destination_id: firstObjectReference(
|
||||
controlAction ? fields.source : fields.destination,
|
||||
archivedObjectReference(fields.NSDestination),
|
||||
controlAction ? fields.from : fields.to,
|
||||
controlAction ? fields.owner : fields.target,
|
||||
),
|
||||
label: firstString(
|
||||
fields.label,
|
||||
fields.NSLabel,
|
||||
fields.selector,
|
||||
fields.identifier,
|
||||
fields.action,
|
||||
@@ -285,6 +292,18 @@ export const firstString = (...values: unknown[]): string | null => {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The node identity of a resolved archive reference: its authored object ID,
|
||||
* otherwise its archive UID, which is how the referenced object's node is keyed.
|
||||
*/
|
||||
const archivedObjectReference = (value: unknown): string | null => {
|
||||
const item = record(value);
|
||||
if (typeof item.archiveUID !== "number") return null;
|
||||
return (
|
||||
firstString(item.objectID, item["object-id"]) ?? String(item.archiveUID)
|
||||
);
|
||||
};
|
||||
|
||||
const firstObjectReference = (...values: unknown[]): string | null => {
|
||||
for (const value of values) {
|
||||
const direct = firstString(value);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type JavaScriptModuleOrigin,
|
||||
type JavaScriptSemanticDefinition,
|
||||
type JavaScriptSemanticIr,
|
||||
type JavaScriptSemanticReference,
|
||||
} from "./javascriptSemanticIr.js";
|
||||
import {
|
||||
collectSemanticModuleLink,
|
||||
@@ -71,6 +72,16 @@ export const analyzeJavaScriptSemantics = (
|
||||
: analyzeParsedJavaScriptSemantics(file);
|
||||
};
|
||||
|
||||
/** Recover lexical references, optionally for one name, without evaluating values or provenance. */
|
||||
export const analyzeParsedJavaScriptReferences = (
|
||||
file: ParsedJavaScriptSource,
|
||||
name?: string,
|
||||
): readonly JavaScriptSemanticReference[] => {
|
||||
const state = createState(file.program);
|
||||
collectDefinitions(file.program, state);
|
||||
return collectSemanticReferences(file.program, state, name);
|
||||
};
|
||||
|
||||
/** Recover semantics from an already parsed JavaScript artifact. */
|
||||
export const analyzeParsedJavaScriptSemantics = (
|
||||
file: ParsedJavaScriptSource,
|
||||
|
||||
@@ -92,16 +92,22 @@ export const collectSemanticModuleLink = (
|
||||
else if (t.isAssignmentExpression(node)) collectCommonJsExport(node, state);
|
||||
};
|
||||
|
||||
/** Collect bounded lexical identifier references after definitions exist. */
|
||||
/** Collect lexical references after definitions exist, optionally for one name. */
|
||||
export const collectSemanticReferences = (
|
||||
program: t.Program,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
name?: string,
|
||||
): JavaScriptSemanticReference[] => {
|
||||
const output: JavaScriptSemanticReference[] = [];
|
||||
const seen = new Set<string>();
|
||||
traverseJavaScriptAst(program, {
|
||||
enter: (node, parent, readAncestors) => {
|
||||
if (!t.isIdentifier(node) || parent === null) return;
|
||||
if (
|
||||
!t.isIdentifier(node) ||
|
||||
parent === null ||
|
||||
(name !== undefined && node.name !== name)
|
||||
)
|
||||
return;
|
||||
const role = semanticIdentifierRole(node, parent, readAncestors());
|
||||
if (role === null) return;
|
||||
// A compound assignment or update reads before it writes; emit both so
|
||||
|
||||
@@ -410,7 +410,7 @@ const BROWSING_CONTEXT_KEYWORDS = new Set([
|
||||
* `window.open(url, "_blank")` share the callee name. A literal method must
|
||||
* be an HTTP method; with a computed method, the second literal is a URL
|
||||
* unless it is provably an fs flag or a browsing-context keyword, or the
|
||||
* receiver is spelled as a browsing context. XHR normalizes the standard
|
||||
* receiver is spelled as a browsing context or storage. XHR normalizes the standard
|
||||
* methods' case; extension methods such as WebDAV `PROPFIND` are
|
||||
* conventionally uppercase tokens.
|
||||
*/
|
||||
@@ -422,13 +422,18 @@ const xhrOpenUrl = (
|
||||
const url = stringValue(urlNode);
|
||||
if (url === undefined) return undefined;
|
||||
const method = stringValue(methodNode);
|
||||
// IndexedDB and Cache Storage `open` take a name and a version, so a
|
||||
// receiver spelled as storage needs a standard HTTP method literal.
|
||||
const storage = storageKind(name) !== undefined;
|
||||
if (method === undefined)
|
||||
return WINDOW_OPEN_CALL.test(name) ||
|
||||
return storage ||
|
||||
WINDOW_OPEN_CALL.test(name) ||
|
||||
FS_OPEN_FLAGS.has(url) ||
|
||||
BROWSING_CONTEXT_KEYWORDS.has(url.toLowerCase())
|
||||
? undefined
|
||||
: url;
|
||||
return XHR_METHODS.has(method.toUpperCase()) || /^[A-Z][A-Z-]*$/u.test(method)
|
||||
return XHR_METHODS.has(method.toUpperCase()) ||
|
||||
(!storage && /^[A-Z][A-Z-]*$/u.test(method))
|
||||
? url
|
||||
: undefined;
|
||||
};
|
||||
|
||||
@@ -68,7 +68,7 @@ export const buildComparisonMatching = (
|
||||
exact_il_signature: methodItems.filter(
|
||||
({ match }) => match.basis === "exact-il-signature",
|
||||
).length,
|
||||
exact_signature: methodItems.filter(
|
||||
exact_signature: [...methodItems, ...fieldItems].filter(
|
||||
({ match }) => match.basis === "exact-signature",
|
||||
).length,
|
||||
structural_method_shape: methodItems.filter(
|
||||
@@ -92,6 +92,7 @@ export const comparisonLimitations = (
|
||||
const limitations: string[] = [
|
||||
"Metadata tokens are build-local coordinates; matched pairs are remaps, not persistent identities.",
|
||||
"Methods pair by exact CIL/signature, then declared type, name, and signature, then structural shape; names alone are never a matching basis.",
|
||||
"Fields pair by exact signature, then declared type, name, and signature; an undecoded signature is compared only by its raw bytes, and an unmatched member with one stays unknown.",
|
||||
];
|
||||
if (left.coverage.state !== "complete" || right.coverage.state !== "complete")
|
||||
limitations.push("At least one managed member observation is partial.");
|
||||
|
||||
@@ -28,6 +28,12 @@ interface ComparisonItemContext {
|
||||
readonly rightComplete: boolean;
|
||||
}
|
||||
|
||||
/** Declared type and name of each unpaired member, per side, whose signature was not decoded. */
|
||||
interface UndecodedOneSidedNames {
|
||||
readonly left: ReadonlySet<string>;
|
||||
readonly right: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
const methodDimensions = (
|
||||
left: Method,
|
||||
right: Method,
|
||||
@@ -113,79 +119,167 @@ const unmatchedComparison = (): UnmatchedComparison => ({
|
||||
const methodOnlyItem = (
|
||||
member: OneSided<Method>,
|
||||
context: ComparisonItemContext,
|
||||
undecoded: UndecodedOneSidedNames,
|
||||
): MethodItem => {
|
||||
if (member.left !== undefined) {
|
||||
const absenceObserved = context.rightComplete;
|
||||
const limitations = absenceLimitations(member.left, {
|
||||
oppositeComplete: context.rightComplete,
|
||||
oppositeUndecodedNames: undecoded.right,
|
||||
opposite: "right",
|
||||
inventory: "method",
|
||||
});
|
||||
return {
|
||||
item_id: `mmc_method_${sha256({ token: member.left.token, side: "left" })}`,
|
||||
status: absenceObserved ? "removed" : "unknown",
|
||||
status: limitations.length === 0 ? "removed" : "unknown",
|
||||
left: methodIdentity(member.left),
|
||||
right: null,
|
||||
match: unmatchedComparison(),
|
||||
dimensions: ["availability"],
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: absenceObserved
|
||||
? []
|
||||
: [
|
||||
"unknown-within-incomplete-metadata: The right method inventory is incomplete, so absence was not observed.",
|
||||
],
|
||||
limitations,
|
||||
};
|
||||
}
|
||||
|
||||
const absenceObserved = context.leftComplete;
|
||||
const limitations = absenceLimitations(member.right, {
|
||||
oppositeComplete: context.leftComplete,
|
||||
oppositeUndecodedNames: undecoded.left,
|
||||
opposite: "left",
|
||||
inventory: "method",
|
||||
});
|
||||
return {
|
||||
item_id: `mmc_method_${sha256({ token: member.right.token, side: "right" })}`,
|
||||
status: absenceObserved ? "added" : "unknown",
|
||||
status: limitations.length === 0 ? "added" : "unknown",
|
||||
left: null,
|
||||
right: methodIdentity(member.right),
|
||||
match: unmatchedComparison(),
|
||||
dimensions: ["availability"],
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: absenceObserved
|
||||
? []
|
||||
: [
|
||||
"unknown-within-incomplete-metadata: The left method inventory is incomplete, so absence was not observed.",
|
||||
],
|
||||
limitations,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Why a one-sided member's absence from the other inventory was not
|
||||
* observed: that inventory is incomplete, the member's own signature was not
|
||||
* decoded, so no shape round could seek a changed counterpart, or the other
|
||||
* inventory has an unpaired (one-sided or ambiguous) same-named member whose
|
||||
* signature was not decoded, which may be this member with a changed
|
||||
* signature.
|
||||
*/
|
||||
const absenceLimitations = (
|
||||
member: Method | Field,
|
||||
opposite: {
|
||||
readonly oppositeComplete: boolean;
|
||||
readonly oppositeUndecodedNames: ReadonlySet<string>;
|
||||
readonly opposite: "left" | "right";
|
||||
readonly inventory: "method" | "field";
|
||||
},
|
||||
): string[] => [
|
||||
...(opposite.oppositeComplete
|
||||
? []
|
||||
: [
|
||||
`unknown-within-incomplete-metadata: The ${opposite.opposite} ${opposite.inventory} inventory is incomplete, so absence was not observed.`,
|
||||
]),
|
||||
...(member.signature.parse_status === "decoded"
|
||||
? []
|
||||
: [
|
||||
`signature-not-decoded: The member's ${member.signature.parse_status} signature was compared only by exact declared type, name, and raw bytes, so a changed or renamed counterpart could not be sought.`,
|
||||
]),
|
||||
...(member.signature.parse_status === "decoded" &&
|
||||
opposite.oppositeUndecodedNames.has(declaredName(member))
|
||||
? [
|
||||
`counterpart-signature-not-decoded: The ${opposite.opposite} ${opposite.inventory} inventory has an unpaired member with the same declared type and name whose signature was not decoded, so it may be this member with a changed signature.`,
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const declaredName = (member: Method | Field): string =>
|
||||
JSON.stringify([member.declaring_type, member.name]);
|
||||
|
||||
/**
|
||||
* Declared names of unpaired members whose signature was not decoded: one-sided
|
||||
* members and candidates of ambiguous groups, which also remain unpaired.
|
||||
*/
|
||||
const undecodedOneSidedNames = (members: {
|
||||
readonly leftOnly: readonly { readonly item: Method | Field }[];
|
||||
readonly rightOnly: readonly { readonly item: Method | Field }[];
|
||||
readonly ambiguous: readonly {
|
||||
readonly left: readonly { readonly item: Method | Field }[];
|
||||
readonly right: readonly { readonly item: Method | Field }[];
|
||||
}[];
|
||||
}): UndecodedOneSidedNames => {
|
||||
const names = (side: readonly { readonly item: Method | Field }[]) =>
|
||||
new Set(
|
||||
side
|
||||
.filter(({ item }) => item.signature.parse_status !== "decoded")
|
||||
.map(({ item }) => declaredName(item)),
|
||||
);
|
||||
return {
|
||||
left: names([
|
||||
...members.leftOnly,
|
||||
...members.ambiguous.flatMap(({ left }) => left),
|
||||
]),
|
||||
right: names([
|
||||
...members.rightOnly,
|
||||
...members.ambiguous.flatMap(({ right }) => right),
|
||||
]),
|
||||
};
|
||||
};
|
||||
|
||||
const fieldOnlyItem = (
|
||||
member: OneSided<Field>,
|
||||
context: ComparisonItemContext,
|
||||
undecoded: UndecodedOneSidedNames,
|
||||
): FieldItem => {
|
||||
if (member.left !== undefined) {
|
||||
const absenceObserved = context.rightComplete;
|
||||
const limitations = absenceLimitations(member.left, {
|
||||
oppositeComplete: context.rightComplete,
|
||||
oppositeUndecodedNames: undecoded.right,
|
||||
opposite: "right",
|
||||
inventory: "field",
|
||||
});
|
||||
return {
|
||||
item_id: `mmc_field_${sha256({ token: member.left.token, side: "left" })}`,
|
||||
status: absenceObserved ? "removed" : "unknown",
|
||||
status: limitations.length === 0 ? "removed" : "unknown",
|
||||
left: fieldIdentity(member.left),
|
||||
right: null,
|
||||
match: unmatchedComparison(),
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: absenceObserved
|
||||
? []
|
||||
: [
|
||||
"unknown-within-incomplete-metadata: The right field inventory is incomplete, so absence was not observed.",
|
||||
],
|
||||
limitations,
|
||||
};
|
||||
}
|
||||
|
||||
const absenceObserved = context.leftComplete;
|
||||
const limitations = absenceLimitations(member.right, {
|
||||
oppositeComplete: context.leftComplete,
|
||||
oppositeUndecodedNames: undecoded.left,
|
||||
opposite: "left",
|
||||
inventory: "field",
|
||||
});
|
||||
return {
|
||||
item_id: `mmc_field_${sha256({ token: member.right.token, side: "right" })}`,
|
||||
status: absenceObserved ? "added" : "unknown",
|
||||
status: limitations.length === 0 ? "added" : "unknown",
|
||||
left: null,
|
||||
right: fieldIdentity(member.right),
|
||||
match: unmatchedComparison(),
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: absenceObserved
|
||||
? []
|
||||
: [
|
||||
"unknown-within-incomplete-metadata: The left field inventory is incomplete, so absence was not observed.",
|
||||
],
|
||||
limitations,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Name the identity key an ambiguous group shares. The exact-signature key
|
||||
* already includes the declared type and name, so names cannot separate it.
|
||||
*/
|
||||
const ambiguityLimitation = (
|
||||
members: "methods" | "fields",
|
||||
basis: MethodItem["match"]["basis"],
|
||||
): string =>
|
||||
basis === "exact-signature"
|
||||
? `Multiple managed ${members} share the same declared type, name, and raw signature; REA did not guess a token remap.`
|
||||
: members === "methods"
|
||||
? "Multiple managed methods share the same non-name identity key; REA did not guess a token remap."
|
||||
: "Multiple managed fields share the same signature; REA did not guess a token remap from names.";
|
||||
|
||||
export const buildMethodItems = (
|
||||
matches: ManagedMethodMatches,
|
||||
context: ComparisonItemContext,
|
||||
@@ -248,15 +342,14 @@ export const buildMethodItems = (
|
||||
},
|
||||
dimensions: ["availability"],
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: [
|
||||
"Multiple managed methods share the same non-name identity key; REA did not guess a token remap.",
|
||||
],
|
||||
limitations: [ambiguityLimitation("methods", ambiguous.basis)],
|
||||
});
|
||||
}
|
||||
const undecoded = undecodedOneSidedNames(matches);
|
||||
for (const item of matches.leftOnly)
|
||||
items.push(methodOnlyItem({ left: item.item }, context));
|
||||
items.push(methodOnlyItem({ left: item.item }, context, undecoded));
|
||||
for (const item of matches.rightOnly)
|
||||
items.push(methodOnlyItem({ right: item.item }, context));
|
||||
items.push(methodOnlyItem({ right: item.item }, context, undecoded));
|
||||
return items;
|
||||
};
|
||||
|
||||
@@ -306,13 +399,12 @@ export const buildFieldItems = (
|
||||
candidate_right_tokens: ambiguous.right.map(({ item }) => item.token),
|
||||
},
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: [
|
||||
"Multiple managed fields share the same signature; REA did not guess a token remap from names.",
|
||||
],
|
||||
limitations: [ambiguityLimitation("fields", ambiguous.basis)],
|
||||
});
|
||||
const undecoded = undecodedOneSidedNames(matches);
|
||||
for (const item of matches.leftOnly)
|
||||
items.push(fieldOnlyItem({ left: item.item }, context));
|
||||
items.push(fieldOnlyItem({ left: item.item }, context, undecoded));
|
||||
for (const item of matches.rightOnly)
|
||||
items.push(fieldOnlyItem({ right: item.item }, context));
|
||||
items.push(fieldOnlyItem({ right: item.item }, context, undecoded));
|
||||
return items;
|
||||
};
|
||||
|
||||
@@ -58,15 +58,13 @@ const keyMethod = (item: Method): Keyed<Method> => ({
|
||||
item.body.normalized_il_sha256,
|
||||
])
|
||||
: null,
|
||||
signatureKey:
|
||||
item.signature.parse_status === "decoded"
|
||||
? stableKey([
|
||||
"method-signature",
|
||||
item.declaring_type,
|
||||
item.name,
|
||||
item.signature.raw_sha256,
|
||||
])
|
||||
: null,
|
||||
// The raw blob is the exact signature; decoding only feeds shape keys.
|
||||
signatureKey: stableKey([
|
||||
"method-signature",
|
||||
item.declaring_type,
|
||||
item.name,
|
||||
item.signature.raw_sha256,
|
||||
]),
|
||||
structuralKey:
|
||||
item.signature.parse_status === "decoded" && item.body.status === "present"
|
||||
? stableKey([
|
||||
@@ -104,7 +102,14 @@ const keyField = (item: Field): Keyed<Field> => ({
|
||||
item.signature.parse_status === "decoded"
|
||||
? stableKey(["field-exact", item.signature.raw_sha256])
|
||||
: null,
|
||||
signatureKey: null,
|
||||
// Only an undecoded field reaches this round: a decoded field with the
|
||||
// same raw signature already met its counterpart in the exact round.
|
||||
signatureKey: stableKey([
|
||||
"field-signature",
|
||||
item.declaring_type,
|
||||
item.name,
|
||||
item.signature.raw_sha256,
|
||||
]),
|
||||
structuralKey: stableKey([
|
||||
"field-structural",
|
||||
item.signature.kind,
|
||||
@@ -143,7 +148,12 @@ const matchFields = (
|
||||
left,
|
||||
right,
|
||||
exactBasis: "field-signature",
|
||||
fallbackBases: [],
|
||||
fallbackBases: [
|
||||
{
|
||||
basis: "exact-signature",
|
||||
key: ({ signatureKey }) => signatureKey,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export type ManagedMethodMatches = ReturnType<typeof matchMethods>;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { webPageInspectionSchema } from "./browserObservation.js";
|
||||
import { analyzeJavaScriptStaticSource } from "./javascript/javascriptStaticAnalysis.js";
|
||||
import { analyzeCapturedWebBundle } from "./webBundleAnalyzer.js";
|
||||
import { webBundleAnalysisSchema } from "./webBundleAnalysis.js";
|
||||
import { createWebTextArtifact } from "./webContentArtifact.js";
|
||||
@@ -208,6 +209,30 @@ describe("web bundle static-analysis parity", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("reads storage open versions as storage rather than endpoints in both analyzers", () => {
|
||||
const source = `
|
||||
indexedDB.open(databaseName, "2");
|
||||
window.indexedDB.open(databaseName, "/v3");
|
||||
indexedDB.open("APP", "4");
|
||||
indexedDB.open("GET", "/storage-named-xhr");
|
||||
`;
|
||||
expect(
|
||||
analyzeCapturedWebBundle(inspection(source)).observations.endpoints.map(
|
||||
({ value }) => value,
|
||||
),
|
||||
).toEqual(["/storage-named-xhr"]);
|
||||
const analysis = analyzeJavaScriptStaticSource(source);
|
||||
expect(analysis.endpoints.map(({ value }) => value)).toEqual([
|
||||
"/storage-named-xhr",
|
||||
]);
|
||||
expect(analysis.storage.map(({ kind }) => kind)).toEqual([
|
||||
"indexed-db",
|
||||
"indexed-db",
|
||||
"indexed-db",
|
||||
"indexed-db",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps HTTP-method-named reads of provable keyed collections out of endpoints", () => {
|
||||
const result = analyzeCapturedWebBundle(
|
||||
inspection(`
|
||||
@@ -276,6 +301,32 @@ describe("web bundle artifact metadata", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("deep captured web bundle syntax", () => {
|
||||
it("retains evidence after a parser-admitted deep property chain", () => {
|
||||
const source = `const value = root${".next".repeat(12_000)};\nimport "./last.js";\nfetch("/after");`;
|
||||
const result = analyzeCapturedWebBundle(inspection(source));
|
||||
expect(result.completeness).toMatchObject({
|
||||
status: "complete",
|
||||
parsed_scripts: 1,
|
||||
parse_failures: 0,
|
||||
});
|
||||
expect(result.observations.chunks.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "static_import",
|
||||
specifier: "./last.js",
|
||||
resolved_url: `${origin}/assets/last.js`,
|
||||
location: expect.objectContaining({ line: 2, column: 0 }),
|
||||
}),
|
||||
);
|
||||
expect(result.observations.endpoints).toContainEqual(
|
||||
expect.objectContaining({
|
||||
value: "/after",
|
||||
location: expect.objectContaining({ line: 3, column: 0 }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const inspection = (source: string) =>
|
||||
webPageInspectionSchema.parse({
|
||||
browser: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as t from "@babel/types";
|
||||
import { sanitizeEndpointCandidate } from "./browserObservation.js";
|
||||
import type { WebPageInspection } from "./browserObservation.js";
|
||||
import type { WebBundleAnalysis } from "./webBundleAnalysis.js";
|
||||
import { traverseJavaScriptAst } from "./javascript/javascriptSemanticTraversal.js";
|
||||
import { semanticStaticPropertyName } from "./javascript/javascriptAstValues.js";
|
||||
import {
|
||||
calleeName,
|
||||
@@ -61,10 +62,11 @@ export const analyzeScript = (
|
||||
}
|
||||
accumulator.parsedScripts += 1;
|
||||
detectVendorFingerprints(script, accumulator);
|
||||
t.traverseFast(file, (node) => {
|
||||
accumulator.visitedNodes += 1;
|
||||
inspectNode(script, node, accumulator);
|
||||
return undefined;
|
||||
traverseJavaScriptAst(file, {
|
||||
enter: (node) => {
|
||||
accumulator.visitedNodes += 1;
|
||||
inspectNode(script, node, accumulator);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -47,6 +47,31 @@ describe("native module syntax observations", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("deep native module syntax", () => {
|
||||
it("retains imports and their locations after a parser-admitted deep property chain", () => {
|
||||
const source = `const value = root${".next".repeat(12_000)};\nimport "./last.js";\nexport * from "./reexport.js";\nimport("./lazy.js");`;
|
||||
const result = collectWebModuleImports(source);
|
||||
expect(result.state).toBe("parsed");
|
||||
expect(result.diagnostics).toEqual([]);
|
||||
expect(
|
||||
result.imports.map(({ kind, specifier }) => [kind, specifier]),
|
||||
).toEqual([
|
||||
["static-import", "./last.js"],
|
||||
["re-export", "./reexport.js"],
|
||||
["dynamic-import", "./lazy.js"],
|
||||
]);
|
||||
for (const item of result.imports)
|
||||
expect(source.slice(item.start.offset, item.end.offset)).toBe(
|
||||
item.expression,
|
||||
);
|
||||
expect(result.imports[0]?.start).toEqual({
|
||||
offset: source.indexOf('"./last.js"'),
|
||||
line: 2,
|
||||
column: 7,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const script = (
|
||||
url: string,
|
||||
kind: "page-script" | "scenario-response",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { traverseFast, isStringLiteral, isTemplateLiteral } from "@babel/types";
|
||||
import { isStringLiteral, isTemplateLiteral } from "@babel/types";
|
||||
import type { Node } from "@babel/types";
|
||||
import { traverseJavaScriptAst } from "./javascript/javascriptSemanticTraversal.js";
|
||||
import { parseJavaScriptSource } from "./javascript/javascriptSourceParser.js";
|
||||
import type { WebModuleImport } from "./webModuleTrace.js";
|
||||
import type { ExportedWebScript } from "./webScriptExport.js";
|
||||
@@ -51,46 +52,48 @@ export const collectWebModuleImports = (
|
||||
},
|
||||
});
|
||||
};
|
||||
traverseFast(ast, (node) => {
|
||||
if (node.type === "ImportDeclaration") {
|
||||
if (node.importKind === "type" || node.importKind === "typeof") return;
|
||||
if (
|
||||
node.specifiers.length > 0 &&
|
||||
node.specifiers.every(
|
||||
(s) =>
|
||||
s.type === "ImportSpecifier" &&
|
||||
(s.importKind === "type" || s.importKind === "typeof"),
|
||||
traverseJavaScriptAst(ast, {
|
||||
enter: (node) => {
|
||||
if (node.type === "ImportDeclaration") {
|
||||
if (node.importKind === "type" || node.importKind === "typeof") return;
|
||||
if (
|
||||
node.specifiers.length > 0 &&
|
||||
node.specifiers.every(
|
||||
(s) =>
|
||||
s.type === "ImportSpecifier" &&
|
||||
(s.importKind === "type" || s.importKind === "typeof"),
|
||||
)
|
||||
)
|
||||
)
|
||||
return;
|
||||
add("static-import", node.source);
|
||||
} else if (
|
||||
node.type === "ExportAllDeclaration" ||
|
||||
node.type === "ExportNamedDeclaration"
|
||||
) {
|
||||
if (
|
||||
node.exportKind === "type" ||
|
||||
node.source === null ||
|
||||
node.source === undefined
|
||||
)
|
||||
return;
|
||||
if (
|
||||
node.type === "ExportNamedDeclaration" &&
|
||||
node.specifiers.length > 0 &&
|
||||
node.specifiers.every(
|
||||
(s) => s.type === "ExportSpecifier" && s.exportKind === "type",
|
||||
return;
|
||||
add("static-import", node.source);
|
||||
} else if (
|
||||
node.type === "ExportAllDeclaration" ||
|
||||
node.type === "ExportNamedDeclaration"
|
||||
) {
|
||||
if (
|
||||
node.exportKind === "type" ||
|
||||
node.source === null ||
|
||||
node.source === undefined
|
||||
)
|
||||
return;
|
||||
if (
|
||||
node.type === "ExportNamedDeclaration" &&
|
||||
node.specifiers.length > 0 &&
|
||||
node.specifiers.every(
|
||||
(s) => s.type === "ExportSpecifier" && s.exportKind === "type",
|
||||
)
|
||||
)
|
||||
return;
|
||||
add("re-export", node.source);
|
||||
} else if (node.type === "ImportExpression")
|
||||
add("dynamic-import", node.source);
|
||||
else if (
|
||||
node.type === "CallExpression" &&
|
||||
node.callee.type === "Import" &&
|
||||
node.arguments[0] !== undefined
|
||||
)
|
||||
return;
|
||||
add("re-export", node.source);
|
||||
} else if (node.type === "ImportExpression")
|
||||
add("dynamic-import", node.source);
|
||||
else if (
|
||||
node.type === "CallExpression" &&
|
||||
node.callee.type === "Import" &&
|
||||
node.arguments[0] !== undefined
|
||||
)
|
||||
add("dynamic-import", node.arguments[0]);
|
||||
add("dynamic-import", node.arguments[0]);
|
||||
},
|
||||
});
|
||||
imports.sort((a, b) => a.start.offset - b.start.offset);
|
||||
return {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -123,7 +123,7 @@ export const runLinuxPrivateDisplayProbe: LinuxPrivateDisplayProbeRunner =
|
||||
cleanup: () => cleanupOwnedProcessGroup(started.ownership),
|
||||
};
|
||||
const supervisor = new ProviderProcessSupervisor(launch);
|
||||
const outcome = await waitForProbeExit(
|
||||
const outcome = await waitForProbeOutputClose(
|
||||
supervisor,
|
||||
options.timeoutMs,
|
||||
options.signal,
|
||||
@@ -300,7 +300,7 @@ const emptyProcessResult = (
|
||||
cleanupIncomplete: false,
|
||||
});
|
||||
|
||||
const waitForProbeExit = async (
|
||||
const waitForProbeOutputClose = async (
|
||||
supervisor: ProviderProcessSupervisor,
|
||||
timeoutMs: number,
|
||||
signal: AbortSignal | undefined,
|
||||
@@ -316,8 +316,8 @@ const waitForProbeExit = async (
|
||||
try {
|
||||
return await Promise.race([
|
||||
supervisor
|
||||
.waitForExit(Math.max(1, timeoutMs))
|
||||
.then((exited): "exit" | "timeout" => (exited ? "exit" : "timeout")),
|
||||
.waitForOutputClose(Math.max(1, timeoutMs))
|
||||
.then((closed): "exit" | "timeout" => (closed ? "exit" : "timeout")),
|
||||
aborted,
|
||||
]);
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { expect, it } from "vitest";
|
||||
import { runWakaruCommand } from "./WakaruCommand.js";
|
||||
import { runWakaruCommand, type WakaruLauncher } from "./WakaruCommand.js";
|
||||
import { RECOVERY_LIMITS } from "./WakaruRelease.js";
|
||||
|
||||
it("rejects an exhausted shared execution budget before launching the next command", async () => {
|
||||
await expect(
|
||||
@@ -36,3 +39,113 @@ it("preserves caller cancellation before acquiring an expired execution budget",
|
||||
}),
|
||||
).rejects.toMatchObject({ _tag: "AnalysisCancelledError" });
|
||||
});
|
||||
|
||||
const commandFixture = (
|
||||
emit: (producer: ReturnType<typeof createProducer>) => void,
|
||||
) => {
|
||||
const producer = createProducer();
|
||||
const launcher: WakaruLauncher = async (options) => {
|
||||
setImmediate(() => emit(producer));
|
||||
return {
|
||||
process: producer,
|
||||
ownership: {
|
||||
runId: options.runId,
|
||||
leaderPid: 12345,
|
||||
processGroupId: 12345,
|
||||
expectedParentPid: process.pid,
|
||||
},
|
||||
cleanup: async () => ({ cleaned: true, signaled: false }),
|
||||
};
|
||||
};
|
||||
return {
|
||||
producer,
|
||||
context: {
|
||||
command: "/selected/wakaru",
|
||||
limiter: "/usr/bin/prlimit",
|
||||
arguments: ["--version"],
|
||||
cwd: "/tmp/unused",
|
||||
environment: {},
|
||||
deadline: Date.now() + 5_000,
|
||||
launcher,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const createProducer = () =>
|
||||
Object.assign(new EventEmitter(), {
|
||||
stdout: new PassThrough(),
|
||||
stderr: new PassThrough(),
|
||||
exitCode: null,
|
||||
signalCode: null,
|
||||
kill: () => false,
|
||||
});
|
||||
|
||||
it("retains stdout and stderr delivered after launcher exit", async () => {
|
||||
const { context } = commandFixture((producer) => {
|
||||
producer.stdout.write("first");
|
||||
producer.emit("exit", 0, null);
|
||||
setImmediate(() => {
|
||||
producer.stdout.end("last");
|
||||
producer.stderr.end("diagnostic");
|
||||
producer.emit("close", 0, null);
|
||||
});
|
||||
});
|
||||
await expect(runWakaruCommand(context)).resolves.toMatchObject({
|
||||
exit_code: 0,
|
||||
signal: null,
|
||||
stdout: "firstlast",
|
||||
stderr: "diagnostic",
|
||||
});
|
||||
});
|
||||
|
||||
it("checks the diagnostic byte limit against output delivered after exit", async () => {
|
||||
const { context } = commandFixture((producer) => {
|
||||
producer.emit("exit", 0, null);
|
||||
setImmediate(() => {
|
||||
producer.stderr.end(Buffer.alloc(RECOVERY_LIMITS.reportBytes + 1, 97));
|
||||
producer.stdout.end();
|
||||
producer.emit("close", 0, null);
|
||||
});
|
||||
});
|
||||
await expect(runWakaruCommand(context)).rejects.toMatchObject({
|
||||
_tag: "AnalysisOutputError",
|
||||
operation: "recover_javascript_sources",
|
||||
});
|
||||
});
|
||||
|
||||
it("enforces the shared deadline while exited output streams remain open", async () => {
|
||||
const { context, producer } = commandFixture((process) => {
|
||||
process.emit("exit", 0, null);
|
||||
});
|
||||
try {
|
||||
await expect(
|
||||
runWakaruCommand({ ...context, deadline: Date.now() + 50 }),
|
||||
).rejects.toMatchObject({
|
||||
_tag: "AnalysisTimeoutError",
|
||||
operation: "recover_javascript_sources",
|
||||
});
|
||||
expect(producer.stderr.listenerCount("data")).toBe(0);
|
||||
} finally {
|
||||
producer.stdout.destroy();
|
||||
producer.stderr.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it("honors cancellation while waiting for output after exit", async () => {
|
||||
const controller = new AbortController();
|
||||
const { context, producer } = commandFixture((process) => {
|
||||
process.emit("exit", 0, null);
|
||||
setImmediate(() => controller.abort());
|
||||
});
|
||||
try {
|
||||
await expect(
|
||||
runWakaruCommand({ ...context, signal: controller.signal }),
|
||||
).rejects.toMatchObject({
|
||||
_tag: "AnalysisCancelledError",
|
||||
});
|
||||
expect(producer.stderr.listenerCount("data")).toBe(0);
|
||||
} finally {
|
||||
producer.stdout.destroy();
|
||||
producer.stderr.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -132,7 +132,7 @@ export const runWakaruCommand = async (context: WakaruCommandContext) => {
|
||||
);
|
||||
let failure: unknown;
|
||||
try {
|
||||
while (!(await supervisor.waitForExit(100))) {
|
||||
while (!(await supervisor.waitForOutputClose(100))) {
|
||||
checkRecoveryDeadline(context.deadline, context.signal);
|
||||
if (oversized)
|
||||
throw new AnalysisOutputError(
|
||||
|
||||
@@ -658,3 +658,52 @@ describe("source-map original artifact language metadata", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("deep source-map original syntax", () => {
|
||||
it("retains valid source maps and native/CommonJS evidence after a parser-admitted deep property chain", async () => {
|
||||
const source = `const value = root${".next".repeat(12_000)};\nimport "./last.js";\nrequire("./common.js");\nfunction local(require) { require("./shadowed.js"); }`;
|
||||
const text = JSON.stringify({
|
||||
version: 3,
|
||||
names: [],
|
||||
sources: ["../src/main.js"],
|
||||
sourcesContent: [source],
|
||||
mappings: "AAAA",
|
||||
});
|
||||
const result = await fetchWebSourceMaps([request], input(), undefined, {
|
||||
fetch: async () => new Response(text),
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
status: "included",
|
||||
items: [
|
||||
{
|
||||
status: "included",
|
||||
limitation: null,
|
||||
artifact: { text },
|
||||
original_sources: [{ artifact: { text: source } }],
|
||||
original_module_edges: [
|
||||
{
|
||||
from_source: `${origin}/src/main.js`,
|
||||
kind: "static_import",
|
||||
specifier: "./last.js",
|
||||
resolved_source: `${origin}/src/last.js`,
|
||||
},
|
||||
{
|
||||
from_source: `${origin}/src/main.js`,
|
||||
kind: "require",
|
||||
specifier: "./common.js",
|
||||
resolved_source: `${origin}/src/common.js`,
|
||||
},
|
||||
],
|
||||
mappings: [
|
||||
{
|
||||
generated_line: 1,
|
||||
generated_column: 0,
|
||||
original_line: 1,
|
||||
original_column: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,6 +77,17 @@ const statusCapability = (
|
||||
});
|
||||
|
||||
describe("server and catalog identity", () => {
|
||||
it("retains all catalog identity fields through transport serialization and detached clones", () => {
|
||||
const serialized = JSON.parse(JSON.stringify(CATALOG_IDENTITY));
|
||||
expect(structuredClone(CATALOG_IDENTITY)).toEqual(serialized);
|
||||
expect(serialized.digests).toEqual(CATALOG_IDENTITY.digests);
|
||||
expect(serialized.tools).toHaveLength(TOOL_CONTRACTS.length);
|
||||
const identity = createServerIdentity({
|
||||
startedAt: "2026-07-13T00:00:00.000Z",
|
||||
});
|
||||
expect(JSON.parse(JSON.stringify(identity)).catalog).toEqual(serialized);
|
||||
});
|
||||
|
||||
it("derives package and SDK versions from canonical package metadata", async () => {
|
||||
const packageJson = JSON.parse(await readFile("package.json", "utf8"));
|
||||
const packageLock = JSON.parse(await readFile("package-lock.json", "utf8"));
|
||||
|
||||
@@ -220,3 +220,24 @@ describe("Linux private display selection", () => {
|
||||
expect(processes).not.toContain(hangingHelperPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe("private display probe output settlement", () => {
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"retains inherited diagnostics written after the launcher exits",
|
||||
async () => {
|
||||
const result = await runLinuxPrivateDisplayProbe("direct", {
|
||||
helperPath: fileURLToPath(
|
||||
new URL("../../../fixtures/x11ProbeOutput.py", import.meta.url),
|
||||
),
|
||||
timeoutMs: 5_000,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
outcome: "exited",
|
||||
exitCode: 0,
|
||||
stderr: "firstlast",
|
||||
stderrBytes: Buffer.byteLength("firstlast"),
|
||||
cleanupIncomplete: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -251,6 +251,152 @@ describe("managed member comparison uncertainty", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed member comparison of undecoded signatures", () => {
|
||||
it("pairs undecoded signatures by exact declared type, name, and raw bytes", () => {
|
||||
const undecoded = {
|
||||
methodSignature: Buffer.from([0x00, 0x00, 0x7f]),
|
||||
fieldSignature: Buffer.from([0x06, 0x15]),
|
||||
};
|
||||
const left = inspect(
|
||||
buildManagedPeFixture(undecoded),
|
||||
"/tmp/undecoded-left.dll",
|
||||
);
|
||||
const right = inspect(
|
||||
buildManagedPeFixture(undecoded),
|
||||
"/tmp/undecoded-right.dll",
|
||||
);
|
||||
expect(left.result.coverage.state).toBe("complete");
|
||||
expect(left.result.methods[0]?.signature.parse_status).toBe("unsupported");
|
||||
expect(left.result.fields[0]?.signature.parse_status).toBe("unsupported");
|
||||
const result = compareManagedMembers(left, right);
|
||||
expect(result.summary).toMatchObject({
|
||||
unchanged: 2,
|
||||
added: 0,
|
||||
removed: 0,
|
||||
unknown: 0,
|
||||
});
|
||||
for (const item of [...result.methods, ...result.fields])
|
||||
expect(item.match).toMatchObject({
|
||||
status: "matched",
|
||||
basis: "exact-signature",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps unmatched undecoded members unknown in complete inventories", () => {
|
||||
const left = inspect(
|
||||
buildManagedPeFixture({
|
||||
methodSignature: Buffer.from([0x00, 0x00, 0x7f]),
|
||||
fieldSignature: Buffer.from([0x06, 0x15]),
|
||||
}),
|
||||
"/tmp/undecoded-before.dll",
|
||||
);
|
||||
const right = inspect(
|
||||
buildManagedPeFixture({
|
||||
methodSignature: Buffer.from([0x00, 0x00, 0x7e]),
|
||||
fieldSignature: Buffer.from([0x06, 0x7e]),
|
||||
}),
|
||||
"/tmp/undecoded-after.dll",
|
||||
);
|
||||
expect(right.result.coverage.state).toBe("complete");
|
||||
const result = compareManagedMembers(left, right);
|
||||
expect(result.summary).toMatchObject({ added: 0, removed: 0, unknown: 4 });
|
||||
for (const item of [...result.methods, ...result.fields])
|
||||
expect(item).toMatchObject({
|
||||
status: "unknown",
|
||||
match: { status: "unmatched" },
|
||||
limitations: [expect.stringContaining("signature-not-decoded")],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a decoded member unknown beside a same-named undecoded counterpart", () => {
|
||||
const left = inspect(buildManagedPeFixture(), "/tmp/decoded-before.dll");
|
||||
const right = inspect(
|
||||
buildManagedPeFixture({
|
||||
methodSignature: Buffer.from([0x00, 0x00, 0x7f]),
|
||||
fieldSignature: Buffer.from([0x06, 0x15]),
|
||||
}),
|
||||
"/tmp/undecoded-after.dll",
|
||||
);
|
||||
expect(left.result.methods[0]?.signature.parse_status).toBe("decoded");
|
||||
expect(left.result.fields[0]?.signature.parse_status).toBe("decoded");
|
||||
expect(right.result.coverage.state).toBe("complete");
|
||||
const result = compareManagedMembers(left, right);
|
||||
expect(result.summary).toMatchObject({ added: 0, removed: 0, unknown: 4 });
|
||||
for (const item of [...result.methods, ...result.fields])
|
||||
expect(item).toMatchObject({
|
||||
status: "unknown",
|
||||
match: { status: "unmatched" },
|
||||
limitations: [
|
||||
expect.stringMatching(
|
||||
item.left === null
|
||||
? /^signature-not-decoded:/
|
||||
: /^counterpart-signature-not-decoded: The right (method|field) inventory/,
|
||||
),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed member comparison of ambiguous undecoded signatures", () => {
|
||||
it("keeps a decoded member unknown beside an ambiguous same-named undecoded group", () => {
|
||||
const undecoded = { methodSignature: Buffer.from([0x00, 0x00, 0x7f]) };
|
||||
const decoded = inspect(buildManagedPeFixture(), "/tmp/decoded.dll");
|
||||
const left = inspect(buildManagedPeFixture(undecoded), "/tmp/left.dll");
|
||||
const right = inspect(buildManagedPeFixture(undecoded), "/tmp/right.dll");
|
||||
const [undecodedMethod] = left.result.methods;
|
||||
const [decodedMethod] = decoded.result.methods;
|
||||
if (undecodedMethod === undefined || decodedMethod === undefined)
|
||||
throw new Error("Expected fixture methods");
|
||||
expect(decodedMethod.name).toBe(undecodedMethod.name);
|
||||
// One undecoded T.M on the left meets two identical undecoded T.M on the
|
||||
// right, so that group stays ambiguous; the left also has a decoded T.M.
|
||||
const result = compareManagedMembers(
|
||||
{
|
||||
...left,
|
||||
result: {
|
||||
...left.result,
|
||||
methods: [undecodedMethod, { ...decodedMethod, token: "0x06000002" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
...right,
|
||||
result: {
|
||||
...right.result,
|
||||
methods: [
|
||||
undecodedMethod,
|
||||
{ ...undecodedMethod, token: "0x06000002" },
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.methods).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
status: "unknown",
|
||||
match: expect.objectContaining({
|
||||
status: "ambiguous",
|
||||
basis: "exact-signature",
|
||||
}),
|
||||
limitations: [
|
||||
"Multiple managed methods share the same declared type, name, and raw signature; REA did not guess a token remap.",
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
status: "unknown",
|
||||
left: expect.objectContaining({ token: "0x06000002" }),
|
||||
limitations: [
|
||||
expect.stringMatching(
|
||||
/^counterpart-signature-not-decoded: The right method inventory has an unpaired member/u,
|
||||
),
|
||||
],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(result.summary).toMatchObject({ removed: 0, added: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
const inspect = (bytes: Buffer, path: string) => {
|
||||
const target = managedPeFixtureTarget(bytes, path);
|
||||
const result = inspectManagedMembersBytes(bytes, target);
|
||||
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
sys.stderr.write("first")
|
||||
sys.stderr.flush()
|
||||
subprocess.Popen(
|
||||
[sys.executable, "-c", "import sys, time; time.sleep(0.05); sys.stderr.write('last'); sys.stderr.flush()"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
)
|
||||
+42
-5
@@ -1,6 +1,6 @@
|
||||
# REA website
|
||||
|
||||
An English static website with explanatory figures and a DX-Ball investigation.
|
||||
An English static website with explanatory figures, worked guides and a DX-Ball investigation.
|
||||
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.
|
||||
@@ -20,6 +20,8 @@ Open <http://127.0.0.1:4173/>. Refresh the browser after editing a file.
|
||||
- `public/index.html`: product introduction and DX-Ball overview.
|
||||
- `public/showcase/dx-ball/index.html`: sound-pan investigation and project status.
|
||||
- `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.
|
||||
- `public/assets/`: shared styles, interactions, favicon and explanatory figures.
|
||||
|
||||
Navigation and assets use relative paths, so the same files work at the local
|
||||
@@ -29,7 +31,18 @@ root and a GitHub Pages project path such as `/rea/`.
|
||||
|
||||
Keep the copy direct and specific. Explain the task and the result before listing
|
||||
tool names. Setup commands and runtime requirements should match the released
|
||||
package.
|
||||
package. Keep the core learning path inside the site. Source, issue and evidence
|
||||
reference links may point to the corresponding repositories.
|
||||
|
||||
[evidence/guide-examples.md](evidence/guide-examples.md) records the published
|
||||
REA package, example digests and observed results behind the guides. The Notes
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
DX-Ball figures and findings refer to the linked 7 October 2026 checkpoint,
|
||||
commit `a55dca27ec0a07018c1b2c95ae2be027f7d8c3c4`. Update those links and figures
|
||||
@@ -41,23 +54,47 @@ Evidence. [evidence/dx-ball-sound-pan.md](evidence/dx-ball-sound-pan.md) records
|
||||
their provenance and the scope of the validation claims. The original executable
|
||||
and complete private Evidence records are not website assets.
|
||||
|
||||
The diagrams are maintained as SVG source. Initial layout references were
|
||||
The homepage and DX-Ball overview diagrams are maintained as SVG source. Initial layout references were
|
||||
created with the built-in image generation tool; [figures.md](figures.md) retains
|
||||
their prompts and the current asset notes. Figures provide an overview;
|
||||
REA requests, assembly and C remain selectable HTML text. On narrow
|
||||
screens, the diagrams scroll horizontally and can also be opened at full size.
|
||||
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.
|
||||
|
||||
## GitHub Pages
|
||||
|
||||
`.github/workflows/website-pages.yml` prepares and deploys only `website/public`.
|
||||
It is manually triggered and restricted to `main`; ordinary pushes and pull
|
||||
requests do not publish the site.
|
||||
It is the sole Pages publisher, manually triggered and restricted to `main`;
|
||||
ordinary pushes and pull requests do not publish the site.
|
||||
|
||||
Website checks run only for pull requests that change `website/`, the verification
|
||||
script or workflow definitions. They check local links, HTML fragments, SVG XML
|
||||
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/verify-website.py
|
||||
```
|
||||
|
||||
`.github/workflows/pages.yml` is a separate, manual-only VitePress build. It
|
||||
has no Pages artifact upload, deployment job or deployment permissions. This
|
||||
prevents documentation updates from replacing the public website. The legacy
|
||||
publishing job has been removed, so enabling this build workflow cannot publish
|
||||
the old site.
|
||||
|
||||
After the site is approved and merged, select **GitHub Actions** under the
|
||||
repository's **Settings → Pages → Build and deployment**. Then run **Publish REA
|
||||
website** from the Actions tab on `main`. The workflow uses the `github-pages`
|
||||
environment and the official Pages actions.
|
||||
|
||||
The equivalent CLI command is:
|
||||
|
||||
```sh
|
||||
gh workflow run website-pages.yml --repo morluto/rea --ref main
|
||||
```
|
||||
|
||||
Local development does not change Pages settings or run the deployment workflow.
|
||||
Any environment protection rules are configured separately when publication is
|
||||
approved.
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Guide example evidence
|
||||
|
||||
The native guide reuses the recorded DX-Ball observations in
|
||||
[dx-ball-sound-pan.md](dx-ball-sound-pan.md). No new native-provider claim is
|
||||
introduced by this iteration.
|
||||
|
||||
## Notes Electron example
|
||||
|
||||
This six-file, source-owned teaching fixture is in
|
||||
`website/public/examples/notes-electron/`. It is a static analysis target, not
|
||||
an installed Electron distribution. Its graph was obtained using the released
|
||||
`rea-agents` 4.1.0 package, Node.js 24.18.0 on Linux x64, on 7 October 2026:
|
||||
|
||||
```sh
|
||||
rea analyze-javascript-application \
|
||||
/absolute/path/to/website/public/examples/notes-electron --json
|
||||
```
|
||||
|
||||
- Evidence ID: `ev_a7e68cbdbd52ee15a959047774d1164d385e3e5ceb704084395639d97fee093d`.
|
||||
- Root artifact SHA-256: `dae5292f341b4f0dfd5be0846d4cfa0d4821f3e062e970cc1cd568c18c3fb37c`.
|
||||
- Four JavaScript files parsed, zero parse failures.
|
||||
- One exposed API member, one literal IPC channel, one renderer invocation,
|
||||
one main-process handler, one paired invocation, zero ambiguous invocations.
|
||||
- The guide's result excerpt comes from `normalized_result.summary.ipc`.
|
||||
- The `exposes`, `invokes` and `handles` graph edges retain their static
|
||||
inference authority and source locations. The guide's end-to-end explanation
|
||||
also uses the visible source in `renderer.js`, `main.js` and `csv.js`; it does
|
||||
not claim that the complete button-to-file path was observed executing.
|
||||
|
||||
The source was formatted before this result was recorded. Re-run analysis
|
||||
when the example files change.
|
||||
|
||||
## Notes browser example
|
||||
|
||||
The three-file browser app is in `website/public/examples/notes-web/`.
|
||||
A dedicated headless Chrome profile opened it at
|
||||
`http://127.0.0.1:4173/examples/notes-web/`. REA 4.1.0 passively inspected that
|
||||
existing page with an 8-second window, accessibility text and JSON body shapes.
|
||||
A separate Playwright controller clicked the button during the capture and
|
||||
saved the resulting CSV; REA did not perform the click.
|
||||
|
||||
- Evidence ID: `ev_7df243651c257e41f525e4225cbca5427467cf1e7b4b5b624ab41ab5717fe9c6`.
|
||||
- Window: `2026-10-07T09:38:43.492Z` to
|
||||
`2026-10-07T09:38:51.531Z`.
|
||||
- One observed `GET` to `/examples/notes-web/notes.json`.
|
||||
- Status `200`, MIME type `application/json`, script initiator `export.js`.
|
||||
- Body shape: `/notes` array, `/notes/*/id` number, `/notes/*/title` string.
|
||||
- The independent browser run downloaded `notes.csv` and displayed
|
||||
“Exported 2 notes to notes.csv.” CSV construction is explained from source,
|
||||
not inferred solely from the request capture.
|
||||
|
||||
The CLI guide uses a 10-second window to give the reader time to perform the
|
||||
action. Capture only covers requests after attachment. The example request
|
||||
path differs under a project prefix such as `/rea/`, while the relative fetch
|
||||
and export behavior stay the same.
|
||||
|
||||
The guide's follow-up CLI commands were also exercised against the real page:
|
||||
`list-browser-targets` returned its exact page target, and
|
||||
`inspect-web-page --include-script-sources` returned the loaded `export.js`
|
||||
source. Discovery Evidence: `ev_31abb33e7707d47625ac909ab9256c705d03a31ee6bcd643fa96730e72469162`. Script capture
|
||||
Evidence: `ev_aeb777df405c09dc1bbdd4c185a5af2452abaa3cb493683c296c5afa97efbc11`.
|
||||
|
||||
Full local outputs and the browser verification harness are under
|
||||
`/tmp/rea-website-guides/` in the development environment, not website assets.
|
||||
|
||||
## Example source digests
|
||||
|
||||
| File | SHA-256 |
|
||||
| -------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `examples/notes-electron/csv.js` | `6875aeecc1d3e6c1ff5b888a199c26b377d812d5b10e5fb4025e192a206b0018` |
|
||||
| `examples/notes-electron/index.html` | `a31df9b29eb9d479f913c7806364609019eedd1d19cda386cc24706d5068082d` |
|
||||
| `examples/notes-electron/main.js` | `cc5b00911bfe428754d94ca40e8c0f354cfaae86f716873423aa5a62b9e0379a` |
|
||||
| `examples/notes-electron/package.json` | `bfef2a7cba39a989715f56e304ca2578807159f28bdb040b75893bb341caaee7` |
|
||||
| `examples/notes-electron/preload.js` | `3cb814f2a0822c2fef437ba722374a2eabf6c6795905f41656d9c33fe8d5d9c4` |
|
||||
| `examples/notes-electron/renderer.js` | `b24aa22ed969c0849dd9da1102935a6b36b96bae46870234bdea10039dd9a5a0` |
|
||||
| `examples/notes-web/export.js` | `66f5c2501bfca72eb743a1fffbdce9a58d295e69ca66f19562e9b0fa9fc189a6` |
|
||||
| `examples/notes-web/index.html` | `97bb4729dcd838de8adae7a00eb49df0dbe8a7b73becc1c7d2a6662825013064` |
|
||||
| `examples/notes-web/notes.json` | `9fc1155e251cfbed09dadca6b195507072fffe3f602564812d644ab27c422282` |
|
||||
@@ -55,3 +55,22 @@ 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();
|
||||
|
||||
@@ -707,6 +707,209 @@ pre code {
|
||||
.small-heading {
|
||||
margin-top: 1.8rem;
|
||||
}
|
||||
.agent-terminal {
|
||||
margin-block: 1.5rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid #303d4a;
|
||||
border-radius: 6px;
|
||||
background: #202933;
|
||||
color: #f1f4f8;
|
||||
}
|
||||
.terminal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.65rem 1rem;
|
||||
border-bottom: 1px solid #384551;
|
||||
color: #bdc8d4;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.terminal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
.terminal-dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.terminal-dots i {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #81909e;
|
||||
}
|
||||
.agent-terminal .copy-button {
|
||||
color: #d6dee7;
|
||||
}
|
||||
.agent-terminal .copy-button:hover {
|
||||
background: #384551;
|
||||
color: white;
|
||||
}
|
||||
.agent-terminal .terminal-prompt {
|
||||
margin: 0;
|
||||
padding: 1.4rem;
|
||||
color: #f1f4f8;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.9;
|
||||
}
|
||||
.prompt-symbol {
|
||||
color: #9bbfe5;
|
||||
margin-right: 0.65rem;
|
||||
}
|
||||
.terminal-cursor {
|
||||
display: inline-block;
|
||||
width: 0.5em;
|
||||
height: 1em;
|
||||
margin-left: 0.4em;
|
||||
vertical-align: -0.12em;
|
||||
background: #9bbfe5;
|
||||
}
|
||||
.agent-terminal.is-visible .terminal-cursor {
|
||||
animation: terminal-blink 1s step-end 6;
|
||||
}
|
||||
@keyframes terminal-blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.workflow-figure {
|
||||
margin: 1.7rem 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 5px;
|
||||
padding: 1.4rem;
|
||||
background: #fafbfc;
|
||||
}
|
||||
.figure-title {
|
||||
margin: 0 0 1.3rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
.flow-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1.9rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.flow-steps li {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.flow-steps li:not(:last-child)::after {
|
||||
content: "→";
|
||||
position: absolute;
|
||||
right: -1.4rem;
|
||||
top: 2.1rem;
|
||||
color: #8191a2;
|
||||
}
|
||||
.flow-label {
|
||||
display: block;
|
||||
margin-bottom: 0.65rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.flow-node {
|
||||
display: block;
|
||||
padding: 0.8rem;
|
||||
background: white;
|
||||
border: 1px solid #ced9e5;
|
||||
border-radius: 4px;
|
||||
color: var(--accent);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
}
|
||||
.flow-note {
|
||||
display: block;
|
||||
margin-top: 0.65rem;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.55;
|
||||
color: var(--muted);
|
||||
}
|
||||
.flow-result {
|
||||
margin: 1.3rem 0 0;
|
||||
padding: 0.9rem 1rem;
|
||||
border-left: 2px solid var(--accent);
|
||||
background: #edf2f8;
|
||||
color: var(--ink);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.workflow-figure figcaption {
|
||||
margin-top: 1rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.guide-index {
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
.guide-entry {
|
||||
display: grid;
|
||||
grid-template-columns: 160px minmax(0, 1fr);
|
||||
gap: 2.5rem;
|
||||
padding-block: 2.5rem;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.guide-entry .eyebrow {
|
||||
margin: 0;
|
||||
}
|
||||
.guide-entry h2 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
.guide-entry p {
|
||||
max-width: 660px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.guide-route {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.guide-route span {
|
||||
padding: 0.2rem 0.55rem;
|
||||
background: var(--soft);
|
||||
border-radius: 3px;
|
||||
color: #414b55;
|
||||
}
|
||||
.example-files {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.65rem 1.25rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.example-files li + li {
|
||||
margin: 0;
|
||||
}
|
||||
.example-app {
|
||||
max-width: 720px;
|
||||
padding-block: 3.5rem;
|
||||
}
|
||||
.example-app h1 {
|
||||
font-size: 3rem;
|
||||
}
|
||||
.example-notes {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
margin-block: 2rem;
|
||||
}
|
||||
.example-notes li {
|
||||
padding-block: 0.8rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.example-status {
|
||||
margin-block: 1.5rem 1rem;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
@@ -737,6 +940,17 @@ pre code {
|
||||
.rea-query-pair {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.flow-steps {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
.flow-steps li:not(:last-child)::after {
|
||||
content: "↓";
|
||||
top: auto;
|
||||
bottom: -1.7rem;
|
||||
right: auto;
|
||||
left: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@@ -827,6 +1041,13 @@ pre code {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.guide-entry {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
.workflow-figure {
|
||||
padding: 1rem;
|
||||
}
|
||||
.step-selector {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
@@ -839,6 +1060,12 @@ pre code {
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.agent-terminal.is-visible .terminal-cursor {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.header-inner {
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
function toCsv(notes) {
|
||||
const quote = (value) => `"${String(value).replaceAll('"', '""')}"`;
|
||||
return (
|
||||
[
|
||||
"id,title",
|
||||
...notes.map((note) => `${note.id},${quote(note.title)}`),
|
||||
].join("\n") + "\n"
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { toCsv };
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Notes — Electron analysis example</title>
|
||||
<script src="renderer.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Notes</h1>
|
||||
<button id="export" type="button">Export notes</button>
|
||||
<p id="status" role="status"></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
const { app, BrowserWindow, ipcMain } = require("electron");
|
||||
const { writeFileSync } = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { toCsv } = require("./csv.js");
|
||||
|
||||
const notes = [{ id: 1, title: "First note" }];
|
||||
|
||||
ipcMain.handle("notes:export", () => {
|
||||
const destination = path.join(app.getPath("downloads"), "notes.csv");
|
||||
writeFileSync(destination, toCsv(notes), "utf8");
|
||||
return destination;
|
||||
});
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const window = new BrowserWindow({
|
||||
webPreferences: { preload: path.join(__dirname, "preload.js") },
|
||||
});
|
||||
window.loadFile("index.html");
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "rea-notes-example",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "main.js"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
contextBridge.exposeInMainWorld("notes", {
|
||||
exportCsv: () => ipcRenderer.invoke("notes:export"),
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
document.querySelector("#export").addEventListener("click", async () => {
|
||||
const destination = await window.notes.exportCsv();
|
||||
document.querySelector("#status").textContent = `Saved to ${destination}`;
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
document.querySelector("#export").addEventListener("click", async () => {
|
||||
const status = document.querySelector("#status");
|
||||
try {
|
||||
const response = await fetch("./notes.json");
|
||||
if (!response.ok)
|
||||
throw new Error(`Notes request failed: ${response.status}`);
|
||||
const { notes } = await response.json();
|
||||
const quote = (value) => `"${String(value).replaceAll('"', '""')}"`;
|
||||
const csv =
|
||||
[
|
||||
"id,title",
|
||||
...notes.map((note) => `${note.id},${quote(note.title)}`),
|
||||
].join("\n") + "\n";
|
||||
const url = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "notes.csv";
|
||||
link.click();
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
status.textContent = `Exported ${notes.length} notes to notes.csv.`;
|
||||
} catch (error) {
|
||||
status.textContent = error.message;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Notes — REA browser example</title>
|
||||
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../../assets/styles.css" />
|
||||
<script src="export.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<main class="container example-app">
|
||||
<p class="breadcrumb">
|
||||
<a href="../../guides/browser/">Browser observation guide</a> / Example
|
||||
</p>
|
||||
<p class="eyebrow">A small app to inspect</p>
|
||||
<h1>Notes</h1>
|
||||
<p class="lede">
|
||||
Two notes. One CSV export. Observe what happens when you click.
|
||||
</p>
|
||||
<ul class="example-notes">
|
||||
<li>First note</li>
|
||||
<li>Follow the export</li>
|
||||
</ul>
|
||||
<button class="button" type="button" id="export">Export notes</button>
|
||||
<p class="example-status" id="status" role="status" aria-live="polite">
|
||||
Ready to export.
|
||||
</p>
|
||||
<p class="caption">
|
||||
This example loads <code>notes.json</code> and creates a CSV in your
|
||||
browser.
|
||||
</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"notes": [
|
||||
{ "id": 1, "title": "First note" },
|
||||
{ "id": 2, "title": "Follow the export" }
|
||||
]
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../showcase/dx-ball/">DX-Ball</a
|
||||
><a class="nav-start" href="./" aria-current="page">Get started</a
|
||||
><a href="https://github.com/morluto/rea/tree/main/docs">Docs</a
|
||||
><a href="../guides/">Guides</a
|
||||
><a href="https://github.com/morluto/rea">GitHub</a>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -39,10 +39,6 @@
|
||||
</p>
|
||||
<p class="requirements">
|
||||
You'll need Node.js 22 (22.19+), 24 (24.11+) or 26+, and npm.
|
||||
<a
|
||||
href="https://github.com/morluto/rea/blob/main/docs/installation.md"
|
||||
>Full installation guide</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="article-layout">
|
||||
@@ -59,11 +55,9 @@
|
||||
<section class="article-section" id="agent">
|
||||
<h2>With your agent</h2>
|
||||
<p>
|
||||
Run setup to connect REA to Claude Code, Codex, Cursor or another
|
||||
<a
|
||||
href="https://github.com/morluto/rea/blob/main/docs/installation.md#supported-agents"
|
||||
>supported client</a
|
||||
>.
|
||||
Run setup to connect REA to your coding agent. Client choices
|
||||
include Claude Code, Codex, Cursor, Gemini CLI, Windsurf and other
|
||||
supported MCP clients.
|
||||
</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
@@ -154,8 +148,7 @@
|
||||
<pre><code id="install-cli">npm install --global rea-agents</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
<a
|
||||
href="https://github.com/morluto/rea/blob/main/docs/javascript-application-workflows.md"
|
||||
<a href="../guides/javascript/"
|
||||
>Continue with the JavaScript application guide</a
|
||||
>
|
||||
</p>
|
||||
@@ -164,47 +157,111 @@
|
||||
<section class="article-section" id="native">
|
||||
<h2>Working with native binaries</h2>
|
||||
<p>
|
||||
Choose a deep-analysis provider for your target and host. If you
|
||||
already use one of these tools, start with its connection guide.
|
||||
REA connects your agent to a local analysis provider. Choose the
|
||||
tool you already use, configure its connection, then ask about
|
||||
your executable or library.
|
||||
</p>
|
||||
<div class="provider-list">
|
||||
<div class="provider">
|
||||
<h3>Ghidra</h3>
|
||||
<div>
|
||||
<p>
|
||||
Use an existing Ghidra installation and JDK. Setup checks
|
||||
their paths and records them for your selected clients.
|
||||
Use an existing Ghidra 12.1.x installation and a 64-bit JDK
|
||||
21 or newer on Linux x64 or macOS x64/arm64. Set the paths
|
||||
before running setup:
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/morluto/rea/blob/main/docs/installation.md#ghidra"
|
||||
>Ghidra setup and supported hosts</a
|
||||
>
|
||||
<details class="evidence-details">
|
||||
<summary>Connect your Ghidra installation</summary>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>Ghidra connection · macOS / Linux</span
|
||||
><button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="ghidra-connect"
|
||||
aria-label="Copy ghidra connection · macos / linux"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre><code id="ghidra-connect">export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC
|
||||
export JAVA_HOME=/absolute/path/to/jdk-21
|
||||
npx -y rea-agents@latest doctor --provider ghidra --json
|
||||
npx rea-agents@latest setup</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Replace both paths with your existing installations. Setup
|
||||
checks them and records valid settings for your selected
|
||||
client. On macOS, use a Ghidra package with the native
|
||||
decompiler matching your host architecture.
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider">
|
||||
<h3>Hopper</h3>
|
||||
<div>
|
||||
<p>
|
||||
Connect Hopper on macOS or Linux. Setup can also install it
|
||||
after you review and approve the installation plan.
|
||||
Connect Hopper on macOS or Linux. Run the setup command
|
||||
above; it detects an existing installation or offers an
|
||||
installation plan for your approval.
|
||||
</p>
|
||||
<p>
|
||||
On macOS, complete Hopper's first-run choice of demo mode or
|
||||
license activation before an unattended analysis.
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper"
|
||||
>Hopper installation</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider">
|
||||
<h3>IDA</h3>
|
||||
<div>
|
||||
<p>
|
||||
Reuse an existing IDA MCP registration for read-only
|
||||
analysis through an attached GUI or headless database.
|
||||
The verified IDA setups use Windows, with an existing MCP
|
||||
registration for an attached GUI or a headless database.
|
||||
Linux and macOS headless analysis remains unverified.
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/morluto/rea/blob/main/docs/ida-provider.md"
|
||||
>IDA connection guide</a
|
||||
>
|
||||
<details class="evidence-details">
|
||||
<summary>Connect an existing IDA MCP registration</summary>
|
||||
<p>
|
||||
Keep the upstream server's working <code>command</code>,
|
||||
<code>args</code> and optional <code>env</code> in a JSON
|
||||
file. An existing <code>mcpServers</code> object with an
|
||||
<code>ida-pro-mcp</code> entry is also accepted. Point REA
|
||||
at that file before setup:
|
||||
</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>IDA connection · Windows PowerShell</span
|
||||
><button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="ida-connect"
|
||||
aria-label="Copy IDA connection for Windows PowerShell"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre><code id="ida-connect">$env:REA_IDA_MCP_CONFIG = "C:/analysis/ida-mcp.json"
|
||||
$env:REA_ANALYSIS_PROVIDER = "ida"
|
||||
npx -y rea-agents@latest doctor --provider ida --json
|
||||
npx rea-agents@latest setup</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
The attached profile uses upstream legacy 1.4.0 tools with
|
||||
the original binary already open in IDA. The headless
|
||||
profile uses the upstream database-supervisor API and a
|
||||
registration with <code>"mode": "headless"</code>; real
|
||||
verification covers an IDA 9.3 Windows x64 supervisor.
|
||||
Doctor checks the registration without starting IDA.
|
||||
</p>
|
||||
<p>
|
||||
Give REA the original executable path, with
|
||||
<code>--provider ida</code> in a CLI query. For an agent,
|
||||
include <code>REA_IDA_MCP_CONFIG</code> in the REA
|
||||
registration's environment and select IDA when opening the
|
||||
binary.
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,8 +285,13 @@
|
||||
</div>
|
||||
<p>
|
||||
Replace <code>main</code> with the function name or address you
|
||||
want to inspect. The first query imports and analyzes the target,
|
||||
so it can take longer than later requests.
|
||||
want to inspect. Each CLI invocation imports and analyzes the
|
||||
target in its own session. Your agent's MCP queries reuse the
|
||||
imported binary while that session is open.
|
||||
</p>
|
||||
<p>
|
||||
<a href="../guides/native/">Follow a native analysis example</a>
|
||||
to see how the function result leads to a recovered calculation.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -273,12 +335,10 @@
|
||||
<p>The report gives the failed check and a suggested next step.</p>
|
||||
<ul class="reading-list">
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/morluto/rea/blob/main/docs/installation.md"
|
||||
>Installation and troubleshooting</a
|
||||
<a href="../guides/">Choose an investigation guide</a
|
||||
><span
|
||||
>Client configuration, versions and provider
|
||||
requirements.</span
|
||||
>Worked examples for a binary, an Electron app and a
|
||||
browser.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Browser observation — REA</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Observe a Notes export with REA: select a browser page, capture a click, inspect the request and follow the export script."
|
||||
/>
|
||||
<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="../../showcase/dx-ball/">DX-Ball</a
|
||||
><a class="nav-start" href="../../get-started/">Get started</a
|
||||
><a href="../">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="../">Guides</a> / Browser
|
||||
observation
|
||||
</p>
|
||||
<h1>See what happens<br />when you click.</h1>
|
||||
<p class="lede">
|
||||
Use a small browser app to connect an action, its network request and
|
||||
the JavaScript that builds a download.
|
||||
</p>
|
||||
</div>
|
||||
<div class="article-layout">
|
||||
<nav class="contents" aria-label="On this page">
|
||||
<p>In this guide</p>
|
||||
<ul>
|
||||
<li><a href="#example">The example</a></li>
|
||||
<li><a href="#prepare">Open the browser</a></li>
|
||||
<li><a href="#ask">Ask your agent</a></li>
|
||||
<li><a href="#capture">Capture from CLI</a></li>
|
||||
<li><a href="#result">Read and follow</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<article class="article" aria-label="Browser observation">
|
||||
<section class="article-section" id="example">
|
||||
<h2>What happens when you click Export?</h2>
|
||||
<p>
|
||||
The
|
||||
<a href="../../examples/notes-web/">Notes browser example</a> has
|
||||
an <strong>Export notes</strong> button. Clicking it loads
|
||||
<code>notes.json</code>, formats the two notes as CSV, and starts
|
||||
a download.
|
||||
</p>
|
||||
<p>
|
||||
We want to see which request the click makes, then use the script
|
||||
to understand how the response becomes a file.
|
||||
</p>
|
||||
<figure class="workflow-figure">
|
||||
<p class="figure-title">One action, captured while it happens</p>
|
||||
<ol class="flow-steps">
|
||||
<li>
|
||||
<span class="flow-label">01 · You</span
|
||||
><span class="flow-node">Click Export notes</span
|
||||
><span class="flow-note"
|
||||
>Perform the action in your browser while the capture is
|
||||
running.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="flow-label">02 · REA</span
|
||||
><span class="flow-node">Record the request</span
|
||||
><span class="flow-note"
|
||||
>Capture the request URL, response status and JSON structure
|
||||
from the selected page.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="flow-label">03 · Your agent</span
|
||||
><span class="flow-node">Read the export code</span
|
||||
><span class="flow-note"
|
||||
>Connect the observed request to
|
||||
<code>fetch("./notes.json")</code> in
|
||||
<code>export.js</code>.</span
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
<p class="flow-result">
|
||||
<code>GET notes.json → 200 → notes[] → CSV</code>
|
||||
</p>
|
||||
<figcaption>
|
||||
The request and response metadata come from the capture. CSV
|
||||
construction is explained by reading the script.
|
||||
</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
<section class="article-section" id="prepare">
|
||||
<h2>Open an observable browser</h2>
|
||||
<p>
|
||||
Start a Chrome-family browser with a dedicated profile and a local
|
||||
debugging port. On Linux, for this site's local preview:
|
||||
</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>Start the example browser</span
|
||||
><button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="browser-start"
|
||||
aria-label="Copy start the example browser"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre><code id="browser-start">google-chrome \
|
||||
--remote-debugging-port=9222 \
|
||||
--user-data-dir=/tmp/rea-notes-browser \
|
||||
http://127.0.0.1:4173/examples/notes-web/</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Use your installed browser's executable path on macOS or Windows.
|
||||
The page URL should point to your local preview or
|
||||
<a href="../../examples/notes-web/">the hosted Notes example</a>.
|
||||
</p>
|
||||
<p>
|
||||
REA connects to this existing browser. You keep control of the
|
||||
page, perform the action, and close the browser when finished.
|
||||
</p>
|
||||
</section>
|
||||
<section class="article-section" id="ask">
|
||||
<h2>Ask your agent to watch one action</h2>
|
||||
<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="browser-prompt"
|
||||
aria-label="Copy example prompt"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p class="terminal-prompt">
|
||||
<span class="prompt-symbol" aria-hidden="true">›</span
|
||||
><span id="browser-prompt"
|
||||
>Use REA to inspect this page while I click Export notes. What
|
||||
request does it make, and how is the CSV built?</span
|
||||
><span class="terminal-cursor" aria-hidden="true"></span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="caption">
|
||||
Example prompt. Give your agent the debugging endpoint,
|
||||
<code>http://127.0.0.1:9222</code>, and the page URL. Let it start
|
||||
the capture before you click.
|
||||
</p>
|
||||
<p>
|
||||
Your agent discovers the page target, starts an observation
|
||||
window, and reads the resulting evidence. It can then inspect the
|
||||
script behind the export.
|
||||
</p>
|
||||
</section>
|
||||
<section class="article-section" id="capture">
|
||||
<h2>Or capture from the terminal</h2>
|
||||
<p>Find the page's target ID:</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>Find the page</span
|
||||
><button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="browser-targets"
|
||||
aria-label="Copy find the page"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre><code id="browser-targets">npx -y rea-agents@latest list-browser-targets \
|
||||
http://127.0.0.1:9222 --json</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Choose the target with the Notes example URL. Replace
|
||||
<code>TARGET_ID</code> below with its ID, then run:
|
||||
</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>Capture the next ten seconds</span
|
||||
><button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="browser-capture"
|
||||
aria-label="Copy capture the next ten seconds"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre><code id="browser-capture">npx -y rea-agents@latest inspect-web-page \
|
||||
http://127.0.0.1:9222 TARGET_ID \
|
||||
--observation-ms 10000 \
|
||||
--include-accessibility-text \
|
||||
--include-json-body-shapes --json</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Let the command connect for a few seconds, then click
|
||||
<strong>Export notes</strong> in that browser. The result arrives
|
||||
after the observation window ends.
|
||||
</p>
|
||||
<p>
|
||||
The captured result includes the selected page, network activity
|
||||
during that window, and the shape of the JSON response. Earlier
|
||||
requests are outside this capture.
|
||||
</p>
|
||||
</section>
|
||||
<section class="article-section" id="result">
|
||||
<h2>Read the result, then follow the code</h2>
|
||||
<div class="evidence-table-wrap">
|
||||
<table class="evidence-table">
|
||||
<caption>
|
||||
Recorded with REA 4.1.0 on the local Notes example
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Captured fact</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Request</td>
|
||||
<td><code>GET /examples/notes-web/notes.json</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Response</td>
|
||||
<td><code>200 · application/json</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Request initiator</td>
|
||||
<td><code>export.js</code> · script</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Response structure</td>
|
||||
<td>
|
||||
<code>notes[]</code> with <code>id</code> and
|
||||
<code>title</code> fields
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>Read the loaded scripts</span
|
||||
><button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="browser-scripts"
|
||||
aria-label="Copy read the loaded scripts"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre><code id="browser-scripts">npx -y rea-agents@latest inspect-web-page \
|
||||
http://127.0.0.1:9222 TARGET_ID \
|
||||
--include-script-sources --json</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
The request identifies the data source, and REA records
|
||||
<code>export.js</code> as its initiator. Read the captured script
|
||||
source to see how the data is used:
|
||||
</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>export.js · example source excerpt</span>
|
||||
</div>
|
||||
<pre><code>const response = await fetch("./notes.json");
|
||||
const { notes } = await response.json();
|
||||
// Format each note as a CSV row.
|
||||
const url = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
|
||||
link.href = url;
|
||||
link.download = "notes.csv";
|
||||
link.click();</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
The download is constructed in the page using the JSON response.
|
||||
The CSV formatting step comes from the script; the network capture
|
||||
establishes that this click requested <code>notes.json</code>.
|
||||
</p>
|
||||
<p>
|
||||
<a href="../../examples/notes-web/">Open the example</a> ·
|
||||
<a href="../../examples/notes-web/export.js" download
|
||||
>Download export.js</a
|
||||
>
|
||||
·
|
||||
<a href="../../examples/notes-web/notes.json" download
|
||||
>Download notes.json</a
|
||||
>
|
||||
</p>
|
||||
<blockquote class="prompt">
|
||||
<p>
|
||||
Find the script that requests <code>notes.json</code>. How does
|
||||
it escape titles containing commas or quotation marks?
|
||||
</p>
|
||||
</blockquote>
|
||||
<p>
|
||||
For an installed JavaScript or Electron app, continue with the
|
||||
<a href="../javascript/">application workflow</a> to follow files
|
||||
and process boundaries.
|
||||
</p>
|
||||
</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>
|
||||
@@ -0,0 +1,119 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Guides — REA</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Learn REA through short worked examples: a native calculation, an Electron CSV export and a browser request."
|
||||
/>
|
||||
<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="../showcase/dx-ball/">DX-Ball</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>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb"><a href="../">Home</a> / Guides</p>
|
||||
<h1>Start with<br />an example.</h1>
|
||||
<p class="lede">
|
||||
Follow an example from the first prompt to the evidence. Then use the
|
||||
same approach with your own app.
|
||||
</p>
|
||||
</div>
|
||||
<div class="guide-index">
|
||||
<section class="guide-entry">
|
||||
<p class="eyebrow">Native binaries</p>
|
||||
<div>
|
||||
<h2>Recover a sound-pan calculation</h2>
|
||||
<p>
|
||||
Start with a Windows game executable. Inspect instructions, follow
|
||||
a caller, read numeric constants and check the recovered C.
|
||||
</p>
|
||||
<div class="guide-route">
|
||||
<span>Instructions</span> <b aria-hidden="true">→</b>
|
||||
<span>Caller</span> <b aria-hidden="true">→</b>
|
||||
<span>Constants</span> <b aria-hidden="true">→</b>
|
||||
<span>C function</span>
|
||||
</div>
|
||||
<a class="text-link" href="native/">Native analysis guide</a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="guide-entry">
|
||||
<p class="eyebrow">JavaScript & Electron</p>
|
||||
<div>
|
||||
<h2>Follow a CSV export</h2>
|
||||
<p>
|
||||
A small Notes app spreads one feature across the renderer, preload
|
||||
and main process. Use REA to connect the API, IPC channel and file
|
||||
writer.
|
||||
</p>
|
||||
<div class="guide-route">
|
||||
<span>Button</span> <b aria-hidden="true">→</b>
|
||||
<span>Preload API</span> <b aria-hidden="true">→</b>
|
||||
<span>IPC</span> <b aria-hidden="true">→</b> <span>CSV file</span>
|
||||
</div>
|
||||
<a class="text-link" href="javascript/">Application workflows</a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="guide-entry">
|
||||
<p class="eyebrow">Browser behavior</p>
|
||||
<div>
|
||||
<h2>Observe a click</h2>
|
||||
<p>
|
||||
Open a working Notes example, capture an export request and find
|
||||
the code that turns its response into a download.
|
||||
</p>
|
||||
<div class="guide-route">
|
||||
<span>Your click</span> <b aria-hidden="true">→</b>
|
||||
<span>Captured request</span> <b aria-hidden="true">→</b>
|
||||
<span>Export script</span>
|
||||
</div>
|
||||
<a class="text-link" href="browser/">Browser observation guide</a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="guide-entry">
|
||||
<p class="eyebrow">First investigation</p>
|
||||
<div>
|
||||
<h2>Connect your agent, then ask.</h2>
|
||||
<p>
|
||||
Each guide starts with a plain-language question and follows a
|
||||
concrete example. If you haven't connected REA yet, setup takes
|
||||
you through the client and provider choices.
|
||||
</p>
|
||||
<a class="text-link" href="../get-started/">Get started with REA</a>
|
||||
</div>
|
||||
</section>
|
||||
</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>
|
||||
@@ -0,0 +1,315 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>JavaScript & Electron — REA</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Trace a Notes CSV export through a renderer API, preload and IPC handler with REA. Includes downloadable example files and a verified analysis result."
|
||||
/>
|
||||
<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="../../showcase/dx-ball/">DX-Ball</a
|
||||
><a class="nav-start" href="../../get-started/">Get started</a
|
||||
><a href="../">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="../">Guides</a> / JavaScript
|
||||
& Electron
|
||||
</p>
|
||||
<h1>Follow a feature<br />across the app.</h1>
|
||||
<p class="lede">
|
||||
Use a CSV export to see how REA connects JavaScript files, exposed
|
||||
APIs and Electron process boundaries.
|
||||
</p>
|
||||
</div>
|
||||
<div class="article-layout">
|
||||
<nav class="contents" aria-label="On this page">
|
||||
<p>In this guide</p>
|
||||
<ul>
|
||||
<li><a href="#example">The example</a></li>
|
||||
<li><a href="#ask">Ask your agent</a></li>
|
||||
<li><a href="#follow">Connect the pieces</a></li>
|
||||
<li><a href="#try">Try the example</a></li>
|
||||
<li><a href="#next">Continue investigating</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<article class="article" aria-label="JavaScript & Electron">
|
||||
<section class="article-section" id="example">
|
||||
<h2>Where does “Export notes” go?</h2>
|
||||
<p>
|
||||
This guide uses a small Electron app. Clicking
|
||||
<strong>Export notes</strong> calls an API in the page, crosses
|
||||
into the main process, and writes a CSV. The behavior is spread
|
||||
across several files.
|
||||
</p>
|
||||
<figure class="workflow-figure">
|
||||
<p class="figure-title">A feature across two processes</p>
|
||||
<ol class="flow-steps">
|
||||
<li>
|
||||
<span class="flow-label">Renderer → preload</span
|
||||
><span class="flow-node"><code>notes.exportCsv()</code></span
|
||||
><span class="flow-note"
|
||||
><code>renderer.js</code> calls the API exposed by
|
||||
<code>preload.js</code>.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="flow-label">Between processes</span
|
||||
><span class="flow-node"><code>notes:export</code></span
|
||||
><span class="flow-note"
|
||||
>The preload invokes an IPC channel: a named message to the
|
||||
main process.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="flow-label">Main process</span
|
||||
><span class="flow-node">An export handler</span
|
||||
><span class="flow-note"
|
||||
><code>main.js</code> handles that channel and calls the CSV
|
||||
writer.</span
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
<p class="flow-result">
|
||||
<code>csv.js</code> formats the notes →
|
||||
<code>main.js</code> writes <code>downloads/notes.csv</code>.
|
||||
</p>
|
||||
<figcaption>
|
||||
REA matches the literal IPC call to its handler and identifies
|
||||
the exposed API. The path is inferred from code; a runtime
|
||||
capture can check what executes.
|
||||
</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
<section class="article-section" id="ask">
|
||||
<h2>Give your agent one feature</h2>
|
||||
<p>
|
||||
<a href="../../get-started/#agent">Connect your agent to REA</a>
|
||||
and give it the app directory or ASAR file:
|
||||
</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="javascript-prompt"
|
||||
aria-label="Copy example prompt"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p class="terminal-prompt">
|
||||
<span class="prompt-symbol" aria-hidden="true">›</span
|
||||
><span id="javascript-prompt"
|
||||
>Use REA to find what happens when I export notes in this app.
|
||||
Where is the CSV created?</span
|
||||
><span class="terminal-cursor" aria-hidden="true"></span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="caption">
|
||||
Example prompt. Include the absolute path to the application
|
||||
folder or <code>app.asar</code>.
|
||||
</p>
|
||||
<p>
|
||||
REA reads the application files and builds a map of entrypoints,
|
||||
modules and process boundaries. Your agent uses that map to narrow
|
||||
the investigation to the export feature.
|
||||
</p>
|
||||
</section>
|
||||
<section class="article-section" id="follow">
|
||||
<h2>Connect the pieces</h2>
|
||||
<h3>The page calls a small API</h3>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>renderer.js · example source</span>
|
||||
</div>
|
||||
<pre><code>document.querySelector("#export").addEventListener("click", async () => {
|
||||
const destination = await window.notes.exportCsv();
|
||||
document.querySelector("#status").textContent = `Saved to ${destination}`;
|
||||
});</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
The page knows the method name. The implementation lives in the
|
||||
preload, which exposes it to the renderer.
|
||||
</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>preload.js · example source</span>
|
||||
</div>
|
||||
<pre><code>contextBridge.exposeInMainWorld("notes", {
|
||||
exportCsv: () => ipcRenderer.invoke("notes:export"),
|
||||
});</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
REA records <code>notes.exportCsv</code> and the literal channel
|
||||
<code>notes:export</code>. It pairs that invocation with the
|
||||
single matching handler in the main process.
|
||||
</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>main.js · example source</span>
|
||||
</div>
|
||||
<pre><code>ipcMain.handle("notes:export", () => {
|
||||
const destination = path.join(app.getPath("downloads"), "notes.csv");
|
||||
writeFileSync(destination, toCsv(notes), "utf8");
|
||||
return destination;
|
||||
});</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Reading the handler answers the original question:
|
||||
<code>csv.js</code> formats the notes, and the main process writes
|
||||
<code>notes.csv</code> into the user's Downloads folder.
|
||||
</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>REA result · summary.ipc excerpt</span>
|
||||
</div>
|
||||
<pre><code>{
|
||||
"literal_channels": 1,
|
||||
"renderer_transmissions": 1,
|
||||
"main_handlers": 1,
|
||||
"paired_renderer_transmissions": 1,
|
||||
"ambiguous_renderer_transmissions": 0
|
||||
}</code></pre>
|
||||
</div>
|
||||
<p class="caption">
|
||||
Recorded by REA 4.1.0 on the downloadable example below. Source
|
||||
locations accompany the API, channel and handler findings.
|
||||
</p>
|
||||
</section>
|
||||
<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.
|
||||
</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>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>Static application analysis</span
|
||||
><button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="javascript-command"
|
||||
aria-label="Copy static application analysis"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre><code id="javascript-command">npx -y rea-agents@latest analyze-javascript-application \
|
||||
/absolute/path/to/notes-example --json</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Replace the path with the folder you saved. 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.
|
||||
</p>
|
||||
<p>
|
||||
For your own app, use the same command with its directory or ASAR
|
||||
path. An agent can follow a literal string, API name or IPC
|
||||
channel with REA's feature tracing tools.
|
||||
</p>
|
||||
</section>
|
||||
<section class="article-section" id="next">
|
||||
<h2>Ask the next question</h2>
|
||||
<blockquote class="prompt">
|
||||
<p>
|
||||
Follow <code>notes:export</code> to its handler. Which inputs
|
||||
reach the CSV writer, and what happens if writing the file
|
||||
fails?
|
||||
</p>
|
||||
</blockquote>
|
||||
<p>
|
||||
Use the source locations to inspect those details. Computed
|
||||
channel names and ambiguous handlers can remain unresolved;
|
||||
runtime observations give you another way to investigate them.
|
||||
</p>
|
||||
<ul class="reading-list">
|
||||
<li>
|
||||
<a href="../browser/">Observe a browser export</a
|
||||
><span
|
||||
>Capture a real click and see which request it makes.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../native/">Follow a native function</a
|
||||
><span
|
||||
>Continue into a binary when a feature depends on native
|
||||
code.</span
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
@@ -0,0 +1,276 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Native analysis — REA</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Recover a native calculation with REA: inspect instructions, follow a caller, read constants and check reconstructed C."
|
||||
/>
|
||||
<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="../../showcase/dx-ball/">DX-Ball</a
|
||||
><a class="nav-start" href="../../get-started/">Get started</a
|
||||
><a href="../">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="../">Guides</a> / Native analysis
|
||||
</p>
|
||||
<h1>Recover a calculation.<br />Follow the evidence.</h1>
|
||||
<p class="lede">
|
||||
Use DX-Ball’s sound panning to see how an agent moves from a binary to
|
||||
an explanation and a checked C function.
|
||||
</p>
|
||||
</div>
|
||||
<div class="article-layout">
|
||||
<nav class="contents" aria-label="On this page">
|
||||
<p>In this guide</p>
|
||||
<ul>
|
||||
<li><a href="#example">The example</a></li>
|
||||
<li><a href="#ask">Ask your agent</a></li>
|
||||
<li><a href="#follow">Follow the evidence</a></li>
|
||||
<li><a href="#result">Recover and check</a></li>
|
||||
<li><a href="#try">Use your own binary</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<article class="article" aria-label="Native analysis">
|
||||
<section class="article-section" id="example">
|
||||
<h2>A position becomes a sound</h2>
|
||||
<p>
|
||||
In DX-Ball, a brick hit supplies a horizontal screen position. The
|
||||
sound code turns it into a left/right panning value. We want to
|
||||
recover that calculation from the executable.
|
||||
</p>
|
||||
<p>
|
||||
The starting function, <code>0x00406400</code>, has a short
|
||||
decompiler result: a call to <code>__ftol()</code>. REA also
|
||||
returns its instructions and callers, which give the agent a way
|
||||
to investigate the missing arithmetic.
|
||||
</p>
|
||||
<figure class="workflow-figure">
|
||||
<p class="figure-title">
|
||||
Three pieces of evidence, one calculation
|
||||
</p>
|
||||
<ol class="flow-steps">
|
||||
<li>
|
||||
<span class="flow-label">01 · Instructions</span
|
||||
><span class="flow-node">An integer input</span
|
||||
><span class="flow-note"
|
||||
>A stack read supplies the value used by the floating-point
|
||||
instructions.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="flow-label">02 · Caller</span
|
||||
><span class="flow-node">A screen coordinate</span
|
||||
><span class="flow-note"
|
||||
>The brick-hit caller passes
|
||||
<code>20 + 30 × tile_x</code>.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="flow-label">03 · Data bytes</span
|
||||
><span class="flow-node">The numeric constants</span
|
||||
><span class="flow-note"
|
||||
>Referenced bytes decode to <code>1.5625</code>,
|
||||
<code>500.0</code> and a scale value.</span
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
<p class="flow-result">
|
||||
<code>pan = (x × 1.5625 − 500.0) × scale</code>
|
||||
</p>
|
||||
<figcaption>
|
||||
REA supplies the instructions, call relationship and bytes. The
|
||||
agent interprets them together.
|
||||
</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
<section class="article-section" id="ask">
|
||||
<h2>Ask about the behavior</h2>
|
||||
<p>
|
||||
After
|
||||
<a href="../../get-started/#agent">connecting REA to your agent</a
|
||||
>, provide the local executable and a question:
|
||||
</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="native-prompt"
|
||||
aria-label="Copy example prompt"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p class="terminal-prompt">
|
||||
<span class="prompt-symbol" aria-hidden="true">›</span
|
||||
><span id="native-prompt"
|
||||
>Use REA to find how DX-Ball calculates sound panning. Explain
|
||||
the calculation and show the code.</span
|
||||
><span class="terminal-cursor" aria-hidden="true"></span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="caption">
|
||||
Example prompt. Give your agent the path to your local DX-Ball
|
||||
1.07 executable.
|
||||
</p>
|
||||
<p>
|
||||
You can start with a feature name. Your agent uses REA to locate
|
||||
relevant functions, then follows the evidence with more focused
|
||||
queries.
|
||||
</p>
|
||||
</section>
|
||||
<section class="article-section" id="follow">
|
||||
<h2>Follow what REA returns</h2>
|
||||
<h3>Read the input</h3>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>REA instruction excerpt · 0x406409</span>
|
||||
</div>
|
||||
<pre><code>MOV EAX, dword ptr [EBP + 0x8]
|
||||
MOV dword ptr [EBP + -0xc], EAX
|
||||
FILD dword ptr [EBP + -0xc]</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
The instructions load an integer argument and convert it for
|
||||
floating-point arithmetic. This is the input absent from the first
|
||||
pseudocode view.
|
||||
</p>
|
||||
<h3 class="small-heading">Follow the caller</h3>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>REA caller excerpt · 0x411f5b</span>
|
||||
</div>
|
||||
<pre><code>ADD EAX, 0x14
|
||||
PUSH EAX
|
||||
CALL 0x00406400</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
REA identifies the brick-hit caller. Its earlier instructions
|
||||
multiply the tile coordinate by 30; adding <code>0x14</code> (20)
|
||||
gives the brick's screen position.
|
||||
</p>
|
||||
<h3 class="small-heading">Read the referenced constants</h3>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span
|
||||
>REA byte results · interpreted as little-endian doubles</span
|
||||
>
|
||||
</div>
|
||||
<pre><code>0x420068 000000000000f93f → 1.5625
|
||||
0x420070 0000000000407f40 → 500.0
|
||||
0x4210a0 000000000000f03f → 1.0</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
The arithmetic references these addresses. Reading the bytes
|
||||
supplies the actual values; the instruction order tells the agent
|
||||
how they are used.
|
||||
</p>
|
||||
</section>
|
||||
<section class="article-section" id="result">
|
||||
<h2>Write and check the recovered function</h2>
|
||||
<div class="code-block">
|
||||
<div class="code-header"><span>Recovered C · excerpt</span></div>
|
||||
<pre><code>pan = (double)x;
|
||||
pan = pan * 1.5625;
|
||||
pan = pan - 500.0;
|
||||
pan = pan * dxball_pan_scale;
|
||||
return (DxBallInt)pan;</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
With a scale of 1.0, screen positions 0, 320 and 640 map to −500,
|
||||
0 and 500 before integer conversion. The calculation puts the
|
||||
center of the screen at the center of the stereo field.
|
||||
</p>
|
||||
<p>
|
||||
The agent used REA's findings to write the C. Separate checks
|
||||
compared 3,205 inputs against the original x86 function and
|
||||
reproduced all 63 compiled bytes of the complete function.
|
||||
</p>
|
||||
<p>
|
||||
<a href="../../showcase/dx-ball/#sound-pan"
|
||||
>See the full investigation and assembly-to-C comparison</a
|
||||
>
|
||||
</p>
|
||||
</section>
|
||||
<section class="article-section" id="try">
|
||||
<h2>Try it with your own binary</h2>
|
||||
<p>
|
||||
<a href="../../get-started/#native"
|
||||
>Connect a native analysis provider</a
|
||||
>, then give your agent a local executable or library and a
|
||||
feature to investigate. Useful starting questions include “How is
|
||||
this file parsed?” or “Which function handles this menu action?”
|
||||
</p>
|
||||
<p>
|
||||
If you already know a function name or address, you can query it
|
||||
from the terminal. For a configured Ghidra installation:
|
||||
</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>Function analysis</span
|
||||
><button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="native-command"
|
||||
aria-label="Copy function analysis"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre><code id="native-command">npx -y rea-agents@latest function /absolute/path/to/program main \
|
||||
--provider ghidra --json</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Replace the target path and <code>main</code> with your own
|
||||
function name or address. Each CLI invocation imports and analyzes
|
||||
the binary, then closes its session. Your agent's MCP queries
|
||||
reuse the imported binary while that session is open.
|
||||
</p>
|
||||
<p>
|
||||
Read the pseudocode alongside instructions and references. When a
|
||||
result leaves a call or value unresolved, use that location as the
|
||||
next question.
|
||||
</p>
|
||||
</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>
|
||||
@@ -24,7 +24,7 @@
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="showcase/dx-ball/">DX-Ball</a>
|
||||
<a class="nav-start" href="get-started/">Get started</a>
|
||||
<a href="https://github.com/morluto/rea/tree/main/docs">Docs</a>
|
||||
<a href="guides/">Guides</a>
|
||||
<a href="https://github.com/morluto/rea">GitHub</a>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -150,10 +150,7 @@
|
||||
Inspect functions, strings, references and call relationships in
|
||||
executables and libraries.
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/morluto/rea/blob/main/docs/native-investigation.md"
|
||||
>Native analysis guide</a
|
||||
>
|
||||
<a href="guides/native/">Native analysis guide</a>
|
||||
</div>
|
||||
<div>
|
||||
<h3>JavaScript & Electron</h3>
|
||||
@@ -161,10 +158,7 @@
|
||||
Map modules, routes, IPC and native dependencies from an
|
||||
application folder or ASAR archive.
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/morluto/rea/blob/main/docs/javascript-application-workflows.md"
|
||||
>Application workflows</a
|
||||
>
|
||||
<a href="guides/javascript/">Application workflows</a>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Runtime behavior</h3>
|
||||
@@ -172,10 +166,7 @@
|
||||
Capture selected browser or process activity, then compare the
|
||||
results across runs.
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/morluto/rea/blob/main/docs/browser-observation.md"
|
||||
>Browser observation guide</a
|
||||
>
|
||||
<a href="guides/browser/">Browser observation guide</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="./" aria-current="page">DX-Ball</a>
|
||||
<a class="nav-start" href="../../get-started/">Get started</a>
|
||||
<a href="https://github.com/morluto/rea/tree/main/docs">Docs</a>
|
||||
<a href="../../guides/">Guides</a>
|
||||
<a href="https://github.com/morluto/rea">GitHub</a>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -125,6 +125,34 @@
|
||||
sound code. The investigation needed to recover how that screen
|
||||
coordinate becomes a left/right panning value.
|
||||
</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="dxball-prompt"
|
||||
aria-label="Copy example prompt"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p class="terminal-prompt">
|
||||
<span class="prompt-symbol" aria-hidden="true">›</span
|
||||
><span id="dxball-prompt"
|
||||
>Use REA to find how DX-Ball calculates sound panning. Explain
|
||||
the calculation and show the code.</span
|
||||
><span class="terminal-cursor" aria-hidden="true"></span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="caption">
|
||||
An example prompt for your coding agent, with the local executable
|
||||
available. The investigation below shows the REA queries and
|
||||
evidence behind the answer.
|
||||
</p>
|
||||
<p>
|
||||
The agent used REA to inspect the function, follow its brick-hit
|
||||
caller and read the constants from the executable. Those results
|
||||
@@ -567,10 +595,11 @@
|
||||
><span>Current source, progress and local setup.</span>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/N0zoM1z0/dx-ball/blob/a55dca27ec0a07018c1b2c95ae2be027f7d8c3c4/docs/REA.md"
|
||||
>REA analysis workflow</a
|
||||
><span>Pinned tools, MCP sessions and retained evidence.</span>
|
||||
<a href="../../guides/native/">REA analysis workflow</a
|
||||
><span
|
||||
>Follow the calculation, then use the same approach with your
|
||||
own binary.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
|
||||
Reference in New Issue
Block a user